From a0f40d23b181519a0d0e4d7dc0c65f3edf7243bd Mon Sep 17 00:00:00 2001
From: Yahor Chaptsou
Date: Thu, 23 Apr 2026 19:55:40 +0200
Subject: [PATCH 1/6] [SITES-41323] improve cfv1 perfromance
---
.../clientlibs/editor/dialog/js/editDialog.js | 68 ++++++++++++++++---
1 file changed, 59 insertions(+), 9 deletions(-)
diff --git a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js
index ce52274332..77970a2486 100644
--- a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js
+++ b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js
@@ -62,6 +62,50 @@
var elementsController;
+ function pathExternalizesToCurrentHost(path) {
+ if (path === undefined || path === null) {
+ return false;
+ }
+ var str = String(path).trim();
+ if (str.length === 0) {
+ return false;
+ }
+ var lowerStr = str.toLowerCase();
+ if (
+ lowerStr.indexOf("javascript:") === 0 ||
+ lowerStr.indexOf("data:") === 0 ||
+ lowerStr.indexOf("vbscript:") === 0
+ ) {
+ return false;
+ }
+ var decoded;
+ try {
+ decoded = decodeURIComponent(str.split("+").join(" "));
+ } catch (e) {
+ return false;
+ }
+ if (decoded.indexOf("..") !== -1) {
+ return false;
+ }
+ var external = Granite.HTTP.externalize(str);
+ var resolved;
+ try {
+ resolved = new URL(external, window.location.href);
+ } catch (ex) {
+ return false;
+ }
+ return resolved.origin === window.location.origin;
+ }
+
+ function innerHtmlFromMarkupFirstWrapper(markup) {
+ var doc = new DOMParser().parseFromString(markup, "text/html");
+ var body = doc.body;
+ if (!body || !body.firstElementChild) {
+ return "";
+ }
+ return body.firstElementChild.innerHTML;
+ }
+
/**
* A class which encapsulates the logic related to element selectors and variation name selector.
*/
@@ -148,6 +192,10 @@
if (typeof displayMode === "undefined") {
displayMode = editDialog.querySelector(SELECTOR_DISPLAY_MODE_CHECKED).value;
}
+ var pathForRequest = type === "variation" ? this.variationNamePath : this.elementsContainerPath;
+ if (!pathExternalizesToCurrentHost(pathForRequest)) {
+ return $.Deferred().reject().promise();
+ }
var data = {
fragmentPath: fragmentPath.value,
displayMode: displayMode
@@ -305,7 +353,7 @@
* @param {String} html - outerHTML value for elementNamesContainer
*/
ElementsController.prototype._updateElementsHTML = function(html) {
- this.elementNamesContainer.innerHTML = $(html)[0].innerHTML;
+ this.elementNamesContainer.innerHTML = innerHtmlFromMarkupFirstWrapper(html);
this._updateFields();
};
@@ -318,14 +366,15 @@
*/
ElementsController.prototype._updateElementsDOM = function(dom) {
this._updateFields();
- if (dom.tagName === "CORAL-MULTIFIELD") {
+ var element = dom.ownerDocument !== document ? document.importNode(dom, true) : dom;
+ if (element.tagName === "CORAL-MULTIFIELD") {
// replace the element names multifield's template
- this.elementNames.template = dom.template;
+ this.elementNames.template = element.template;
} else {
this._clearValidationError(this.singleTextSelector);
- dom.value = this.singleTextSelector.value;
- this.singleTextSelector.parentNode.replaceChild(dom, this.singleTextSelector);
- this.singleTextSelector = dom;
+ element.value = this.singleTextSelector.value;
+ this.singleTextSelector.parentNode.replaceChild(element, this.singleTextSelector);
+ this.singleTextSelector = element;
this.singleTextSelector.removeAttribute("disabled");
}
this._updateFields();
@@ -353,9 +402,10 @@
*/
ElementsController.prototype._updateVariationDOM = function(dom) {
// replace the variation name select, keeping its value
- dom.value = this.variationName.value;
- this.variationName.parentNode.replaceChild(dom, this.variationName);
- this.variationName = dom;
+ var element = dom.ownerDocument !== document ? document.importNode(dom, true) : dom;
+ element.value = this.variationName.value;
+ this.variationName.parentNode.replaceChild(element, this.variationName);
+ this.variationName = element;
this.variationName.removeAttribute("disabled");
this._updateFields();
};
From c8ec1f8b4bf871490b6da650d010795f9b8cb94c Mon Sep 17 00:00:00 2001
From: Yahor Chaptsou
Date: Wed, 13 May 2026 13:02:16 +0200
Subject: [PATCH 2/6] [SITES-41323] cf v1 performance improve
---
content/karma.conf.js | 4 +-
.../authoringutils/js/authoringMarkupUtils.js | 80 ++++++++++++++++++-
.../clientlibs/editor/dialog/.content.xml | 3 +-
.../clientlibs/editor/dialog/js/editDialog.js | 77 +++++++++++++++++-
.../authoringMarkupUtilsTest.js | 28 +++++++
.../contentfragment/editDialogTest.js | 80 +++++++++++++++++++
content/test/mocks.js | 2 +
7 files changed, 269 insertions(+), 5 deletions(-)
diff --git a/content/karma.conf.js b/content/karma.conf.js
index c291077dd6..132f7190d1 100644
--- a/content/karma.conf.js
+++ b/content/karma.conf.js
@@ -35,14 +35,14 @@ module.exports = function(config) {
'src/content/jcr_root/apps/core/wcm/components/image/v3/image/clientlibs/site/**/js/*.js',
'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/authoring/js/editAction.js',
'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/authoring/js/vcfRenderer.js',
+ 'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/htmlidvalidator/js/*.js',
+ 'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/*.js',
'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js',
{
pattern: 'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/site/js/vcf.js',
included: false,
served: true
},
- 'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/htmlidvalidator/js/*.js',
- 'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/*.js',
'src/content/jcr_root/apps/core/wcm/components/image/v2/image/clientlibs/editor/js/image.js',
'src/content/jcr_root/apps/core/wcm/components/image/v3/image/clientlibs/editor/js/image.js',
'test/**/*Test.js',
diff --git a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
index 18f2684f5f..ed29b5b1b3 100644
--- a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
+++ b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
@@ -99,6 +99,83 @@
);
}
+ /**
+ * Normalizes parsed authoring markup under a root element: drops disallowed subtrees and clears
+ * event-handler and disallowed URL schemes on link-like attributes.
+ *
+ * @param {Element} rootElement - parsed subtree root (typically {@code document.body})
+ */
+ function sanitizeAuthoringMarkupSubtree(rootElement) {
+ if (!rootElement || rootElement.nodeType !== 1) {
+ return;
+ }
+ var all = rootElement.querySelectorAll("*");
+ var list = [];
+ var i;
+ for (i = 0; i < all.length; i++) {
+ list.push(all[i]);
+ }
+ list.push(rootElement);
+ var removeEls = [];
+ for (i = 0; i < list.length; i++) {
+ var el = list[i];
+ if (el.nodeType !== 1) {
+ continue;
+ }
+ var tag = el.tagName;
+ if (tag === "SCRIPT" || tag === "IFRAME" || tag === "OBJECT" || tag === "EMBED") {
+ removeEls.push(el);
+ continue;
+ }
+ var attrs = el.attributes;
+ var names = [];
+ var j;
+ for (j = 0; attrs && j < attrs.length; j++) {
+ names.push(attrs[j].name);
+ }
+ for (j = 0; j < names.length; j++) {
+ var name = names[j];
+ var val = el.getAttribute(name);
+ var nl = name.toLowerCase();
+ if (/^on/i.test(name)) {
+ el.removeAttribute(name);
+ continue;
+ }
+ if (
+ (nl === "href" || nl === "src" || nl === "formaction" || nl === "xlink:href") &&
+ linkValueHasExcludedRepositoryPrefix(val)
+ ) {
+ el.removeAttribute(name);
+ }
+ }
+ }
+ for (i = 0; i < removeEls.length; i++) {
+ var node = removeEls[i];
+ if (node.parentNode) {
+ node.parentNode.removeChild(node);
+ }
+ }
+ }
+
+ /**
+ * Parses datasource HTML and returns the inner markup of the first body child, after subtree
+ * normalization is applied to the parsed document body.
+ *
+ * @param {String} markup - HTML document string from a datasource response
+ * @returns {String} normalized inner markup (empty string when the parsed body has no element child)
+ */
+ function sanitizeAuthoringEditorResponseMarkup(markup) {
+ var doc = parseMarkupDocument(String(markup == null ? "" : markup));
+ if (doc.body) {
+ sanitizeAuthoringMarkupSubtree(doc.body);
+ }
+ var body = doc.body;
+ if (!body || !body.firstElementChild) {
+ return "";
+ }
+ return body.firstElementChild.innerHTML;
+ }
+
function filterClassAttribute(raw, allowedTokens) {
if (!raw || typeof raw !== "string") {
return "";
@@ -236,7 +313,8 @@
innerHtmlFromFirstBodyChild: innerHtmlFromFirstBodyChild,
adoptNodeForDocument: adoptNodeForDocument,
linkValueHasExcludedRepositoryPrefix: linkValueHasExcludedRepositoryPrefix,
- buildPageImageThumbnailShellForEditor: buildPageImageThumbnailShellForEditor
+ buildPageImageThumbnailShellForEditor: buildPageImageThumbnailShellForEditor,
+ sanitizeAuthoringEditorResponseMarkup: sanitizeAuthoringEditorResponseMarkup
};
})(window);
diff --git a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/.content.xml b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/.content.xml
index 04103836f4..b2a5a45a8c 100644
--- a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/.content.xml
+++ b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/.content.xml
@@ -1,4 +1,5 @@
+ categories="[core.wcm.components.contentfragment.v1.dialog]"
+ dependencies="[core.wcm.components.commons.editor.authoringutils]"/>
diff --git a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js
index dfb4914b7b..2f1d9c67fb 100644
--- a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js
+++ b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js
@@ -49,6 +49,71 @@
// ui helper
var ui = $(window).adaptTo("foundation-ui");
+ var CT_SITES_41323 = "CT_SITES-41323";
+
+ /**
+ * Granite feature toggle CT_SITES-41323 gates datasource HTML handling for the element names container.
+ * When Granite or {@code Toggles} is unavailable, helpers are treated as enabled.
+ * When the toggle is explicitly disabled, behaviour matches earlier dialog revisions.
+ *
+ * @returns {Boolean}
+ */
+ function isContentFragmentV1DialogAuthoringMarkupHelpersEnabled() {
+ if (typeof Granite === "undefined" || !Granite.Toggles || typeof Granite.Toggles.isEnabled !== "function") {
+ return true;
+ }
+ return Granite.Toggles.isEnabled(CT_SITES_41323) !== false;
+ }
+
+ /**
+ * @returns {Object|null} {@code CQ.CoreComponents.AuthoringEditorUtils.markup} when present
+ */
+ function getAuthoringMarkupUtils() {
+ if (window.CQ && window.CQ.CoreComponents && window.CQ.CoreComponents.AuthoringEditorUtils) {
+ return window.CQ.CoreComponents.AuthoringEditorUtils.markup;
+ }
+ return null;
+ }
+
+ function extractFirstParsedInnerHtml(html) {
+ if (typeof $ === "function" && typeof $.parseHTML === "function") {
+ var nodes = $.parseHTML(String(html == null ? "" : html), document, true);
+ var i;
+ for (i = 0; i < nodes.length; i++) {
+ if (nodes[i].nodeType === 1) {
+ return nodes[i].innerHTML;
+ }
+ }
+ return "";
+ }
+ var fallbackMarkup = getAuthoringMarkupUtils();
+ if (fallbackMarkup && typeof fallbackMarkup.innerHtmlFromFirstBodyChild === "function") {
+ return fallbackMarkup.innerHtmlFromFirstBodyChild(html);
+ }
+ var first = $(html)[0];
+ if (!first || typeof first.innerHTML !== "string") {
+ return "";
+ }
+ return first.innerHTML;
+ }
+
+ /**
+ * Resolves inner HTML for the element names container from a datasource response string.
+ *
+ * @param {String} html - outer HTML for the element names container
+ * @returns {String}
+ */
+ function resolveElementNamesContainerInnerHtml(html) {
+ if (!isContentFragmentV1DialogAuthoringMarkupHelpersEnabled()) {
+ return extractFirstParsedInnerHtml(html);
+ }
+ var markupUtils = getAuthoringMarkupUtils();
+ if (markupUtils && typeof markupUtils.sanitizeAuthoringEditorResponseMarkup === "function") {
+ return markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
+ }
+ return extractFirstParsedInnerHtml(html);
+ }
+
// dialog texts
var confirmationDialogTitle = Granite.I18n.get("Warning");
var confirmationDialogMessage = Granite.I18n.get("Please confirm replacing the current content fragment and its configuration");
@@ -351,7 +416,7 @@
* @param {String} html - outerHTML value for elementNamesContainer
*/
ElementsController.prototype._updateElementsHTML = function(html) {
- this.elementNamesContainer.innerHTML = $(html)[0].innerHTML;
+ this.elementNamesContainer.innerHTML = resolveElementNamesContainerInnerHtml(html);
this._updateFields();
};
@@ -868,4 +933,14 @@
}
});
+ var cfV1DialogTestApiHost = typeof globalThis !== "undefined" ? globalThis : window;
+ /* Karma (mocks.js) sets __CONTENTFRAGMENT_V1_DIALOG_TEST_API on the global object; AEM runtime leaves it undefined. */
+ if (cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API) {
+ cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API.resolveElementNamesContainerInnerHtml =
+ resolveElementNamesContainerInnerHtml;
+ cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled =
+ isContentFragmentV1DialogAuthoringMarkupHelpersEnabled;
+ cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API.getAuthoringMarkupUtils = getAuthoringMarkupUtils;
+ }
+
})(window, jQuery, jQuery(document), Granite, Coral);
diff --git a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
index d25a020d00..9fedcc8785 100644
--- a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
+++ b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
@@ -154,4 +154,32 @@ describe("AuthoringEditorUtils.markup (core.wcm.components.commons.editor.author
expect(img.getAttribute("src")).toBe("/content/dam/x.png");
});
});
+
+ describe("sanitizeAuthoringEditorResponseMarkup", function() {
+ it("returns inner markup of the first body child with event attributes removed", function() {
+ const html =
+ "";
+ const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
+ expect(out.indexOf("onerror")).toBe(-1);
+ expect(out.indexOf("onmouseover")).toBe(-1);
+ expect(out.indexOf("
")).toBe(-1);
+ expect(out.indexOf(">ok
")).not.toBe(-1);
+ });
+
+ it("removes href with disallowed schemes", function() {
+ const html = "";
+ const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
+ expect(out.indexOf("javascript")).toBe(-1);
+ });
+
+ it("returns empty string when body has no element child", function() {
+ expect(markupUtils.sanitizeAuthoringEditorResponseMarkup("")).toBe("");
+ });
+ });
});
diff --git a/content/test/clientlibs/contentfragment/editDialogTest.js b/content/test/clientlibs/contentfragment/editDialogTest.js
index a1dae3c69f..9f3b8e28ed 100644
--- a/content/test/clientlibs/contentfragment/editDialogTest.js
+++ b/content/test/clientlibs/contentfragment/editDialogTest.js
@@ -13,6 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
+/**
+ * Helpers for {@code Granite.Toggles} during editDialog markup-helper test suites
+ * ({@code globalThis.__CONTENTFRAGMENT_V1_DIALOG_TEST_API}).
+ */
+function editDialogTest_graniteAllOn() {
+ globalThis.Granite.Toggles.isEnabled = function() {
+ return true;
+ };
+}
+
+function editDialogTest_graniteCt41323Off() {
+ globalThis.Granite.Toggles.isEnabled = function(key) {
+ return key !== "CT_SITES-41323";
+ };
+}
+
describe("Test editDialog VCF template retention for", function() {
let channel;
@@ -167,3 +183,67 @@ describe("Test editDialog VCF template retention for", function() {
});
});
+
+/**
+ * Covers Content Fragment v1 editor {@code editDialog.js} (CT_SITES-41323) wired through
+ * {@code globalThis.__CONTENTFRAGMENT_V1_DIALOG_TEST_API}. Karma loads {@code authoringutils} before {@code editDialog.js}.
+ */
+describe("Content Fragment v1 editor editDialog.js (Karma-loaded)", function() {
+ let api;
+ let togglesIsEnabled;
+
+ beforeAll(function() {
+ api = globalThis.__CONTENTFRAGMENT_V1_DIALOG_TEST_API;
+ togglesIsEnabled = globalThis.Granite.Toggles.isEnabled;
+ });
+
+ afterEach(function() {
+ globalThis.Granite.Toggles.isEnabled = togglesIsEnabled;
+ });
+
+ describe("__CONTENTFRAGMENT_V1_DIALOG_TEST_API", function() {
+ it("exposes resolve and toggle helpers", function() {
+ expect(api).toBeDefined();
+ expect(typeof api.resolveElementNamesContainerInnerHtml).toBe("function");
+ expect(typeof api.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled).toBe("function");
+ expect(typeof api.getAuthoringMarkupUtils).toBe("function");
+ });
+ });
+
+ describe("isContentFragmentV1DialogAuthoringMarkupHelpersEnabled", function() {
+ it("treats missing Granite.Toggles as enabled", function() {
+ const saved = globalThis.Granite.Toggles;
+ globalThis.Granite.Toggles = undefined;
+ expect(api.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled()).toBe(true);
+ globalThis.Granite.Toggles = saved;
+ });
+
+ it("returns false when CT_SITES-41323 is explicitly disabled", function() {
+ editDialogTest_graniteCt41323Off();
+ expect(api.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled()).toBe(false);
+ });
+
+ it("returns true when CT_SITES-41323 is enabled", function() {
+ editDialogTest_graniteAllOn();
+ expect(api.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled()).toBe(true);
+ });
+ });
+
+ describe("resolveElementNamesContainerInnerHtml", function() {
+ it("normalizes markup when helpers are enabled", function() {
+ editDialogTest_graniteAllOn();
+ const doc =
+ "";
+ const inner = api.resolveElementNamesContainerInnerHtml(doc);
+ expect(inner.indexOf("onerror")).toBe(-1);
+ });
+
+ it("keeps legacy parsing when helpers are disabled", function() {
+ editDialogTest_graniteCt41323Off();
+ const doc =
+ "";
+ const inner = api.resolveElementNamesContainerInnerHtml(doc);
+ expect(inner.indexOf("onerror")).not.toBe(-1);
+ });
+ });
+});
diff --git a/content/test/mocks.js b/content/test/mocks.js
index 9309133124..178d76f71d 100644
--- a/content/test/mocks.js
+++ b/content/test/mocks.js
@@ -427,3 +427,5 @@ if (!globalThis.CQ.CoreComponents.CheckboxTextfieldTuple) {
/** Filled by Image v2 or v3 editor image.js when present (Karma loads those scripts after mocks). */
globalThis.__IMAGE_V2_EDITOR_TEST_API = {};
globalThis.__IMAGE_V3_EDITOR_TEST_API = {};
+/** Filled by Content Fragment v1 editDialog.js when present (Karma loads that script after mocks). */
+globalThis.__CONTENTFRAGMENT_V1_DIALOG_TEST_API = {};
From f1703a991a0b591bf3f888b97507c2e06c450c12 Mon Sep 17 00:00:00 2001
From: Yahor Chaptsou
Date: Wed, 13 May 2026 13:54:05 +0200
Subject: [PATCH 3/6] [SITES-41323] cf v1 performance improve
---
.../authoringutils/js/authoringMarkupUtils.js | 19 ++++++++++++++++---
.../authoringMarkupUtilsTest.js | 15 +++++++++++++++
2 files changed, 31 insertions(+), 3 deletions(-)
diff --git a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
index ed29b5b1b3..ecc14172c8 100644
--- a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
+++ b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
@@ -39,6 +39,19 @@
"data-thumbnail-config-path"
];
+ /** Element names removed entirely when normalizing parsed authoring datasource markup. */
+ var AUTHORING_MARKUP_STRIPPED_ELEMENT_TAGS = {
+ SCRIPT: true,
+ IFRAME: true,
+ OBJECT: true,
+ EMBED: true,
+ STYLE: true,
+ LINK: true,
+ META: true,
+ BASE: true,
+ FORM: true
+ };
+
/**
* Parses an HTML document string into a Document instance.
*
@@ -100,8 +113,8 @@
}
/**
- * Normalizes parsed authoring markup under a root element: drops disallowed subtrees and clears
- * event-handler and disallowed URL schemes on link-like attributes.
+ * Normalizes parsed authoring markup under a root element: drops disallowed subtrees (including
+ * active content, document-influencing, and styling hooks) and clears event-handler and disallowed URL schemes on link-like attributes.
*
* @param {Element} rootElement - parsed subtree root (typically {@code document.body})
*/
@@ -123,7 +136,7 @@
continue;
}
var tag = el.tagName;
- if (tag === "SCRIPT" || tag === "IFRAME" || tag === "OBJECT" || tag === "EMBED") {
+ if (AUTHORING_MARKUP_STRIPPED_ELEMENT_TAGS[tag]) {
removeEls.push(el);
continue;
}
diff --git a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
index 9fedcc8785..790a621c55 100644
--- a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
+++ b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
@@ -178,6 +178,21 @@ describe("AuthoringEditorUtils.markup (core.wcm.components.commons.editor.author
expect(out.indexOf("javascript")).toBe(-1);
});
+ it("drops style, link, meta, base, and form elements", function() {
+ const html =
+ "";
+ const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
+ expect(out.indexOf("" +
From fa1f046e595f231cf26820e1f274491ff4dca8e8 Mon Sep 17 00:00:00 2001
From: Yahor Chaptsou
Date: Wed, 13 May 2026 14:15:15 +0200
Subject: [PATCH 5/6] [SITES-41323] cf v1 performance improve
---
.../authoringutils/js/authoringMarkupUtils.js | 9 +++++++--
.../authoringutils/authoringMarkupUtilsTest.js | 16 ++++++++++++++++
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
index 4adf8cc27f..aeae7500d0 100644
--- a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
+++ b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
@@ -96,15 +96,20 @@
/**
* Whether a link-like attribute value uses a non-http(s) scheme prefix that authoring dialogs do not treat as repository paths.
+ * Leading C0 control characters, DEL, and whitespace are stripped before the check so values cannot hide schemes from prefix matching.
*
- * @param {*} value - attribute value
+ * @param {*} value - attribute value (typically after DOM parsing, so entities are decoded)
* @returns {Boolean}
*/
function linkValueHasExcludedRepositoryPrefix(value) {
if (value === undefined || value === null) {
return false;
}
- var t = String(value).trim().toLowerCase();
+ // Strip C0 controls (incl. NUL, TAB, LF, CR) and all whitespace so
+ // browser URL-parser normalisations like "java\tscript:" can't bypass
+ // the prefix check. Values reach this helper post-DOM-parse, so HTML
+ // entities are already decoded by the parser.
+ var t = String(value).replace(/[\u0000-\u001F\u007F\s]+/g, "").toLowerCase();
return (
t.indexOf("javascript:") === 0 ||
t.indexOf("data:") === 0 ||
diff --git a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
index 59efe4b8ed..1f1781ef61 100644
--- a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
+++ b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
@@ -41,6 +41,22 @@ describe("AuthoringEditorUtils.markup (core.wcm.components.commons.editor.author
expect(markupUtils.linkValueHasExcludedRepositoryPrefix("DATA:image/png;base64,xx")).toBe(true);
expect(markupUtils.linkValueHasExcludedRepositoryPrefix(" javascript:x ")).toBe(true);
});
+
+ it("returns true when C0 controls or whitespace break up or precede the scheme", function() {
+ expect(markupUtils.linkValueHasExcludedRepositoryPrefix(" javascript:alert(1)")).toBe(true);
+ expect(markupUtils.linkValueHasExcludedRepositoryPrefix("\n\tjavascript:alert(1)")).toBe(true);
+ expect(markupUtils.linkValueHasExcludedRepositoryPrefix("java\tscript:alert(1)")).toBe(true);
+ expect(markupUtils.linkValueHasExcludedRepositoryPrefix("java\nscript:alert(1)")).toBe(true);
+ expect(markupUtils.linkValueHasExcludedRepositoryPrefix("jav\rascript:alert(1)")).toBe(true);
+ expect(markupUtils.linkValueHasExcludedRepositoryPrefix("\u0000javascript:alert(1)")).toBe(true);
+ expect(markupUtils.linkValueHasExcludedRepositoryPrefix("\u0001javascript:alert(1)")).toBe(true);
+ });
+
+ it("returns false for https URLs and nullish values", function() {
+ expect(markupUtils.linkValueHasExcludedRepositoryPrefix("https://example.com/x")).toBe(false);
+ expect(markupUtils.linkValueHasExcludedRepositoryPrefix(null)).toBe(false);
+ expect(markupUtils.linkValueHasExcludedRepositoryPrefix(undefined)).toBe(false);
+ });
});
describe("buildPageImageThumbnailShellForEditor", function() {
From 63d898600c1fc71f33880a4b96ed279bbac57ca3 Mon Sep 17 00:00:00 2001
From: Yahor Chaptsou
Date: Wed, 13 May 2026 14:26:19 +0200
Subject: [PATCH 6/6] [SITES-41323] cf v1 performance improve
---
.../authoringutils/js/authoringMarkupUtils.js | 34 +++++++++++++++----
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
index aeae7500d0..c132b3fc12 100644
--- a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
+++ b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js
@@ -94,22 +94,44 @@
return node;
}
+ /**
+ * Drops ASCII C0 controls, DEL, and whitespace so scheme prefix checks cannot be bypassed with
+ * characters the URL layer may normalise away (e.g. TAB inside {@code javascript:}).
+ *
+ * @param {String} str - raw attribute value
+ * @returns {String} characters kept for scheme prefix checks
+ */
+ function stripAsciiControlsAndWhitespaceForSchemeCheck(str) {
+ var out = "";
+ var i;
+ var ch;
+ var c;
+ for (i = 0; i < str.length; i++) {
+ ch = str.charAt(i);
+ c = str.charCodeAt(i);
+ if (c <= 31 || c === 127) {
+ continue;
+ }
+ if (/\s/.test(ch)) {
+ continue;
+ }
+ out += ch;
+ }
+ return out;
+ }
+
/**
* Whether a link-like attribute value uses a non-http(s) scheme prefix that authoring dialogs do not treat as repository paths.
* Leading C0 control characters, DEL, and whitespace are stripped before the check so values cannot hide schemes from prefix matching.
*
* @param {*} value - attribute value (typically after DOM parsing, so entities are decoded)
- * @returns {Boolean}
+ * @returns {Boolean} true when the normalised value starts with javascript, data, or vbscript
*/
function linkValueHasExcludedRepositoryPrefix(value) {
if (value === undefined || value === null) {
return false;
}
- // Strip C0 controls (incl. NUL, TAB, LF, CR) and all whitespace so
- // browser URL-parser normalisations like "java\tscript:" can't bypass
- // the prefix check. Values reach this helper post-DOM-parse, so HTML
- // entities are already decoded by the parser.
- var t = String(value).replace(/[\u0000-\u001F\u007F\s]+/g, "").toLowerCase();
+ var t = stripAsciiControlsAndWhitespaceForSchemeCheck(String(value)).toLowerCase();
return (
t.indexOf("javascript:") === 0 ||
t.indexOf("data:") === 0 ||