Skip to content

Commit 895f386

Browse files
authored
refactor: enhance UVE script injection logic (#35128)
This pull request improves the logging and error handling in the UVE script injection process within the HTML page rendering flow. The changes make it easier to trace when and how the UVE script is injected, and ensure that failures in script building are handled gracefully. **Enhanced logging and error handling for UVE script injection:** * Added debug logs to indicate when the UVE script is injected or skipped based on the page mode in the `build` method of `HTMLPageAssetRenderedBuilder` (`dotCMS/src/main/java/com/dotmarketing/portlets/htmlpageasset/business/render/page/HTMLPageAssetRenderedBuilder.java`). * Improved the `injectUVEScript` method to: - Log when the rendered HTML is empty or null and script injection is skipped. - Use a `Try` block to handle exceptions during script building, logging errors and falling back to a default script if needed. - Add debug logs to indicate where the script is injected (before `</body>` or at the end if no `</body>` tag is found). #35127
1 parent 8c32a34 commit 895f386

File tree

16 files changed

+374
-18
lines changed

16 files changed

+374
-18
lines changed

core-web/libs/dotcms-models/src/lib/shared-models.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export const enum FeaturedFlags {
3232
FEATURE_FLAG_UVE_PREVIEW_MODE = 'FEATURE_FLAG_UVE_PREVIEW_MODE',
3333
FEATURE_FLAG_UVE_TOGGLE_LOCK = 'FEATURE_FLAG_UVE_TOGGLE_LOCK',
3434
FEATURE_FLAG_UVE_STYLE_EDITOR = 'FEATURE_FLAG_UVE_STYLE_EDITOR',
35-
FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES = 'FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES'
35+
FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES = 'FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES',
36+
FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION = 'FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION'
3637
}
3738

3839
export const enum DotConfigurationVariables {

core-web/libs/dotcms-webcomponents/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"build": {
3232
"executor": "nx:run-commands",
3333
"options": {
34-
"command": "npx stencil build --config libs/dotcms-webcomponents/stencil.config.ts --prod"
34+
"command": "yarn stencil build --config libs/dotcms-webcomponents/stencil.config.ts --prod"
3535
}
3636
},
3737
"serve": {

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import { ActionPayload, ContentTypeDragPayload } from '../shared/models';
117117
import { UVEStore } from '../store/dot-uve.store';
118118
import * as uveUtils from '../utils';
119119
import { TEMPORAL_DRAG_ITEM } from '../utils';
120+
import { SDK_EDITOR_SCRIPT_SOURCE } from '../utils/ema-legacy-script-injection';
120121

121122
global.URL.createObjectURL = jest.fn(
122123
() => 'blob:http://localhost:3000/12345678-1234-1234-1234-123456789012'
@@ -3481,6 +3482,77 @@ describe('EditEmaEditorComponent', () => {
34813482
});
34823483
});
34833484

3485+
describe('legacy script injection (FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION)', () => {
3486+
let componentStore: InstanceType<typeof UVEStore>;
3487+
3488+
beforeEach(() => {
3489+
componentStore = (
3490+
spectator.component as unknown as {
3491+
uveStore: InstanceType<typeof UVEStore>;
3492+
}
3493+
).uveStore;
3494+
3495+
jest.spyOn(dotPageApiService, 'get').mockReturnValue(of(MOCK_RESPONSE_VTL));
3496+
store.loadPageAsset({ url: 'index', clientHost: null });
3497+
});
3498+
3499+
it('should inject the UVE script when the flag is enabled', () => {
3500+
patchState(componentStore, {
3501+
flags: {
3502+
...componentStore.flags(),
3503+
[FeaturedFlags.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION]: true
3504+
}
3505+
});
3506+
3507+
const iframe = spectator.query(byTestId('iframe')) as HTMLIFrameElement;
3508+
const spyWrite = jest.spyOn(iframe.contentDocument, 'write');
3509+
3510+
iframe.dispatchEvent(new Event('load'));
3511+
spectator.detectChanges();
3512+
3513+
expect(spyWrite).toHaveBeenCalledWith(
3514+
expect.stringContaining(
3515+
`<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`
3516+
)
3517+
);
3518+
});
3519+
3520+
it('should NOT inject the UVE script when the flag is disabled', () => {
3521+
patchState(componentStore, {
3522+
flags: {
3523+
...componentStore.flags(),
3524+
[FeaturedFlags.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION]: false
3525+
}
3526+
});
3527+
3528+
const iframe = spectator.query(byTestId('iframe')) as HTMLIFrameElement;
3529+
const spyWrite = jest.spyOn(iframe.contentDocument, 'write');
3530+
3531+
iframe.dispatchEvent(new Event('load'));
3532+
spectator.detectChanges();
3533+
3534+
expect(spyWrite).toHaveBeenCalledWith(
3535+
expect.not.stringContaining(
3536+
`<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`
3537+
)
3538+
);
3539+
});
3540+
3541+
it('should NOT inject the UVE script when the flag is not set', () => {
3542+
const iframe = spectator.query(byTestId('iframe')) as HTMLIFrameElement;
3543+
const spyWrite = jest.spyOn(iframe.contentDocument, 'write');
3544+
3545+
iframe.dispatchEvent(new Event('load'));
3546+
spectator.detectChanges();
3547+
3548+
expect(spyWrite).toHaveBeenCalledWith(
3549+
expect.not.stringContaining(
3550+
`<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`
3551+
)
3552+
);
3553+
});
3554+
});
3555+
34843556
describe('when pageRender is undefined', () => {
34853557
beforeEach(() => {
34863558
jest.spyOn(dotPageApiService, 'get').mockReturnValue(

core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ import {
115115
insertContentletInContainer,
116116
shouldNavigate
117117
} from '../utils';
118+
import { addEditorPageScript } from '../utils/ema-legacy-script-injection';
118119

119120
// Message keys constants
120121
const MESSAGE_KEY = {
@@ -565,18 +566,30 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy, AfterViewInit
565566
}
566567

567568
/**
568-
* Inject the editor page styles to the VTL content
569+
* Inject the editor page styles (and optionally the legacy UVE script)
570+
* into the VTL content before writing it to the iframe.
571+
*
572+
* When `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION` is enabled, the
573+
* `dot-uve.js` script tag is also injected. This restores the pre-PR
574+
* #34995 behavior for EMA customers whose headless frontends do not
575+
* consume the backend-rendered output where the script now lives.
569576
*
570577
* @private
571-
* @param {string} html
572-
* @return {*} {string}
573-
* @memberof EditEmaEditorComponent
578+
* @param html - Raw rendered HTML from the Page API.
579+
* @returns Transformed HTML ready for `document.write()`.
580+
* @see {@link addEditorPageScript} for the legacy script injection logic.
574581
*/
575582
private injectCodeToVTL(html: string): string {
576583
const url = this.uveStore.pageAPIResponse()?.page?.pageURI ?? '';
577584
const origin = this.window.location.origin;
578585
const fileWithBase = injectBaseTag({ html, url, origin });
579-
return this.addCustomStyles(fileWithBase);
586+
const fileWithStyles = this.addCustomStyles(fileWithBase);
587+
588+
if (this.uveStore.$isEmaLegacyScriptInjectionEnabled()) {
589+
return addEditorPageScript(fileWithStyles);
590+
}
591+
592+
return fileWithStyles;
580593
}
581594

582595
ngOnDestroy(): void {
@@ -747,9 +760,11 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy, AfterViewInit
747760
/**
748761
* Sets up the iframe content for traditional (VTL) pages.
749762
*
750-
* NOTE: The `dot-uve.js` editor script is intentionally NOT injected here.
751-
* It is now included by the backend directly in `entity.page.rendered`
752-
* (see PR #34927). Do not re-add script injection on the frontend.
763+
* The `dot-uve.js` editor script is normally included by the backend
764+
* directly in `entity.page.rendered` (see PR #34927). However, when
765+
* `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION` is enabled, the script
766+
* is also injected from the frontend to support EMA customers whose
767+
* headless setup does not consume the backend-rendered output.
753768
*
754769
* @memberof EditEmaEditorComponent
755770
*/

core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ export const DEFAULT_PERSONA: DotCMSViewAsPersona = {
7676
export const UVE_FEATURE_FLAGS = [
7777
FeaturedFlags.FEATURE_FLAG_UVE_TOGGLE_LOCK,
7878
FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR,
79-
FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES
79+
FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES,
80+
FeaturedFlags.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION
8081
];
8182

8283
export const DEFAULT_DEVICE: DotDeviceListItem = {

core-web/libs/portlets/edit-ema/portlet/src/lib/shared/mocks.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1044,7 +1044,8 @@ export const dotPropertiesServiceMock = {
10441044
[FeaturedFlags.FEATURE_FLAG_UVE_PREVIEW_MODE]: false,
10451045
[FeaturedFlags.FEATURE_FLAG_UVE_TOGGLE_LOCK]: false,
10461046
[FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR]: false,
1047-
[FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES]: false
1047+
[FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR_FOR_TRADITIONAL_PAGES]: false,
1048+
[FeaturedFlags.FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION]: false
10481049
})
10491050
};
10501051

core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/withPageContext.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface PageContextComputed {
1616
$isPageLocked: Signal<boolean>;
1717
$isLockFeatureEnabled: Signal<boolean>;
1818
$isStyleEditorEnabled: Signal<boolean>;
19+
$isEmaLegacyScriptInjectionEnabled: Signal<boolean>;
1920
$hasAccessToEditMode: Signal<boolean>;
2021
$languageId: Signal<number>;
2122
$isPreviewMode: Signal<boolean>;
@@ -60,6 +61,9 @@ export function withPageContext() {
6061

6162
return flags().FEATURE_FLAG_UVE_STYLE_EDITOR;
6263
});
64+
const $isEmaLegacyScriptInjectionEnabled = computed(
65+
() => flags().FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION === true
66+
);
6367
const $isPageLocked = computed(() => {
6468
return computeIsPageLocked(page(), currentUser());
6569
});
@@ -79,6 +83,7 @@ export function withPageContext() {
7983
$isPageLocked,
8084
$isLockFeatureEnabled,
8185
$isStyleEditorEnabled,
86+
$isEmaLegacyScriptInjectionEnabled,
8287
$hasAccessToEditMode,
8388
$languageId: computed(() => viewAs()?.language?.id || 1),
8489
$pageURI: computed(() => page()?.pageURI ?? ''),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { addEditorPageScript, SDK_EDITOR_SCRIPT_SOURCE } from './ema-legacy-script-injection';
2+
3+
describe('ema-legacy-script-injection', () => {
4+
describe('SDK_EDITOR_SCRIPT_SOURCE', () => {
5+
it('should point to the UVE editor script path', () => {
6+
expect(SDK_EDITOR_SCRIPT_SOURCE).toBe('/ext/uve/dot-uve.js');
7+
});
8+
});
9+
10+
describe('addEditorPageScript', () => {
11+
const expectedScript = `<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`;
12+
13+
it('should insert the script tag before </body> when body tag exists', () => {
14+
const html = '<html><head></head><body><p>Hello</p></body></html>';
15+
const result = addEditorPageScript(html);
16+
17+
expect(result).toContain(expectedScript + '</body>');
18+
expect(result).toBe(
19+
`<html><head></head><body><p>Hello</p>${expectedScript}</body></html>`
20+
);
21+
});
22+
23+
it('should append the script tag at the end when no body tag exists', () => {
24+
const html = '<div>Advanced template content</div>';
25+
const result = addEditorPageScript(html);
26+
27+
expect(result).toBe(html + expectedScript);
28+
});
29+
30+
it('should handle empty string input', () => {
31+
const result = addEditorPageScript('');
32+
expect(result).toBe(expectedScript);
33+
});
34+
35+
it('should handle undefined input (defaults to empty string)', () => {
36+
const result = addEditorPageScript();
37+
expect(result).toBe(expectedScript);
38+
});
39+
});
40+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* @module ema-legacy-script-injection
3+
*
4+
* @deprecated This module is a temporary workaround and will be removed once
5+
* EMA (Edit Mode Anywhere) headless editing is fully sunset.
6+
*
7+
* ## Why this exists
8+
*
9+
* PR #34927 moved the `dot-uve.js` editor script injection from the Angular
10+
* frontend to the backend, embedding it directly in `entity.page.rendered`.
11+
* PR #34995 then removed the frontend injection code.
12+
*
13+
* This broke EMA-based customers (e.g. Freshdesk #36029) because EMA runs
14+
* headless — the iframe loads the customer's own frontend (Next.js, etc.)
15+
* and does **not** consume the backend-rendered HTML that now contains the
16+
* script. As a result, the UVE editor controls stopped appearing in edit mode.
17+
*
18+
* ## How it works
19+
*
20+
* When the backend feature flag `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION` is
21+
* set to `true`, the Angular editor re-injects the `dot-uve.js` script tag
22+
* into VTL page content before writing it to the iframe — restoring the
23+
* pre-PR #34995 behavior.
24+
*
25+
* ## Removal plan
26+
*
27+
* Once all EMA customers have migrated to the Universal Visual Editor (UVE),
28+
* delete this file and remove:
29+
* - `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION` from `FeaturedFlags` enum
30+
* - The flag entry from `UVE_FEATURE_FLAGS` in `consts.ts`
31+
* - `$isEmaLegacyScriptInjectionEnabled` from `withPageContext.ts`
32+
* - The conditional call in `EditEmaEditorComponent.injectCodeToVTL`
33+
*
34+
* @see {@link https://github.com/dotCMS/core/pull/34927 PR #34927 — backend script injection}
35+
* @see {@link https://github.com/dotCMS/core/pull/34995 PR #34995 — frontend injection removal}
36+
*/
37+
38+
/**
39+
* Path to the UVE editor script served by the dotCMS backend.
40+
*
41+
* @deprecated Gated behind `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION`.
42+
*/
43+
export const SDK_EDITOR_SCRIPT_SOURCE = '/ext/uve/dot-uve.js';
44+
45+
/**
46+
* Injects the `dot-uve.js` editor script tag into the rendered HTML content.
47+
*
48+
* If a `</body>` tag exists, the script is inserted just before it.
49+
* For advanced templates that may lack a `<body>`, the script is appended
50+
* at the end of the content.
51+
*
52+
* @deprecated Gated behind `FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION`.
53+
* @param rendered - The HTML string to inject the script into.
54+
* @returns The HTML string with the editor script tag added.
55+
*/
56+
export function addEditorPageScript(rendered = ''): string {
57+
const scriptString = `<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`;
58+
const bodyExists = rendered.includes('</body>');
59+
60+
if (!bodyExists) {
61+
return rendered + scriptString;
62+
}
63+
64+
return rendered.replace('</body>', scriptString + '</body>');
65+
}

core-web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,5 +280,6 @@
280280
},
281281
"nx": {
282282
"includedScripts": []
283-
}
283+
},
284+
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
284285
}

0 commit comments

Comments
 (0)