(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`; })(); // 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. where xss='><') // 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.
. // 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) { 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); } } } } // 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);