2017-08-31 22:05:18 +01:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 12:08:49 -07:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2017-08-31 22:05:18 +01:00
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This helper class is used to get hold of an inert tree of DOM elements containing dirty HTML
|
|
|
|
* that needs sanitizing.
|
2020-04-11 16:29:47 +03:00
|
|
|
* Depending upon browser support we use one of two strategies for doing this.
|
|
|
|
* Default: DomParser strategy
|
|
|
|
* Fallback: InertDocument strategy
|
2017-08-31 22:05:18 +01:00
|
|
|
*/
|
|
|
|
export class InertBodyHelper {
|
2018-03-01 13:16:13 -08:00
|
|
|
private inertDocument: Document;
|
2017-08-31 22:05:18 +01:00
|
|
|
|
2018-03-01 13:16:13 -08:00
|
|
|
constructor(private defaultDoc: Document) {
|
|
|
|
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
|
2020-04-11 16:29:47 +03:00
|
|
|
if (this.inertDocument.body == null) {
|
2017-08-31 22:05:18 +01:00
|
|
|
// usually there should be only one body element in the document, but IE doesn't have any, so
|
|
|
|
// we need to create one.
|
2018-03-01 13:16:13 -08:00
|
|
|
const inertHtml = this.inertDocument.createElement('html');
|
|
|
|
this.inertDocument.appendChild(inertHtml);
|
2020-04-11 16:29:47 +03:00
|
|
|
const inertBodyElement = this.inertDocument.createElement('body');
|
2019-12-11 22:00:42 +01:00
|
|
|
inertHtml.appendChild(inertBodyElement);
|
2017-08-31 22:05:18 +01:00
|
|
|
}
|
|
|
|
|
2020-04-11 16:29:47 +03:00
|
|
|
this.getInertBodyElement = isDOMParserAvailable() ? this.getInertBodyElement_DOMParser :
|
|
|
|
this.getInertBodyElement_InertDocument;
|
2017-08-31 22:05:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an inert DOM element containing DOM created from the dirty HTML string provided.
|
|
|
|
* The implementation of this is determined in the constructor, when the class is instantiated.
|
|
|
|
*/
|
|
|
|
getInertBodyElement: (html: string) => HTMLElement | null;
|
|
|
|
|
|
|
|
/**
|
2020-04-11 16:29:47 +03:00
|
|
|
* Use DOMParser to create and fill an inert body element in browsers that support it.
|
2017-08-31 22:05:18 +01:00
|
|
|
*/
|
|
|
|
private getInertBodyElement_DOMParser(html: string) {
|
|
|
|
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
|
|
|
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
|
|
|
// `<head>` tag.
|
|
|
|
html = '<body><remove></remove>' + html + '</body>';
|
|
|
|
try {
|
2020-04-13 16:40:21 -07:00
|
|
|
const body = new (window as any).DOMParser().parseFromString(html, 'text/html').body as
|
|
|
|
HTMLBodyElement;
|
|
|
|
body.removeChild(body.firstChild!);
|
2017-08-31 22:05:18 +01:00
|
|
|
return body;
|
2018-08-14 15:34:51 +02:00
|
|
|
} catch {
|
2017-08-31 22:05:18 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Use an HTML5 `template` element, if supported, or an inert body element created via
|
|
|
|
* `createHtmlDocument` to create and fill an inert DOM element.
|
2020-04-11 16:29:47 +03:00
|
|
|
* This is the fallback strategy if the browser does not support DOMParser.
|
2017-08-31 22:05:18 +01:00
|
|
|
*/
|
|
|
|
private getInertBodyElement_InertDocument(html: string) {
|
|
|
|
// Prefer using <template> element if supported.
|
2018-03-01 13:16:13 -08:00
|
|
|
const templateEl = this.inertDocument.createElement('template');
|
2017-08-31 22:05:18 +01:00
|
|
|
if ('content' in templateEl) {
|
2018-03-01 13:16:13 -08:00
|
|
|
templateEl.innerHTML = html;
|
2017-08-31 22:05:18 +01:00
|
|
|
return templateEl;
|
|
|
|
}
|
|
|
|
|
2019-12-11 22:00:42 +01:00
|
|
|
// Note that previously we used to do something like `this.inertDocument.body.innerHTML = html`
|
|
|
|
// and we returned the inert `body` node. This was changed, because IE seems to treat setting
|
|
|
|
// `innerHTML` on an inserted element differently, compared to one that hasn't been inserted
|
|
|
|
// yet. In particular, IE appears to split some of the text into multiple text nodes rather
|
|
|
|
// than keeping them in a single one which ends up messing with Ivy's i18n parsing further
|
|
|
|
// down the line. This has been worked around by creating a new inert `body` and using it as
|
|
|
|
// the root node in which we insert the HTML.
|
|
|
|
const inertBody = this.inertDocument.createElement('body');
|
|
|
|
inertBody.innerHTML = html;
|
2017-08-31 22:05:18 +01:00
|
|
|
|
|
|
|
// Support: IE 9-11 only
|
|
|
|
// strip custom-namespaced attributes on IE<=11
|
2018-03-01 13:16:13 -08:00
|
|
|
if ((this.defaultDoc as any).documentMode) {
|
2019-12-11 22:00:42 +01:00
|
|
|
this.stripCustomNsAttrs(inertBody);
|
2017-08-31 22:05:18 +01:00
|
|
|
}
|
|
|
|
|
2019-12-11 22:00:42 +01:00
|
|
|
return inertBody;
|
2017-08-31 22:05:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1'
|
|
|
|
* attribute to declare ns1 namespace and prefixes the attribute with 'ns1' (e.g.
|
|
|
|
* 'ns1:xlink:foo').
|
|
|
|
*
|
|
|
|
* This is undesirable since we don't want to allow any of these custom attributes. This method
|
|
|
|
* strips them all.
|
|
|
|
*/
|
|
|
|
private stripCustomNsAttrs(el: Element) {
|
2018-03-01 13:16:13 -08:00
|
|
|
const elAttrs = el.attributes;
|
|
|
|
// loop backwards so that we can support removals.
|
|
|
|
for (let i = elAttrs.length - 1; 0 < i; i--) {
|
|
|
|
const attrib = elAttrs.item(i);
|
2020-04-13 16:40:21 -07:00
|
|
|
const attrName = attrib!.name;
|
2017-08-31 22:05:18 +01:00
|
|
|
if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
|
2018-03-01 13:16:13 -08:00
|
|
|
el.removeAttribute(attrName);
|
2017-08-31 22:05:18 +01:00
|
|
|
}
|
2018-03-01 13:16:13 -08:00
|
|
|
}
|
2018-09-27 16:47:19 -07:00
|
|
|
let childNode = el.firstChild as Node | null;
|
2018-03-01 13:16:13 -08:00
|
|
|
while (childNode) {
|
|
|
|
if (childNode.nodeType === Node.ELEMENT_NODE) this.stripCustomNsAttrs(childNode as Element);
|
|
|
|
childNode = childNode.nextSibling;
|
2017-08-31 22:05:18 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* We need to determine whether the DOMParser exists in the global context.
|
|
|
|
* The try-catch is because, on some browsers, trying to access this property
|
|
|
|
* on window can actually throw an error.
|
|
|
|
*
|
|
|
|
* @suppress {uselessCode}
|
|
|
|
*/
|
|
|
|
function isDOMParserAvailable() {
|
|
|
|
try {
|
|
|
|
return !!(window as any).DOMParser;
|
2018-08-14 15:34:51 +02:00
|
|
|
} catch {
|
2017-08-31 22:05:18 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|