Skip to content

Commit 3653bcc

Browse files
YahorCYahor Chaptsou
andauthored
[SITES-41323] improve cfv1 performance (#3025)
* [SITES-41323] improve cfv1 perfromance * [SITES-41323] cf v1 performance improve * [SITES-41323] cf v1 performance improve * [SITES-41323] cf v1 performance improve * [SITES-41323] cf v1 performance improve * [SITES-41323] cf v1 performance improve --------- Co-authored-by: Yahor Chaptsou <yahorchaptsou@Yahors-MacBook-Pro.local>
1 parent 9b1b59b commit 3653bcc

7 files changed

Lines changed: 355 additions & 8 deletions

File tree

content/karma.conf.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ module.exports = function(config) {
3535
'src/content/jcr_root/apps/core/wcm/components/image/v3/image/clientlibs/site/**/js/*.js',
3636
'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/authoring/js/editAction.js',
3737
'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/authoring/js/vcfRenderer.js',
38+
'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/htmlidvalidator/js/*.js',
39+
'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/*.js',
3840
'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js',
3941
{
4042
pattern: 'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/site/js/vcf.js',
4143
included: false,
4244
served: true
4345
},
44-
'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/htmlidvalidator/js/*.js',
45-
'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/*.js',
4646
'src/content/jcr_root/apps/core/wcm/components/image/v2/image/clientlibs/editor/js/image.js',
4747
'src/content/jcr_root/apps/core/wcm/components/image/v3/image/clientlibs/editor/js/image.js',
4848
'test/**/*Test.js',

content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@
3939
"data-thumbnail-config-path"
4040
];
4141

42+
/** Element names removed entirely when normalizing parsed authoring datasource markup. */
43+
var AUTHORING_MARKUP_STRIPPED_ELEMENT_TAGS = {
44+
SCRIPT: true,
45+
IFRAME: true,
46+
OBJECT: true,
47+
EMBED: true,
48+
STYLE: true,
49+
LINK: true,
50+
META: true,
51+
BASE: true,
52+
FORM: true
53+
};
54+
4255
/**
4356
* Parses an HTML document string into a Document instance.
4457
*
@@ -81,24 +94,134 @@
8194
return node;
8295
}
8396

97+
/**
98+
* Drops ASCII C0 controls, DEL, and whitespace so scheme prefix checks cannot be bypassed with
99+
* characters the URL layer may normalise away (e.g. TAB inside {@code javascript:}).
100+
*
101+
* @param {String} str - raw attribute value
102+
* @returns {String} characters kept for scheme prefix checks
103+
*/
104+
function stripAsciiControlsAndWhitespaceForSchemeCheck(str) {
105+
var out = "";
106+
var i;
107+
var ch;
108+
var c;
109+
for (i = 0; i < str.length; i++) {
110+
ch = str.charAt(i);
111+
c = str.charCodeAt(i);
112+
if (c <= 31 || c === 127) {
113+
continue;
114+
}
115+
if (/\s/.test(ch)) {
116+
continue;
117+
}
118+
out += ch;
119+
}
120+
return out;
121+
}
122+
84123
/**
85124
* Whether a link-like attribute value uses a non-http(s) scheme prefix that authoring dialogs do not treat as repository paths.
125+
* Leading C0 control characters, DEL, and whitespace are stripped before the check so values cannot hide schemes from prefix matching.
86126
*
87-
* @param {*} value - attribute value
88-
* @returns {Boolean}
127+
* @param {*} value - attribute value (typically after DOM parsing, so entities are decoded)
128+
* @returns {Boolean} true when the normalised value starts with javascript, data, or vbscript
89129
*/
90130
function linkValueHasExcludedRepositoryPrefix(value) {
91131
if (value === undefined || value === null) {
92132
return false;
93133
}
94-
var t = String(value).trim().toLowerCase();
134+
var t = stripAsciiControlsAndWhitespaceForSchemeCheck(String(value)).toLowerCase();
95135
return (
96136
t.indexOf("javascript:") === 0 ||
97137
t.indexOf("data:") === 0 ||
98138
t.indexOf("vbscript:") === 0
99139
);
100140
}
101141

142+
/**
143+
* Normalizes parsed authoring markup under a root element: drops disallowed subtrees (including
144+
* active content, document-influencing, and styling hooks) and clears event-handler and disallowed URL schemes on link-like attributes.
145+
*
146+
* @param {Element} rootElement - parsed subtree root (typically {@code document.body})
147+
*/
148+
function sanitizeAuthoringMarkupSubtree(rootElement) {
149+
if (!rootElement || rootElement.nodeType !== 1) {
150+
return;
151+
}
152+
var all = rootElement.querySelectorAll("*");
153+
var list = [];
154+
var i;
155+
for (i = 0; i < all.length; i++) {
156+
list.push(all[i]);
157+
}
158+
list.push(rootElement);
159+
var removeEls = [];
160+
for (i = 0; i < list.length; i++) {
161+
var el = list[i];
162+
if (el.nodeType !== 1) {
163+
continue;
164+
}
165+
var tag = el.tagName;
166+
if (AUTHORING_MARKUP_STRIPPED_ELEMENT_TAGS[tag]) {
167+
removeEls.push(el);
168+
continue;
169+
}
170+
var attrs = el.attributes;
171+
var names = [];
172+
var j;
173+
for (j = 0; attrs && j < attrs.length; j++) {
174+
names.push(attrs[j].name);
175+
}
176+
for (j = 0; j < names.length; j++) {
177+
var name = names[j];
178+
var val = el.getAttribute(name);
179+
var nl = name.toLowerCase();
180+
if (/^on/i.test(name)) {
181+
el.removeAttribute(name);
182+
continue;
183+
}
184+
if (
185+
(
186+
nl === "href" ||
187+
nl === "src" ||
188+
nl === "action" ||
189+
nl === "formaction" ||
190+
nl === "xlink:href"
191+
) &&
192+
linkValueHasExcludedRepositoryPrefix(val)
193+
) {
194+
el.removeAttribute(name);
195+
}
196+
}
197+
}
198+
for (i = 0; i < removeEls.length; i++) {
199+
var node = removeEls[i];
200+
if (node.parentNode) {
201+
node.parentNode.removeChild(node);
202+
}
203+
}
204+
}
205+
206+
/**
207+
* Parses datasource HTML and returns the inner markup of the first body child, after subtree
208+
* normalization is applied to the parsed document body.
209+
*
210+
* @param {String} markup - HTML document string from a datasource response
211+
* @returns {String} normalized inner markup (empty string when the parsed body has no element child)
212+
*/
213+
function sanitizeAuthoringEditorResponseMarkup(markup) {
214+
var doc = parseMarkupDocument(String(markup == null ? "" : markup));
215+
if (doc.body) {
216+
sanitizeAuthoringMarkupSubtree(doc.body);
217+
}
218+
var body = doc.body;
219+
if (!body || !body.firstElementChild) {
220+
return "";
221+
}
222+
return body.firstElementChild.innerHTML;
223+
}
224+
102225
function filterClassAttribute(raw, allowedTokens) {
103226
if (!raw || typeof raw !== "string") {
104227
return "";
@@ -236,7 +359,8 @@
236359
innerHtmlFromFirstBodyChild: innerHtmlFromFirstBodyChild,
237360
adoptNodeForDocument: adoptNodeForDocument,
238361
linkValueHasExcludedRepositoryPrefix: linkValueHasExcludedRepositoryPrefix,
239-
buildPageImageThumbnailShellForEditor: buildPageImageThumbnailShellForEditor
362+
buildPageImageThumbnailShellForEditor: buildPageImageThumbnailShellForEditor,
363+
sanitizeAuthoringEditorResponseMarkup: sanitizeAuthoringEditorResponseMarkup
240364
};
241365

242366
})(window);
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
33
jcr:primaryType="cq:ClientLibraryFolder"
4-
categories="[core.wcm.components.contentfragment.v1.dialog]"/>
4+
categories="[core.wcm.components.contentfragment.v1.dialog]"
5+
dependencies="[core.wcm.components.commons.editor.authoringutils]"/>

