html-tagged-template.js: Accept moz-extension protocol

html-tagged-template.js forbids every protocol except http:// when
sanitizing urls. This prevents Tridactyl from loading its icons from
moz-extension urls. This commit fixes that (ignore prettier reformatting
the file, the only line I actually changed was line 314 before prettier
which became line 467 after prettier).
This commit is contained in:
glacambre 2019-01-17 08:09:10 +01:00
parent e62eae4d15
commit d1ce62ec6d
No known key found for this signature in database
GPG key ID: B9625DB1767553AC

View file

@ -1,408 +1,568 @@
(function(window) {
"use strict";
;(function(window) {
"use strict"
// test for es6 support of needed functionality
try {
// spread operator and template strings support
(function testSpreadOpAndTemplate() {
const tag = function tag(strings, ...values) {return;};
tag`test`;
})();
// test for es6 support of needed functionality
try {
// spread operator and template strings support
;(function testSpreadOpAndTemplate() {
const tag = function tag(strings, ...values) {
return
}
tag`test`
})()
// template tag and Array.from support
if (!('content' in document.createElement('template') && 'from' in Array)) {
throw new Error();
}
}
catch (e) {
// missing support;
console.log('Your browser does not support the needed functionality to use the html tagged template');
return;
}
if (typeof window.html === 'undefined') {
// --------------------------------------------------
// constants
// --------------------------------------------------
const SUBSTITUTION_INDEX = 'substitutionindex:'; // tag names are always all lowercase
const SUBSTITUTION_REGEX = new RegExp(SUBSTITUTION_INDEX + '([0-9]+):', 'g');
// rejection string is used to replace xss attacks that cannot be escaped either
// because the escaped string is still executable
// (e.g. setTimeout(/* escaped string */)) or because it produces invalid results
// (e.g. <h${xss}> where xss='><script>alert(1337)</script')
// @see https://developers.google.com/closure/templates/docs/security#in_tags_and_attrs
const REJECTION_STRING = 'zXssPreventedz';
// which characters should be encoded in which contexts
const ENCODINGS = {
attribute: {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
},
uri: {
'&': '&amp;'
}
};
// which attributes are DOM Level 0 events
// taken from https://en.wikipedia.org/wiki/DOM_events#DOM_Level_0
const DOM_EVENTS = ["onclick", "ondblclick", "onmousedown", "onmouseup", "onmouseover", "onmousemove", "onmouseout", "ondragstart", "ondrag", "ondragenter", "ondragleave", "ondragover", "ondrop", "ondragend", "onkeydown", "onkeypress", "onkeyup", "onload", "onunload", "onabort", "onerror", "onresize", "onscroll", "onselect", "onchange", "onsubmit", "onreset", "onfocus", "onblur", "onpointerdown", "onpointerup", "onpointercancel", "onpointermove", "onpointerover", "onpointerout", "onpointerenter", "onpointerleave", "ongotpointercapture", "onlostpointercapture", "oncut", "oncopy", "onpaste", "onbeforecut", "onbeforecopy", "onbeforepaste", "onafterupdate", "onbeforeupdate", "oncellchange", "ondataavailable", "ondatasetchanged", "ondatasetcomplete", "onerrorupdate", "onrowenter", "onrowexit", "onrowsdelete", "onrowinserted", "oncontextmenu", "ondrag", "ondragstart", "ondragenter", "ondragover", "ondragleave", "ondragend", "ondrop", "onselectstart", "help", "onbeforeunload", "onstop", "beforeeditfocus", "onstart", "onfinish", "onbounce", "onbeforeprint", "onafterprint", "onpropertychange", "onfilterchange", "onreadystatechange", "onlosecapture", "DOMMouseScroll", "ondragdrop", "ondragenter", "ondragexit", "ondraggesture", "ondragover", "onclose", "oncommand", "oninput", "DOMMenuItemActive", "DOMMenuItemInactive", "oncontextmenu", "onoverflow", "onoverflowchanged", "onunderflow", "onpopuphidden", "onpopuphiding", "onpopupshowing", "onpopupshown", "onbroadcast", "oncommandupdate"];
// which attributes take URIs
// taken from https://www.w3.org/TR/html4/index/attributes.html
const URI_ATTRIBUTES = ["action", "background", "cite", "classid", "codebase", "data", "href", "longdesc", "profile", "src", "usemap"];
const ENCODINGS_REGEX = {
attribute: new RegExp('[' + Object.keys(ENCODINGS.attribute).join('') + ']', 'g'),
uri: new RegExp('[' + Object.keys(ENCODINGS.uri).join('') + ']', 'g')
};
// find all attributes after the first whitespace (which would follow the tag
// name. Only used when the DOM has been clobbered to still parse attributes
const ATTRIBUTE_PARSER_REGEX = /\s([^">=\s]+)(?:="[^"]+")?/g;
// test if a javascript substitution is wrapped with quotes
const WRAPPED_WITH_QUOTES_REGEX = /^('|")[\s\S]*\1$/;
// allow custom attribute names that start or end with url or ui to do uri escaping
// @see https://developers.google.com/closure/templates/docs/security#in_urls
const CUSTOM_URI_ATTRIBUTES_REGEX = /\bur[il]|ur[il]s?$/i;
// --------------------------------------------------
// private functions
// --------------------------------------------------
/**
* Escape HTML entities in an attribute.
* @private
*
* @param {string} str - String to escape.
*
* @returns {string}
*/
function encodeAttributeHTMLEntities(str) {
return str.replace(ENCODINGS_REGEX.attribute, function(match) {
return ENCODINGS.attribute[match];
});
}
/**
* Escape entities in a URI.
* @private
*
* @param {string} str - URI to escape.
*
* @returns {string}
*/
function encodeURIEntities(str) {
return str.replace(ENCODINGS_REGEX.uri, function(match) {
return ENCODINGS.uri[match];
});
}
// --------------------------------------------------
// html tagged template function
// --------------------------------------------------
/**
* Safely convert a DOM string into DOM nodes using by using E4H and contextual
* auto-escaping techniques to prevent xss attacks.
*
* @param {string[]} strings - Safe string literals.
* @param {*} values - Unsafe substitution expressions.
*
* @returns {HTMLElement|DocumentFragment}
*/
window.html = function(strings, ...values) {
// break early if called with empty content
if (!strings[0] && values.length === 0) {
return;
}
/**
* Replace a string with substitution placeholders with its substitution values.
* @private
*
* @param {string} match - Matched substitution placeholder.
* @param {string} index - Substitution placeholder index.
*/
function replaceSubstitution(match, index) {
return values[parseInt(index, 10)];
}
// insert placeholders into the generated string so we can run it through the
// HTML parser without any malicious content.
// (this particular placeholder will even work when used to create a DOM element)
let str = strings[0];
for (let i = 0; i < values.length; i++) {
str += SUBSTITUTION_INDEX + i + ':' + strings[i+1];
}
// template tags allow any HTML (even <tr> elements out of context)
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
let template = document.createElement('template');
template.innerHTML = str;
// find all substitution values and safely encode them using DOM APIs and
// contextual auto-escaping
let walker = document.createNodeIterator(template.content, NodeFilter.SHOW_ALL);
let node;
while (node = walker.nextNode()) {
let tag = null;
let attributesToRemove = [];
// --------------------------------------------------
// node name substitution
// --------------------------------------------------
let nodeName = node.nodeName.toLowerCase();
if (nodeName.indexOf(SUBSTITUTION_INDEX) !== -1) {
nodeName = nodeName.replace(SUBSTITUTION_REGEX, replaceSubstitution);
// createElement() should not need to be escaped to prevent XSS?
// this will throw an error if the tag name is invalid (e.g. xss tried
// to escape out of the tag using '><script>alert(1337)</script><')
// instead of replacing the tag name we'll just let the error be thrown
tag = document.createElement(nodeName);
// mark that this node needs to be cleaned up later with the newly
// created node
node._replacedWith = tag;
// use insertBefore() instead of replaceChild() so that the node Iterator
// doesn't think the new tag should be the next node
node.parentNode.insertBefore(tag, node);
}
// special case for script tags:
// using innerHTML with a string that contains a script tag causes the script
// tag to not be executed when added to the DOM. We'll need to create a script
// tag and append its contents which will make it execute correctly.
// @see http://stackoverflow.com/questions/1197575/can-scripts-be-inserted-with-innerhtml
else if (node.nodeName === 'SCRIPT') {
let script = document.createElement('script');
tag = script;
node._replacedWith = script;
node.parentNode.insertBefore(script, node);
}
// --------------------------------------------------
// attribute substitution
// --------------------------------------------------
let attributes;
if (node.attributes) {
// if the attributes property is not of type NamedNodeMap then the DOM
// has been clobbered. E.g. <form><input name="attributes"></form>.
// We'll manually build up an array of objects that mimic the Attr
// object so the loop will still work as expected.
if ( !(node.attributes instanceof NamedNodeMap) ) {
// first clone the node so we can isolate it from any children
let temp = node.cloneNode();
// parse the node string for all attributes
let attributeMatches = temp.outerHTML.match(ATTRIBUTE_PARSER_REGEX);
// get all attribute names and their value
attributes = [];
for (let i = 0; i < attributeMatches.length; i++) {
let attributeName = attributeMatches[i].trim().split('=')[0];
let attributeValue = node.getAttribute(attributeName);
attributes.push({
name: attributeName,
value: attributeValue
});
}
// template tag and Array.from support
if (
!(
"content" in document.createElement("template") &&
"from" in Array
)
) {
throw new Error()
}
else {
// Windows 10 Firefox 44 will shift the attributes NamedNodeMap and
// push the attribute to the end when using setAttribute(). We'll have
// to clone the NamedNodeMap so the order isn't changed for setAttribute()
attributes = Array.from(node.attributes);
} catch (e) {
// missing support;
console.log(
"Your browser does not support the needed functionality to use the html tagged template",
)
return
}
if (typeof window.html === "undefined") {
// --------------------------------------------------
// constants
// --------------------------------------------------
const SUBSTITUTION_INDEX = "substitutionindex:" // tag names are always all lowercase
const SUBSTITUTION_REGEX = new RegExp(
SUBSTITUTION_INDEX + "([0-9]+):",
"g",
)
// rejection string is used to replace xss attacks that cannot be escaped either
// because the escaped string is still executable
// (e.g. setTimeout(/* escaped string */)) or because it produces invalid results
// (e.g. <h${xss}> where xss='><script>alert(1337)</script')
// @see https://developers.google.com/closure/templates/docs/security#in_tags_and_attrs
const REJECTION_STRING = "zXssPreventedz"
// which characters should be encoded in which contexts
const ENCODINGS = {
attribute: {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
},
uri: {
"&": "&amp;",
},
}
for (let i = 0; i < attributes.length; i++) {
let attribute = attributes[i];
let name = attribute.name;
let value = attribute.value;
let hasSubstitution = false;
// which attributes are DOM Level 0 events
// taken from https://en.wikipedia.org/wiki/DOM_events#DOM_Level_0
const DOM_EVENTS = [
"onclick",
"ondblclick",
"onmousedown",
"onmouseup",
"onmouseover",
"onmousemove",
"onmouseout",
"ondragstart",
"ondrag",
"ondragenter",
"ondragleave",
"ondragover",
"ondrop",
"ondragend",
"onkeydown",
"onkeypress",
"onkeyup",
"onload",
"onunload",
"onabort",
"onerror",
"onresize",
"onscroll",
"onselect",
"onchange",
"onsubmit",
"onreset",
"onfocus",
"onblur",
"onpointerdown",
"onpointerup",
"onpointercancel",
"onpointermove",
"onpointerover",
"onpointerout",
"onpointerenter",
"onpointerleave",
"ongotpointercapture",
"onlostpointercapture",
"oncut",
"oncopy",
"onpaste",
"onbeforecut",
"onbeforecopy",
"onbeforepaste",
"onafterupdate",
"onbeforeupdate",
"oncellchange",
"ondataavailable",
"ondatasetchanged",
"ondatasetcomplete",
"onerrorupdate",
"onrowenter",
"onrowexit",
"onrowsdelete",
"onrowinserted",
"oncontextmenu",
"ondrag",
"ondragstart",
"ondragenter",
"ondragover",
"ondragleave",
"ondragend",
"ondrop",
"onselectstart",
"help",
"onbeforeunload",
"onstop",
"beforeeditfocus",
"onstart",
"onfinish",
"onbounce",
"onbeforeprint",
"onafterprint",
"onpropertychange",
"onfilterchange",
"onreadystatechange",
"onlosecapture",
"DOMMouseScroll",
"ondragdrop",
"ondragenter",
"ondragexit",
"ondraggesture",
"ondragover",
"onclose",
"oncommand",
"oninput",
"DOMMenuItemActive",
"DOMMenuItemInactive",
"oncontextmenu",
"onoverflow",
"onoverflowchanged",
"onunderflow",
"onpopuphidden",
"onpopuphiding",
"onpopupshowing",
"onpopupshown",
"onbroadcast",
"oncommandupdate",
]
// name has substitution
if (name.indexOf(SUBSTITUTION_INDEX) !== -1) {
name = name.replace(SUBSTITUTION_REGEX, replaceSubstitution);
// which attributes take URIs
// taken from https://www.w3.org/TR/html4/index/attributes.html
const URI_ATTRIBUTES = [
"action",
"background",
"cite",
"classid",
"codebase",
"data",
"href",
"longdesc",
"profile",
"src",
"usemap",
]
// ensure substitution was with a non-empty string
if (name && typeof name === 'string') {
hasSubstitution = true;
const ENCODINGS_REGEX = {
attribute: new RegExp(
"[" + Object.keys(ENCODINGS.attribute).join("") + "]",
"g",
),
uri: new RegExp(
"[" + Object.keys(ENCODINGS.uri).join("") + "]",
"g",
),
}
// find all attributes after the first whitespace (which would follow the tag
// name. Only used when the DOM has been clobbered to still parse attributes
const ATTRIBUTE_PARSER_REGEX = /\s([^">=\s]+)(?:="[^"]+")?/g
// test if a javascript substitution is wrapped with quotes
const WRAPPED_WITH_QUOTES_REGEX = /^('|")[\s\S]*\1$/
// allow custom attribute names that start or end with url or ui to do uri escaping
// @see https://developers.google.com/closure/templates/docs/security#in_urls
const CUSTOM_URI_ATTRIBUTES_REGEX = /\bur[il]|ur[il]s?$/i
// --------------------------------------------------
// private functions
// --------------------------------------------------
/**
* Escape HTML entities in an attribute.
* @private
*
* @param {string} str - String to escape.
*
* @returns {string}
*/
function encodeAttributeHTMLEntities(str) {
return str.replace(ENCODINGS_REGEX.attribute, function(match) {
return ENCODINGS.attribute[match]
})
}
/**
* Escape entities in a URI.
* @private
*
* @param {string} str - URI to escape.
*
* @returns {string}
*/
function encodeURIEntities(str) {
return str.replace(ENCODINGS_REGEX.uri, function(match) {
return ENCODINGS.uri[match]
})
}
// --------------------------------------------------
// html tagged template function
// --------------------------------------------------
/**
* Safely convert a DOM string into DOM nodes using by using E4H and contextual
* auto-escaping techniques to prevent xss attacks.
*
* @param {string[]} strings - Safe string literals.
* @param {*} values - Unsafe substitution expressions.
*
* @returns {HTMLElement|DocumentFragment}
*/
window.html = function(strings, ...values) {
// break early if called with empty content
if (!strings[0] && values.length === 0) {
return
}
// remove old attribute
attributesToRemove.push(attribute.name);
}
/**
* Replace a string with substitution placeholders with its substitution values.
* @private
*
* @param {string} match - Matched substitution placeholder.
* @param {string} index - Substitution placeholder index.
*/
function replaceSubstitution(match, index) {
return values[parseInt(index, 10)]
}
// value has substitution - only check if name exists (only happens
// when name is a substitution with an empty value)
if (name && value.indexOf(SUBSTITUTION_INDEX) !== -1) {
hasSubstitution = true;
// insert placeholders into the generated string so we can run it through the
// HTML parser without any malicious content.
// (this particular placeholder will even work when used to create a DOM element)
let str = strings[0]
for (let i = 0; i < values.length; i++) {
str += SUBSTITUTION_INDEX + i + ":" + strings[i + 1]
}
// if an uri attribute has been rejected
let isRejected = false;
// template tags allow any HTML (even <tr> elements out of context)
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
let template = document.createElement("template")
template.innerHTML = str
value = value.replace(SUBSTITUTION_REGEX, function(match, index, offset) {
if (isRejected) {
return '';
}
// find all substitution values and safely encode them using DOM APIs and
// contextual auto-escaping
let walker = document.createNodeIterator(
template.content,
NodeFilter.SHOW_ALL,
)
let node
while ((node = walker.nextNode())) {
let tag = null
let attributesToRemove = []
let substitutionValue = values[parseInt(index, 10)];
// --------------------------------------------------
// node name substitution
// --------------------------------------------------
// contextual auto-escaping:
// if attribute is a DOM Level 0 event then we need to ensure it
// is quoted
if (DOM_EVENTS.indexOf(name) !== -1 &&
typeof substitutionValue === 'string' &&
!WRAPPED_WITH_QUOTES_REGEX.test(substitutionValue) ) {
substitutionValue = '"' + substitutionValue + '"';
}
let nodeName = node.nodeName.toLowerCase()
if (nodeName.indexOf(SUBSTITUTION_INDEX) !== -1) {
nodeName = nodeName.replace(
SUBSTITUTION_REGEX,
replaceSubstitution,
)
// contextual auto-escaping:
// if the attribute is a uri attribute then we need to uri encode it and
// remove bad protocols
else if (URI_ATTRIBUTES.indexOf(name) !== -1 ||
CUSTOM_URI_ATTRIBUTES_REGEX.test(name)) {
// createElement() should not need to be escaped to prevent XSS?
// percent encode if the value is inside of a query parameter
let queryParamIndex = value.indexOf('=');
if (queryParamIndex !== -1 && offset > queryParamIndex) {
substitutionValue = encodeURIComponent(substitutionValue);
// this will throw an error if the tag name is invalid (e.g. xss tried
// to escape out of the tag using '><script>alert(1337)</script><')
// instead of replacing the tag name we'll just let the error be thrown
tag = document.createElement(nodeName)
// mark that this node needs to be cleaned up later with the newly
// created node
node._replacedWith = tag
// use insertBefore() instead of replaceChild() so that the node Iterator
// doesn't think the new tag should be the next node
node.parentNode.insertBefore(tag, node)
}
// entity encode if value is part of the URL
else {
substitutionValue = encodeURI( encodeURIEntities(substitutionValue) );
// special case for script tags:
// using innerHTML with a string that contains a script tag causes the script
// tag to not be executed when added to the DOM. We'll need to create a script
// tag and append its contents which will make it execute correctly.
// @see http://stackoverflow.com/questions/1197575/can-scripts-be-inserted-with-innerhtml
else if (node.nodeName === "SCRIPT") {
let script = document.createElement("script")
tag = script
// only allow the : when used after http or https otherwise reject
// the entire url (will not allow any 'javascript:' or filter
// evasion techniques)
if (offset === 0 && substitutionValue.indexOf(':') !== -1) {
let protocol = substitutionValue.substring(0, 5);
if (protocol.indexOf('http') === -1) {
isRejected = true;
node._replacedWith = script
node.parentNode.insertBefore(script, node)
}
// --------------------------------------------------
// attribute substitution
// --------------------------------------------------
let attributes
if (node.attributes) {
// if the attributes property is not of type NamedNodeMap then the DOM
// has been clobbered. E.g. <form><input name="attributes"></form>.
// We'll manually build up an array of objects that mimic the Attr
// object so the loop will still work as expected.
if (!(node.attributes instanceof NamedNodeMap)) {
// first clone the node so we can isolate it from any children
let temp = node.cloneNode()
// parse the node string for all attributes
let attributeMatches = temp.outerHTML.match(
ATTRIBUTE_PARSER_REGEX,
)
// get all attribute names and their value
attributes = []
for (let i = 0; i < attributeMatches.length; i++) {
let attributeName = attributeMatches[i]
.trim()
.split("=")[0]
let attributeValue = node.getAttribute(
attributeName,
)
attributes.push({
name: attributeName,
value: attributeValue,
})
}
} else {
// Windows 10 Firefox 44 will shift the attributes NamedNodeMap and
// push the attribute to the end when using setAttribute(). We'll have
// to clone the NamedNodeMap so the order isn't changed for setAttribute()
attributes = Array.from(node.attributes)
}
for (let i = 0; i < attributes.length; i++) {
let attribute = attributes[i]
let name = attribute.name
let value = attribute.value
let hasSubstitution = false
// name has substitution
if (name.indexOf(SUBSTITUTION_INDEX) !== -1) {
name = name.replace(
SUBSTITUTION_REGEX,
replaceSubstitution,
)
// ensure substitution was with a non-empty string
if (name && typeof name === "string") {
hasSubstitution = true
}
// remove old attribute
attributesToRemove.push(attribute.name)
}
// value has substitution - only check if name exists (only happens
// when name is a substitution with an empty value)
if (name && value.indexOf(SUBSTITUTION_INDEX) !== -1) {
hasSubstitution = true
// if an uri attribute has been rejected
let isRejected = false
value = value.replace(SUBSTITUTION_REGEX, function(
match,
index,
offset,
) {
if (isRejected) {
return ""
}
let substitutionValue =
values[parseInt(index, 10)]
// contextual auto-escaping:
// if attribute is a DOM Level 0 event then we need to ensure it
// is quoted
if (
DOM_EVENTS.indexOf(name) !== -1 &&
typeof substitutionValue === "string" &&
!WRAPPED_WITH_QUOTES_REGEX.test(
substitutionValue,
)
) {
substitutionValue =
'"' + substitutionValue + '"'
}
// contextual auto-escaping:
// if the attribute is a uri attribute then we need to uri encode it and
// remove bad protocols
else if (
URI_ATTRIBUTES.indexOf(name) !== -1 ||
CUSTOM_URI_ATTRIBUTES_REGEX.test(name)
) {
// percent encode if the value is inside of a query parameter
let queryParamIndex = value.indexOf("=")
if (
queryParamIndex !== -1 &&
offset > queryParamIndex
) {
substitutionValue = encodeURIComponent(
substitutionValue,
)
}
// entity encode if value is part of the URL
else {
substitutionValue = encodeURI(
encodeURIEntities(
substitutionValue,
),
)
// only allow the : when used after http or https otherwise reject
// the entire url (will not allow any 'javascript:' or filter
// evasion techniques)
if (
offset === 0 &&
substitutionValue.indexOf(":") !==
-1
) {
let protocol = substitutionValue.substring(
0,
5,
)
if (
protocol.indexOf("http") ===
-1 &&
protocol.indexOf("moz-") == -1
) {
isRejected = true
}
}
}
}
// contextual auto-escaping:
// HTML encode attribute value if it is not a URL or URI to prevent
// DOM Level 0 event handlers from executing xss code
else if (
typeof substitutionValue === "string"
) {
substitutionValue = encodeAttributeHTMLEntities(
substitutionValue,
)
}
return substitutionValue
})
if (isRejected) {
value = "#" + REJECTION_STRING
}
}
// add the attribute to the new tag or replace it on the current node
// setAttribute() does not need to be escaped to prevent XSS since it does
// all of that for us
// @see https://www.mediawiki.org/wiki/DOM-based_XSS
if (tag || hasSubstitution) {
let el = tag || node
// optional attribute
if (name.substr(-1) === "?") {
el.removeAttribute(name)
if (value === "true") {
name = name.slice(0, -1)
el.setAttribute(name, "")
}
} else {
el.setAttribute(name, value)
}
}
}
}
}
}
// contextual auto-escaping:
// HTML encode attribute value if it is not a URL or URI to prevent
// DOM Level 0 event handlers from executing xss code
else if (typeof substitutionValue === 'string') {
substitutionValue = encodeAttributeHTMLEntities(substitutionValue);
}
// remove placeholder attributes outside of the attribute loop since it
// will modify the attributes NamedNodeMap indices.
// @see https://github.com/straker/html-tagged-template/issues/13
attributesToRemove.forEach(function(attribute) {
node.removeAttribute(attribute)
})
return substitutionValue;
});
// append the current node to a replaced parent
let parentNode
if (node.parentNode && node.parentNode._replacedWith) {
parentNode = node.parentNode
node.parentNode._replacedWith.appendChild(node)
}
if (isRejected) {
value = '#' + REJECTION_STRING;
// remove the old node from the DOM
if (
(node._replacedWith && node.childNodes.length === 0) ||
(parentNode && parentNode.childNodes.length === 0)
) {
;(parentNode || node).remove()
}
// --------------------------------------------------
// text content substitution
// --------------------------------------------------
if (
node.nodeType === 3 &&
node.nodeValue.indexOf(SUBSTITUTION_INDEX) !== -1
) {
let nodeValue = node.nodeValue.replace(
SUBSTITUTION_REGEX,
replaceSubstitution,
)
// createTextNode() should not need to be escaped to prevent XSS?
let text = document.createTextNode(nodeValue)
// since the parent node has already gone through the iterator, we can use
// replaceChild() here
node.parentNode.replaceChild(text, node)
}
}
}
// add the attribute to the new tag or replace it on the current node
// setAttribute() does not need to be escaped to prevent XSS since it does
// all of that for us
// @see https://www.mediawiki.org/wiki/DOM-based_XSS
if (tag || hasSubstitution) {
let el = (tag || node);
// optional attribute
if (name.substr(-1) === '?') {
el.removeAttribute(name);
if (value === 'true') {
name = name.slice(0, -1);
el.setAttribute(name, '');
}
// return the documentFragment for multiple nodes
if (template.content.childNodes.length > 1) {
return template.content
}
else {
el.setAttribute(name, value);
}
}
return template.content.firstChild
}
}
// remove placeholder attributes outside of the attribute loop since it
// will modify the attributes NamedNodeMap indices.
// @see https://github.com/straker/html-tagged-template/issues/13
attributesToRemove.forEach(function(attribute) {
node.removeAttribute(attribute);
});
// append the current node to a replaced parent
let parentNode;
if (node.parentNode && node.parentNode._replacedWith) {
parentNode = node.parentNode;
node.parentNode._replacedWith.appendChild(node);
}
// remove the old node from the DOM
if ((node._replacedWith && node.childNodes.length === 0) ||
(parentNode && parentNode.childNodes.length === 0) ){
(parentNode || node).remove();
}
// --------------------------------------------------
// text content substitution
// --------------------------------------------------
if (node.nodeType === 3 && node.nodeValue.indexOf(SUBSTITUTION_INDEX) !== -1) {
let nodeValue = node.nodeValue.replace(SUBSTITUTION_REGEX, replaceSubstitution);
// createTextNode() should not need to be escaped to prevent XSS?
let text = document.createTextNode(nodeValue);
// since the parent node has already gone through the iterator, we can use
// replaceChild() here
node.parentNode.replaceChild(text, node);
}
}
// return the documentFragment for multiple nodes
if (template.content.childNodes.length > 1) {
return template.content;
}
return template.content.firstChild;
};
}
})(window);
})(window)