Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions content/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
return new window.DOMParser().parseFromString(markup, "text/html");
}

/**

Check warning on line 52 in content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js

View workflow job for this annotation

GitHub Actions / build-js (14)

Missing JSDoc return description
* Inner HTML of the first element child of the parsed document body (mirrors jQuery(html)[0].innerHTML for one root).
*
* @param {String} markup - HTML document string
Expand All @@ -64,7 +64,7 @@
return body.firstElementChild.innerHTML;
}

/**

Check warning on line 67 in content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js

View workflow job for this annotation

GitHub Actions / build-js (14)

Missing JSDoc return description
* Ensures a node can be inserted into targetDocument (uses importNode when the node comes from another document).
*
* @param {Node} node - element or fragment from parsing or another document
Expand All @@ -81,7 +81,7 @@
return node;
}

/**

Check warning on line 84 in content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js

View workflow job for this annotation

GitHub Actions / build-js (14)

Missing JSDoc return description
* Whether a link-like attribute value uses a non-http(s) scheme prefix that authoring dialogs do not treat as repository paths.
*
* @param {*} value - attribute value
Expand All @@ -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") {
Comment thread
YahorC marked this conversation as resolved.
Outdated
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") &&
Comment thread
YahorC marked this conversation as resolved.
Outdated
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 "";
Expand Down Expand Up @@ -236,7 +313,8 @@
innerHtmlFromFirstBodyChild: innerHtmlFromFirstBodyChild,
adoptNodeForDocument: adoptNodeForDocument,
linkValueHasExcludedRepositoryPrefix: linkValueHasExcludedRepositoryPrefix,
buildPageImageThumbnailShellForEditor: buildPageImageThumbnailShellForEditor
buildPageImageThumbnailShellForEditor: buildPageImageThumbnailShellForEditor,
sanitizeAuthoringEditorResponseMarkup: sanitizeAuthoringEditorResponseMarkup
};

})(window);
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
categories="[core.wcm.components.contentfragment.v1.dialog]"/>
categories="[core.wcm.components.contentfragment.v1.dialog]"
dependencies="[core.wcm.components.commons.editor.authoringutils]"/>
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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();
};

Expand Down Expand Up @@ -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);
28 changes: 28 additions & 0 deletions content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
"<html><body><div><span onmouseover=\"x\"><img src=\"x\" onerror=\"alert(1)\"></span></div></body></html>";
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
expect(out.indexOf("onerror")).toBe(-1);
expect(out.indexOf("onmouseover")).toBe(-1);
expect(out.indexOf("<img")).not.toBe(-1);
});

it("drops script tags from nested markup", function() {
const html = "<div><script>z</script><p>ok</p></div>";
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
expect(out.indexOf("<script>")).toBe(-1);
expect(out.indexOf(">ok</p>")).not.toBe(-1);
});

it("removes href with disallowed schemes", function() {
const html = "<div><a href=\"javascript:void(0)\">t</a></div>";
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("");
});
});
});
80 changes: 80 additions & 0 deletions content/test/clientlibs/contentfragment/editDialogTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 =
"<div><div data-element-names-container=\"true\"><img src=\"x\" onerror=\"alert(1)\"></div></div>";
const inner = api.resolveElementNamesContainerInnerHtml(doc);
expect(inner.indexOf("onerror")).toBe(-1);
});

it("keeps legacy parsing when helpers are disabled", function() {
editDialogTest_graniteCt41323Off();
const doc =
"<div><div data-element-names-container=\"true\"><img src=\"x\" onerror=\"alert(1)\"></div></div>";
const inner = api.resolveElementNamesContainerInnerHtml(doc);
expect(inner.indexOf("onerror")).not.toBe(-1);
});
});
});
2 changes: 2 additions & 0 deletions content/test/mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Loading