content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,71 @@
4949
// ui helper
5050
var ui = $(window).adaptTo("foundation-ui");
5151

52+
var CT_SITES_41323 = "CT_SITES-41323";
53+
54+
/**
55+
* Granite feature toggle CT_SITES-41323 gates datasource HTML handling for the element names container.
56+
* When Granite or {@code Toggles} is unavailable, helpers are treated as enabled.
57+
* When the toggle is explicitly disabled, behaviour matches earlier dialog revisions.
58+
*
59+
* @returns {Boolean}
60+
*/
61+
function isContentFragmentV1DialogAuthoringMarkupHelpersEnabled() {
62+
if (typeof Granite === "undefined" || !Granite.Toggles || typeof Granite.Toggles.isEnabled !== "function") {
63+
return true;
64+
}
65+
return Granite.Toggles.isEnabled(CT_SITES_41323) !== false;
66+
}
67+
68+
/**
69+
* @returns {Object|null} {@code CQ.CoreComponents.AuthoringEditorUtils.markup} when present
70+
*/
71+
function getAuthoringMarkupUtils() {
72+
if (window.CQ && window.CQ.CoreComponents && window.CQ.CoreComponents.AuthoringEditorUtils) {
73+
return window.CQ.CoreComponents.AuthoringEditorUtils.markup;
74+
}
75+
return null;
76+
}
77+
78+
function extractFirstParsedInnerHtml(html) {
79+
if (typeof $ === "function" && typeof $.parseHTML === "function") {
80+
var nodes = $.parseHTML(String(html == null ? "" : html), document, true);
81+
var i;
82+
for (i = 0; i < nodes.length; i++) {
83+
if (nodes[i].nodeType === 1) {
84+
return nodes[i].innerHTML;
85+
}
86+
}
87+
return "";
88+
}
89+
var fallbackMarkup = getAuthoringMarkupUtils();
90+
if (fallbackMarkup && typeof fallbackMarkup.innerHtmlFromFirstBodyChild === "function") {
91+
return fallbackMarkup.innerHtmlFromFirstBodyChild(html);
92+
}
93+
var first = $(html)[0];
94+
if (!first || typeof first.innerHTML !== "string") {
95+
return "";
96+
}
97+
return first.innerHTML;
98+
}
99+
100+
/**
101+
* Resolves inner HTML for the element names container from a datasource response string.
102+
*
103+
* @param {String} html - outer HTML for the element names container
104+
* @returns {String}
105+
*/
106+
function resolveElementNamesContainerInnerHtml(html) {
107+
if (!isContentFragmentV1DialogAuthoringMarkupHelpersEnabled()) {
108+
return extractFirstParsedInnerHtml(html);
109+
}
110+
var markupUtils = getAuthoringMarkupUtils();
111+
if (markupUtils && typeof markupUtils.sanitizeAuthoringEditorResponseMarkup === "function") {
112+
return markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
113+
}
114+
return extractFirstParsedInnerHtml(html);
115+
}
116+
52117
// dialog texts
53118
var confirmationDialogTitle = Granite.I18n.get("Warning");
54119
var confirmationDialogMessage = Granite.I18n.get("Please confirm replacing the current content fragment and its configuration");
@@ -351,7 +416,7 @@
351416
* @param {String} html - outerHTML value for elementNamesContainer
352417
*/
353418
ElementsController.prototype._updateElementsHTML = function(html) {
354-
this.elementNamesContainer.innerHTML = $(html)[0].innerHTML;
419+
this.elementNamesContainer.innerHTML = resolveElementNamesContainerInnerHtml(html);
355420
this._updateFields();
356421
};
357422

@@ -868,4 +933,14 @@
868933
}
869934
});
870935

936+
var cfV1DialogTestApiHost = typeof globalThis !== "undefined" ? globalThis : window;
937+
/* Karma (mocks.js) sets __CONTENTFRAGMENT_V1_DIALOG_TEST_API on the global object; AEM runtime leaves it undefined. */
938+
if (cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API) {
939+
cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API.resolveElementNamesContainerInnerHtml =
940+
resolveElementNamesContainerInnerHtml;
941+
cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled =
942+
isContentFragmentV1DialogAuthoringMarkupHelpersEnabled;
943+
cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API.getAuthoringMarkupUtils = getAuthoringMarkupUtils;
944+
}
945+
871946
})(window, jQuery, jQuery(document), Granite, Coral);

content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ describe("AuthoringEditorUtils.markup (core.wcm.components.commons.editor.author
4141
expect(markupUtils.linkValueHasExcludedRepositoryPrefix("DATA:image/png;base64,xx")).toBe(true);
4242
expect(markupUtils.linkValueHasExcludedRepositoryPrefix(" javascript:x ")).toBe(true);
4343
});
44+
45+
it("returns true when C0 controls or whitespace break up or precede the scheme", function() {
46+
expect(markupUtils.linkValueHasExcludedRepositoryPrefix(" javascript:alert(1)")).toBe(true);
47+
expect(markupUtils.linkValueHasExcludedRepositoryPrefix("\n\tjavascript:alert(1)")).toBe(true);
48+
expect(markupUtils.linkValueHasExcludedRepositoryPrefix("java\tscript:alert(1)")).toBe(true);
49+
expect(markupUtils.linkValueHasExcludedRepositoryPrefix("java\nscript:alert(1)")).toBe(true);
50+
expect(markupUtils.linkValueHasExcludedRepositoryPrefix("jav\rascript:alert(1)")).toBe(true);
51+
expect(markupUtils.linkValueHasExcludedRepositoryPrefix("\u0000javascript:alert(1)")).toBe(true);
52+
expect(markupUtils.linkValueHasExcludedRepositoryPrefix("\u0001javascript:alert(1)")).toBe(true);
53+
});
54+
55+
it("returns false for https URLs and nullish values", function() {
56+
expect(markupUtils.linkValueHasExcludedRepositoryPrefix("https://example.com/x")).toBe(false);
57+
expect(markupUtils.linkValueHasExcludedRepositoryPrefix(null)).toBe(false);
58+
expect(markupUtils.linkValueHasExcludedRepositoryPrefix(undefined)).toBe(false);
59+
});
4460
});
4561

4662
describe("buildPageImageThumbnailShellForEditor", function() {
@@ -154,4 +170,53 @@ describe("AuthoringEditorUtils.markup (core.wcm.components.commons.editor.author
154170
expect(img.getAttribute("src")).toBe("/content/dam/x.png");
155171
});
156172
});
173+
174+
describe("sanitizeAuthoringEditorResponseMarkup", function() {
175+
it("returns inner markup of the first body child with event attributes removed", function() {
176+
const html =
177+
"<html><body><div><span onmouseover=\"x\"><img src=\"x\" onerror=\"alert(1)\"></span></div></body></html>";
178+
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
179+
expect(out.indexOf("onerror")).toBe(-1);
180+
expect(out.indexOf("onmouseover")).toBe(-1);
181+
expect(out.indexOf("<img")).not.toBe(-1);
182+
});
183+
184+
it("drops script tags from nested markup", function() {
185+
const html = "<div><script>z</script><p>ok</p></div>";
186+
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
187+
expect(out.indexOf("<script>")).toBe(-1);
188+
expect(out.indexOf(">ok</p>")).not.toBe(-1);
189+
});
190+
191+
it("removes href with disallowed schemes", function() {
192+
const html = "<div><a href=\"javascript:void(0)\">t</a></div>";
193+
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
194+
expect(out.indexOf("javascript")).toBe(-1);
195+
});
196+
197+
it("removes action with disallowed schemes", function() {
198+
const html = "<div><div action=\"javascript:void(0)\">t</div></div>";
199+
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
200+
expect(out.indexOf("javascript")).toBe(-1);
201+
});
202+
203+
it("drops style, link, meta, base, and form elements", function() {
204+
const html =
205+
"<div><style>x</style><link rel=\"stylesheet\" href=\"/etc.clientlibs/a.css\">" +
206+
"<meta http-equiv=\"refresh\" content=\"0\">" +
207+
"<base href=\"http://example.com/\">" +
208+
"<form action=\"/search\"></form><p>ok</p></div>";
209+
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
210+
expect(out.indexOf("<style")).toBe(-1);
211+
expect(out.indexOf("<link")).toBe(-1);
212+
expect(out.indexOf("<meta")).toBe(-1);
213+
expect(out.indexOf("<base")).toBe(-1);
214+
expect(out.indexOf("<form")).toBe(-1);
215+
expect(out.indexOf(">ok</p>")).not.toBe(-1);
216+
});
217+
218+
it("returns empty string when body has no element child", function() {
219+
expect(markupUtils.sanitizeAuthoringEditorResponseMarkup("")).toBe("");
220+
});
221+
});
157222
});

0 commit comments

Comments
 (0)