You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					813 lines
				
				30 KiB
			
		
		
			
		
	
	
					813 lines
				
				30 KiB
			| 
											4 years ago
										 | ;(function(factory) { | ||
|  |     'use strict'; | ||
|  |     /* global window: false, define: false, module: false */ | ||
|  |     var root = typeof window === 'undefined' ? null : window; | ||
|  | 
 | ||
|  |     if (typeof define === 'function' && define.amd) { | ||
|  |         define(function(){ return factory(root); }); | ||
|  |     } else if (typeof module !== 'undefined') { | ||
|  |         module.exports = factory(root); | ||
|  |     } else { | ||
|  |         root.DOMPurify = factory(root); | ||
|  |     } | ||
|  | }(function factory(window) { | ||
|  |     'use strict'; | ||
|  | 
 | ||
|  |     var DOMPurify = function(window) { | ||
|  |         return factory(window); | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Version label, exposed for easier checks | ||
|  |      * if DOMPurify is up to date or not | ||
|  |      */ | ||
|  |     DOMPurify.version = '0.7.4'; | ||
|  | 
 | ||
|  |     if (!window || !window.document || window.document.nodeType !== 9) { | ||
|  |         // not running in a browser, provide a factory function
 | ||
|  |         // so that you can pass your own Window
 | ||
|  |         DOMPurify.isSupported = false; | ||
|  |         return DOMPurify; | ||
|  |     } | ||
|  | 
 | ||
|  |     var document = window.document; | ||
|  |     var originalDocument = document; | ||
|  |     var DocumentFragment = window.DocumentFragment; | ||
|  |     var HTMLTemplateElement = window.HTMLTemplateElement; | ||
|  |     var NodeFilter = window.NodeFilter; | ||
|  |     var NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap; | ||
|  |     var Text = window.Text; | ||
|  |     var Comment = window.Comment; | ||
|  |     var DOMParser = window.DOMParser; | ||
|  | 
 | ||
|  |     // As per issue #47, the web-components registry is inherited by a
 | ||
|  |     // new document created via createHTMLDocument. As per the spec
 | ||
|  |     // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
 | ||
|  |     // a new empty registry is used when creating a template contents owner
 | ||
|  |     // document, so we use that as our parent document to ensure nothing
 | ||
|  |     // is inherited.
 | ||
|  |     if (typeof HTMLTemplateElement === 'function') { | ||
|  |         var template = document.createElement('template'); | ||
|  |         if (template.content && template.content.ownerDocument) { | ||
|  |             document = template.content.ownerDocument; | ||
|  |         } | ||
|  |     } | ||
|  |     var implementation = document.implementation; | ||
|  |     var createNodeIterator = document.createNodeIterator; | ||
|  |     var getElementsByTagName = document.getElementsByTagName; | ||
|  |     var createDocumentFragment = document.createDocumentFragment; | ||
|  |     var importNode = originalDocument.importNode; | ||
|  | 
 | ||
|  |     var hooks = {}; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Expose whether this browser supports running the full DOMPurify. | ||
|  |      */ | ||
|  |     DOMPurify.isSupported = | ||
|  |         typeof implementation.createHTMLDocument !== 'undefined' && | ||
|  |         document.documentMode !== 9; | ||
|  | 
 | ||
|  |     /* Add properties to a lookup table */ | ||
|  |     var _addToSet = function(set, array) { | ||
|  |         var l = array.length; | ||
|  |         while (l--) { | ||
|  |             if (typeof array[l] === 'string') { | ||
|  |                 array[l] = array[l].toLowerCase(); | ||
|  |             } | ||
|  |             set[array[l]] = true; | ||
|  |         } | ||
|  |         return set; | ||
|  |     }; | ||
|  | 
 | ||
|  |     /* Shallow clone an object */ | ||
|  |     var _cloneObj = function(object) { | ||
|  |         var newObject = {}; | ||
|  |         var property; | ||
|  |         for (property in object) { | ||
|  |             if (object.hasOwnProperty(property)) { | ||
|  |                 newObject[property] = object[property]; | ||
|  |             } | ||
|  |         } | ||
|  |         return newObject; | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * We consider the elements and attributes below to be safe. Ideally | ||
|  |      * don't add any new ones but feel free to remove unwanted ones. | ||
|  |      */ | ||
|  | 
 | ||
|  |     /* allowed element names */ | ||
|  |     var ALLOWED_TAGS = null; | ||
|  |     var DEFAULT_ALLOWED_TAGS = _addToSet({}, [ | ||
|  | 
 | ||
|  |         // HTML
 | ||
|  |         'a','abbr','acronym','address','area','article','aside','audio','b', | ||
|  |         'bdi','bdo','big','blink','blockquote','body','br','button','canvas', | ||
|  |         'caption','center','cite','code','col','colgroup','content','data', | ||
|  |         'datalist','dd','decorator','del','details','dfn','dir','div','dl','dt', | ||
|  |         'element','em','fieldset','figcaption','figure','font','footer','form', | ||
|  |         'h1','h2','h3','h4','h5','h6','head','header','hgroup','hr','html','i', | ||
|  |         'img','input','ins','kbd','label','legend','li','main','map','mark', | ||
|  |         'marquee','menu','menuitem','meter','nav','nobr','ol','optgroup', | ||
|  |         'option','output','p','pre','progress','q','rp','rt','ruby','s','samp', | ||
|  |         'section','select','shadow','small','source','spacer','span','strike', | ||
|  |         'strong','style','sub','summary','sup','table','tbody','td','template', | ||
|  |         'textarea','tfoot','th','thead','time','tr','track','tt','u','ul','var', | ||
|  |         'video','wbr', | ||
|  | 
 | ||
|  |         // SVG
 | ||
|  |         'svg','altglyph','altglyphdef','altglyphitem','animatecolor', | ||
|  |         'animatemotion','animatetransform','circle','clippath','defs','desc', | ||
|  |         'ellipse','filter','font','g','glyph','glyphref','hkern','image','line', | ||
|  |         'lineargradient','marker','mask','metadata','mpath','path','pattern', | ||
|  |         'polygon','polyline','radialgradient','rect','stop','switch','symbol', | ||
|  |         'text','textpath','title','tref','tspan','view','vkern', | ||
|  | 
 | ||
|  |         // SVG Filters
 | ||
|  |         'feBlend','feColorMatrix','feComponentTransfer','feComposite', | ||
|  |         'feConvolveMatrix','feDiffuseLighting','feDisplacementMap', | ||
|  |         'feFlood','feFuncA','feFuncB','feFuncG','feFuncR','feGaussianBlur', | ||
|  |         'feMerge','feMergeNode','feMorphology','feOffset', | ||
|  |         'feSpecularLighting','feTile','feTurbulence', | ||
|  | 
 | ||
|  |         //MathML
 | ||
|  |         'math','menclose','merror','mfenced','mfrac','mglyph','mi','mlabeledtr', | ||
|  |         'mmuliscripts','mn','mo','mover','mpadded','mphantom','mroot','mrow', | ||
|  |         'ms','mpspace','msqrt','mystyle','msub','msup','msubsup','mtable','mtd', | ||
|  |         'mtext','mtr','munder','munderover', | ||
|  | 
 | ||
|  |         //Text
 | ||
|  |         '#text' | ||
|  |     ]); | ||
|  | 
 | ||
|  |     /* Allowed attribute names */ | ||
|  |     var ALLOWED_ATTR = null; | ||
|  |     var DEFAULT_ALLOWED_ATTR = _addToSet({}, [ | ||
|  | 
 | ||
|  |         // HTML
 | ||
|  |         'accept','action','align','alt','autocomplete','background','bgcolor', | ||
|  |         'border','cellpadding','cellspacing','checked','cite','class','clear','color', | ||
|  |         'cols','colspan','coords','datetime','default','dir','disabled', | ||
|  |         'download','enctype','face','for','headers','height','hidden','high','href', | ||
|  |         'hreflang','id','ismap','label','lang','list','loop', 'low','max', | ||
|  |         'maxlength','media','method','min','multiple','name','noshade','novalidate', | ||
|  |         'nowrap','open','optimum','pattern','placeholder','poster','preload','pubdate', | ||
|  |         'radiogroup','readonly','rel','required','rev','reversed','rows', | ||
|  |         'rowspan','spellcheck','scope','selected','shape','size','span', | ||
|  |         'srclang','start','src','step','style','summary','tabindex','title', | ||
|  |         'type','usemap','valign','value','width','xmlns', | ||
|  | 
 | ||
|  |         // SVG
 | ||
|  |         'accent-height','accumulate','additivive','alignment-baseline', | ||
|  |         'ascent','attributename','attributetype','azimuth','basefrequency', | ||
|  |         'baseline-shift','begin','bias','by','clip','clip-path','clip-rule', | ||
|  |         'color','color-interpolation','color-interpolation-filters','color-profile', | ||
|  |         'color-rendering','cx','cy','d','dx','dy','diffuseconstant','direction', | ||
|  |         'display','divisor','dur','edgemode','elevation','end','fill','fill-opacity', | ||
|  |         'fill-rule','filter','flood-color','flood-opacity','font-family','font-size', | ||
|  |         'font-size-adjust','font-stretch','font-style','font-variant','font-weight', | ||
|  |         'fx', 'fy','g1','g2','glyph-name','glyphref','gradientunits','gradienttransform', | ||
|  |         'image-rendering','in','in2','k','k1','k2','k3','k4','kerning','keypoints', | ||
|  |         'keysplines','keytimes','lengthadjust','letter-spacing','kernelmatrix', | ||
|  |         'kernelunitlength','lighting-color','local','marker-end','marker-mid', | ||
|  |         'marker-start','markerheight','markerunits','markerwidth','maskcontentunits', | ||
|  |         'maskunits','max','mask','mode','min','numoctaves','offset','operator', | ||
|  |         'opacity','order','orient','orientation','origin','overflow','paint-order', | ||
|  |         'path','pathlength','patterncontentunits','patterntransform','patternunits', | ||
|  |         'points','preservealpha','r','rx','ry','radius','refx','refy','repeatcount', | ||
|  |         'repeatdur','restart','result','rotate','scale','seed','shape-rendering', | ||
|  |         'specularconstant','specularexponent','spreadmethod','stddeviation','stitchtiles', | ||
|  |         'stop-color','stop-opacity','stroke-dasharray','stroke-dashoffset','stroke-linecap', | ||
|  |         'stroke-linejoin','stroke-miterlimit','stroke-opacity','stroke','stroke-width', | ||
|  |         'surfacescale','targetx','targety','transform','text-anchor','text-decoration', | ||
|  |         'text-rendering','textlength','u1','u2','unicode','values','viewbox', | ||
|  |         'visibility','vert-adv-y','vert-origin-x','vert-origin-y','word-spacing', | ||
|  |         'wrap','writing-mode','xchannelselector','ychannelselector','x','x1','x2', | ||
|  |         'y','y1','y2','z','zoomandpan', | ||
|  | 
 | ||
|  |         // MathML
 | ||
|  |         'accent','accentunder','bevelled','close','columnsalign','columnlines', | ||
|  |         'columnspan','denomalign','depth','display','displaystyle','fence', | ||
|  |         'frame','largeop','length','linethickness','lspace','lquote', | ||
|  |         'mathbackground','mathcolor','mathsize','mathvariant','maxsize', | ||
|  |         'minsize','movablelimits','notation','numalign','open','rowalign', | ||
|  |         'rowlines','rowspacing','rowspan','rspace','rquote','scriptlevel', | ||
|  |         'scriptminsize','scriptsizemultiplier','selection','separator', | ||
|  |         'separators','stretchy','subscriptshift','supscriptshift','symmetric', | ||
|  |         'voffset', | ||
|  | 
 | ||
|  |         // XML
 | ||
|  |         'xlink:href','xml:id','xlink:title','xml:space','xmlns:xlink' | ||
|  |     ]); | ||
|  | 
 | ||
|  |     /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ | ||
|  |     var FORBID_TAGS = null; | ||
|  | 
 | ||
|  |     /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ | ||
|  |     var FORBID_ATTR = null; | ||
|  | 
 | ||
|  |     /* Decide if custom data attributes are okay */ | ||
|  |     var ALLOW_DATA_ATTR = true; | ||
|  | 
 | ||
|  |     /* Decide if unknown protocols are okay */ | ||
|  |     var ALLOW_UNKNOWN_PROTOCOLS = false; | ||
|  | 
 | ||
|  |     /* Output should be safe for jQuery's $() factory? */ | ||
|  |     var SAFE_FOR_JQUERY = false; | ||
|  | 
 | ||
|  |     /* Output should be safe for common template engines. | ||
|  |      * This means, DOMPurify removes data attributes, mustaches and ERB | ||
|  |      */ | ||
|  |     var SAFE_FOR_TEMPLATES = false; | ||
|  | 
 | ||
|  |     /* Specify template detection regex for SAFE_FOR_TEMPLATES mode */ | ||
|  |     var MUSTACHE_EXPR = /\{\{[\s\S]*|[\s\S]*\}\}/gm; | ||
|  |     var ERB_EXPR = /<%[\s\S]*|[\s\S]*%>/gm; | ||
|  | 
 | ||
|  |     /* Decide if document with <html>... should be returned */ | ||
|  |     var WHOLE_DOCUMENT = false; | ||
|  | 
 | ||
|  |     /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html string. | ||
|  |      * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead | ||
|  |      */ | ||
|  |     var RETURN_DOM = false; | ||
|  | 
 | ||
|  |     /* Decide if a DOM `DocumentFragment` should be returned, instead of a html string */ | ||
|  |     var RETURN_DOM_FRAGMENT = false; | ||
|  | 
 | ||
|  |     /* If `RETURN_DOM` or `RETURN_DOM_FRAGMENT` is enabled, decide if the returned DOM | ||
|  |      * `Node` is imported into the current `Document`. If this flag is not enabled the | ||
|  |      * `Node` will belong (its ownerDocument) to a fresh `HTMLDocument`, created by | ||
|  |      * DOMPurify. */ | ||
|  |     var RETURN_DOM_IMPORT = false; | ||
|  | 
 | ||
|  |     /* Output should be free from DOM clobbering attacks? */ | ||
|  |     var SANITIZE_DOM = true; | ||
|  | 
 | ||
|  |     /* Keep element content when removing element? */ | ||
|  |     var KEEP_CONTENT = true; | ||
|  | 
 | ||
|  |     /* Tags to ignore content of when KEEP_CONTENT is true */ | ||
|  |     var FORBID_CONTENTS = _addToSet({}, [ | ||
|  |         'audio', 'head', 'math', 'script', 'style', 'svg', 'video' | ||
|  |     ]); | ||
|  | 
 | ||
|  |     /* Tags that are safe for data: URIs */ | ||
|  |     var DATA_URI_TAGS = _addToSet({}, [ | ||
|  |         'audio', 'video', 'img', 'source' | ||
|  |     ]); | ||
|  | 
 | ||
|  |     /* Attributes safe for values like "javascript:" */ | ||
|  |     var URI_SAFE_ATTRIBUTES = _addToSet({}, [ | ||
|  |         'alt','class','for','id','label','name','pattern','placeholder', | ||
|  |         'summary','title','value','style','xmlns' | ||
|  |     ]); | ||
|  | 
 | ||
|  |     /* Keep a reference to config to pass to hooks */ | ||
|  |     var CONFIG = null; | ||
|  | 
 | ||
|  |     /* Ideally, do not touch anything below this line */ | ||
|  |     /* ______________________________________________ */ | ||
|  | 
 | ||
|  |     var formElement = document.createElement('form'); | ||
|  | 
 | ||
|  |     /** | ||
|  |      * _parseConfig | ||
|  |      * | ||
|  |      * @param  optional config literal | ||
|  |      */ | ||
|  |     var _parseConfig = function(cfg) { | ||
|  |         /* Shield configuration object from tampering */ | ||
|  |         if (typeof cfg !== 'object') { | ||
|  |             cfg = {}; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Set configuration parameters */ | ||
|  |         ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? | ||
|  |             _addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS; | ||
|  |         ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? | ||
|  |             _addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR; | ||
|  |         FORBID_TAGS = 'FORBID_TAGS' in cfg ? | ||
|  |             _addToSet({}, cfg.FORBID_TAGS) : {}; | ||
|  |         FORBID_ATTR = 'FORBID_ATTR' in cfg ? | ||
|  |             _addToSet({}, cfg.FORBID_ATTR) : {}; | ||
|  |         ALLOW_DATA_ATTR     = cfg.ALLOW_DATA_ATTR     !== false; // Default true
 | ||
|  |         ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
 | ||
|  |         SAFE_FOR_JQUERY     = cfg.SAFE_FOR_JQUERY     ||  false; // Default false
 | ||
|  |         SAFE_FOR_TEMPLATES  = cfg.SAFE_FOR_TEMPLATES  ||  false; // Default false
 | ||
|  |         WHOLE_DOCUMENT      = cfg.WHOLE_DOCUMENT      ||  false; // Default false
 | ||
|  |         RETURN_DOM          = cfg.RETURN_DOM          ||  false; // Default false
 | ||
|  |         RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT ||  false; // Default false
 | ||
|  |         RETURN_DOM_IMPORT   = cfg.RETURN_DOM_IMPORT   ||  false; // Default false
 | ||
|  |         SANITIZE_DOM        = cfg.SANITIZE_DOM        !== false; // Default true
 | ||
|  |         KEEP_CONTENT        = cfg.KEEP_CONTENT        !== false; // Default true
 | ||
|  | 
 | ||
|  |         if (SAFE_FOR_TEMPLATES) { | ||
|  |             ALLOW_DATA_ATTR = false; | ||
|  |         } | ||
|  | 
 | ||
|  |         if (RETURN_DOM_FRAGMENT) { | ||
|  |             RETURN_DOM = true; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Merge configuration parameters */ | ||
|  |         if (cfg.ADD_TAGS) { | ||
|  |             if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { | ||
|  |                 ALLOWED_TAGS = _cloneObj(ALLOWED_TAGS); | ||
|  |             } | ||
|  |             _addToSet(ALLOWED_TAGS, cfg.ADD_TAGS); | ||
|  |         } | ||
|  |         if (cfg.ADD_ATTR) { | ||
|  |             if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { | ||
|  |                 ALLOWED_ATTR = _cloneObj(ALLOWED_ATTR); | ||
|  |             } | ||
|  |             _addToSet(ALLOWED_ATTR, cfg.ADD_ATTR); | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Add #text in case KEEP_CONTENT is set to true */ | ||
|  |         if (KEEP_CONTENT) { ALLOWED_TAGS['#text'] = true; } | ||
|  | 
 | ||
|  |         // Prevent further manipulation of configuration.
 | ||
|  |         // Not available in IE8, Safari 5, etc.
 | ||
|  |         if (Object && 'freeze' in Object) { Object.freeze(cfg); } | ||
|  | 
 | ||
|  |         CONFIG = cfg; | ||
|  |     }; | ||
|  | 
 | ||
|  |    /** | ||
|  |      * _forceRemove | ||
|  |      * | ||
|  |      * @param  a DOM node | ||
|  |      */ | ||
|  |     var _forceRemove = function(node) { | ||
|  |         try { | ||
|  |             node.parentNode.removeChild(node); | ||
|  |         } catch (e) { | ||
|  |             node.outerHTML = ''; | ||
|  |         } | ||
|  |     }; | ||
|  | 
 | ||
|  |    /** | ||
|  |      * _initDocument | ||
|  |      * | ||
|  |      * @param  a string of dirty markup | ||
|  |      * @return a DOM, filled with the dirty markup | ||
|  |      */ | ||
|  |     var _initDocument = function(dirty) { | ||
|  |         /* Create a HTML document using DOMParser */ | ||
|  |         var doc, body; | ||
|  |         try { | ||
|  |             doc = new DOMParser().parseFromString(dirty, 'text/html'); | ||
|  |         } catch (e) {} | ||
|  | 
 | ||
|  |         /* Some browsers throw, some browsers return null for the code above | ||
|  |            DOMParser with text/html support is only in very recent browsers. */ | ||
|  |         if (!doc) { | ||
|  |             doc = implementation.createHTMLDocument(''); | ||
|  |             body = doc.body; | ||
|  |             body.parentNode.removeChild(body.parentNode.firstElementChild); | ||
|  |             body.outerHTML = dirty; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Work on whole document or just its body */ | ||
|  |         if (typeof doc.getElementsByTagName === 'function') { | ||
|  |             return doc.getElementsByTagName( | ||
|  |                 WHOLE_DOCUMENT ? 'html' : 'body')[0]; | ||
|  |         } | ||
|  |         return getElementsByTagName.call(doc, | ||
|  |             WHOLE_DOCUMENT ? 'html' : 'body')[0]; | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * _createIterator | ||
|  |      * | ||
|  |      * @param  document/fragment to create iterator for | ||
|  |      * @return iterator instance | ||
|  |      */ | ||
|  |     var _createIterator = function(root) { | ||
|  |         return createNodeIterator.call(root.ownerDocument || root, | ||
|  |             root, | ||
|  |             NodeFilter.SHOW_ELEMENT | ||
|  |             | NodeFilter.SHOW_COMMENT | ||
|  |             | NodeFilter.SHOW_TEXT, | ||
|  |             function() { return NodeFilter.FILTER_ACCEPT; }, | ||
|  |             false | ||
|  |         ); | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * _isClobbered | ||
|  |      * | ||
|  |      * @param  element to check for clobbering attacks | ||
|  |      * @return true if clobbered, false if safe | ||
|  |      */ | ||
|  |     var _isClobbered = function(elm) { | ||
|  |         if (elm instanceof Text || elm instanceof Comment) { | ||
|  |             return false; | ||
|  |         } | ||
|  |         if (  typeof elm.nodeName !== 'string' | ||
|  |            || typeof elm.textContent !== 'string' | ||
|  |            || typeof elm.removeChild !== 'function' | ||
|  |            || !(elm.attributes instanceof NamedNodeMap) | ||
|  |            || typeof elm.removeAttribute !== 'function' | ||
|  |            || typeof elm.setAttribute !== 'function' | ||
|  |         ) { | ||
|  |             return true; | ||
|  |         } | ||
|  |         return false; | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * _sanitizeElements | ||
|  |      * | ||
|  |      * @protect nodeName | ||
|  |      * @protect textContent | ||
|  |      * @protect removeChild | ||
|  |      * | ||
|  |      * @param   node to check for permission to exist | ||
|  |      * @return  true if node was killed, false if left alive | ||
|  |      */ | ||
|  |     var _sanitizeElements = function(currentNode) { | ||
|  |         var tagName, content; | ||
|  |         /* Execute a hook if present */ | ||
|  |         _executeHook('beforeSanitizeElements', currentNode, null); | ||
|  | 
 | ||
|  |         /* Check if element is clobbered or can clobber */ | ||
|  |         if (_isClobbered(currentNode)) { | ||
|  |             _forceRemove(currentNode); | ||
|  |             return true; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Now let's check the element's type and name */ | ||
|  |         tagName = currentNode.nodeName.toLowerCase(); | ||
|  | 
 | ||
|  |         /* Execute a hook if present */ | ||
|  |         _executeHook('uponSanitizeElement', currentNode, { | ||
|  |             tagName: tagName | ||
|  |         }); | ||
|  | 
 | ||
|  |         /* Remove element if anything forbids its presence */ | ||
|  |         if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { | ||
|  |             /* Keep content except for black-listed elements */ | ||
|  |             if (KEEP_CONTENT && !FORBID_CONTENTS[tagName] | ||
|  |                     && typeof currentNode.insertAdjacentHTML === 'function') { | ||
|  |                 try { | ||
|  |                     currentNode.insertAdjacentHTML('AfterEnd', currentNode.innerHTML); | ||
|  |                 } catch (e) {} | ||
|  |             } | ||
|  |             _forceRemove(currentNode); | ||
|  |             return true; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Convert markup to cover jQuery behavior */ | ||
|  |         if (SAFE_FOR_JQUERY && !currentNode.firstElementChild && | ||
|  |                 (!currentNode.content || !currentNode.content.firstElementChild)) { | ||
|  |             currentNode.innerHTML = currentNode.textContent.replace(/</g, '<'); | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Sanitize element content to be template-safe */ | ||
|  |         if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) { | ||
|  |             /* Get the element's text content */ | ||
|  |             content = currentNode.textContent; | ||
|  |             content = content.replace(MUSTACHE_EXPR, ' '); | ||
|  |             content = content.replace(ERB_EXPR, ' '); | ||
|  |             currentNode.textContent = content; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Execute a hook if present */ | ||
|  |         _executeHook('afterSanitizeElements', currentNode, null); | ||
|  | 
 | ||
|  |         return false; | ||
|  |     }; | ||
|  | 
 | ||
|  |     var DATA_ATTR = /^data-[\w.\u00B7-\uFFFF-]/; | ||
|  |     var IS_ALLOWED_URI = /^(?:(?:(?:f|ht)tps?|mailto|tel):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i; | ||
|  |     var IS_SCRIPT_OR_DATA = /^(?:\w+script|data):/i; | ||
|  |     /* This needs to be extensive thanks to Webkit/Blink's behavior */ | ||
|  |     var ATTR_WHITESPACE = /[\x00-\x20\xA0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * _sanitizeAttributes | ||
|  |      * | ||
|  |      * @protect attributes | ||
|  |      * @protect nodeName | ||
|  |      * @protect removeAttribute | ||
|  |      * @protect setAttribute | ||
|  |      * | ||
|  |      * @param   node to sanitize | ||
|  |      * @return  void | ||
|  |      */ | ||
|  |     var _sanitizeAttributes = function(currentNode) { | ||
|  |         var attr, name, value, lcName, idAttr, attributes, hookEvent, l; | ||
|  |         /* Execute a hook if present */ | ||
|  |         _executeHook('beforeSanitizeAttributes', currentNode, null); | ||
|  | 
 | ||
|  |         attributes = currentNode.attributes; | ||
|  | 
 | ||
|  |         /* Check if we have attributes; if not we might have a text node */ | ||
|  |         if (!attributes) { return; } | ||
|  | 
 | ||
|  |         hookEvent = { | ||
|  |             attrName: '', | ||
|  |             attrValue: '', | ||
|  |             keepAttr: true | ||
|  |         }; | ||
|  |         l = attributes.length; | ||
|  | 
 | ||
|  |         /* Go backwards over all attributes; safely remove bad ones */ | ||
|  |         while (l--) { | ||
|  |             attr = attributes[l]; | ||
|  |             name = attr.name; | ||
|  |             value = attr.value; | ||
|  |             lcName = name.toLowerCase(); | ||
|  | 
 | ||
|  |             /* Execute a hook if present */ | ||
|  |             hookEvent.attrName = lcName; | ||
|  |             hookEvent.attrValue = value; | ||
|  |             hookEvent.keepAttr = true; | ||
|  |             _executeHook('uponSanitizeAttribute', currentNode, hookEvent ); | ||
|  |             value = hookEvent.attrValue; | ||
|  | 
 | ||
|  |             /* Remove attribute */ | ||
|  |             // Safari (iOS + Mac), last tested v8.0.5, crashes if you try to
 | ||
|  |             // remove a "name" attribute from an <img> tag that has an "id"
 | ||
|  |             // attribute at the time.
 | ||
|  |             if (lcName === 'name'  && | ||
|  |                     currentNode.nodeName === 'IMG' && attributes.id) { | ||
|  |                 idAttr = attributes.id; | ||
|  |                 attributes = Array.prototype.slice.apply(attributes); | ||
|  |                 currentNode.removeAttribute('id'); | ||
|  |                 currentNode.removeAttribute(name); | ||
|  |                 if (attributes.indexOf(idAttr) > l) { | ||
|  |                     currentNode.setAttribute('id', idAttr.value); | ||
|  |                 } | ||
|  |             } else { | ||
|  |                 // This avoids a crash in Safari v9.0 with double-ids.
 | ||
|  |                 // The trick is to first set the id to be empty and then to
 | ||
|  |                 // remove the attriubute
 | ||
|  |                 if (name === 'id') { | ||
|  |                     currentNode.setAttribute(name, ''); | ||
|  |                 } | ||
|  |                 currentNode.removeAttribute(name); | ||
|  |             } | ||
|  | 
 | ||
|  |             /* Did the hooks approve of the attribute? */ | ||
|  |             if (!hookEvent.keepAttr) { | ||
|  |                 continue; | ||
|  |             } | ||
|  | 
 | ||
|  |             /* Make sure attribute cannot clobber */ | ||
|  |             if (SANITIZE_DOM && | ||
|  |                     (lcName === 'id' || lcName === 'name') && | ||
|  |                     (value in window || value in document || value in formElement)) { | ||
|  |                 continue; | ||
|  |             } | ||
|  | 
 | ||
|  |             /* Sanitize attribute content to be template-safe */ | ||
|  |             if (SAFE_FOR_TEMPLATES) { | ||
|  |                 value = value.replace(MUSTACHE_EXPR, ' '); | ||
|  |                 value = value.replace(ERB_EXPR, ' '); | ||
|  |             } | ||
|  | 
 | ||
|  |             if ( | ||
|  |                 /* Check the name is permitted */ | ||
|  |                 (ALLOWED_ATTR[lcName] && !FORBID_ATTR[lcName] && ( | ||
|  |                   /* Check no script, data or unknown possibly unsafe URI | ||
|  |                      unless we know URI values are safe for that attribute */ | ||
|  |                   URI_SAFE_ATTRIBUTES[lcName] || | ||
|  |                   IS_ALLOWED_URI.test(value.replace(ATTR_WHITESPACE,'')) || | ||
|  |                   /* Keep image data URIs alive if src is allowed */ | ||
|  |                   (lcName === 'src' && value.indexOf('data:') === 0 && | ||
|  |                    DATA_URI_TAGS[currentNode.nodeName.toLowerCase()]) | ||
|  |                 )) || | ||
|  |                 /* Allow potentially valid data-* attributes: | ||
|  |                  * At least one character after "-" (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
 | ||
|  |                  * XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
 | ||
|  |                  * We don't need to check the value; it's always URI safe. | ||
|  |                  */ | ||
|  |                  (ALLOW_DATA_ATTR && DATA_ATTR.test(lcName)) || | ||
|  |                  /* Allow unknown protocols: | ||
|  |                   * This provides support for links that are handled by protocol handlers which may be unknown | ||
|  |                   * ahead of time, e.g. fb:, spotify: | ||
|  |                   */ | ||
|  |                  (ALLOW_UNKNOWN_PROTOCOLS && !IS_SCRIPT_OR_DATA.test(value.replace(ATTR_WHITESPACE,''))) | ||
|  |             ) { | ||
|  |                 /* Handle invalid data-* attribute set by try-catching it */ | ||
|  |                 try { | ||
|  |                     currentNode.setAttribute(name, value); | ||
|  |                 } catch (e) {} | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Execute a hook if present */ | ||
|  |         _executeHook('afterSanitizeAttributes', currentNode, null); | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * _sanitizeShadowDOM | ||
|  |      * | ||
|  |      * @param  fragment to iterate over recursively | ||
|  |      * @return void | ||
|  |      */ | ||
|  |     var _sanitizeShadowDOM = function(fragment) { | ||
|  |         var shadowNode; | ||
|  |         var shadowIterator = _createIterator(fragment); | ||
|  | 
 | ||
|  |         /* Execute a hook if present */ | ||
|  |         _executeHook('beforeSanitizeShadowDOM', fragment, null); | ||
|  | 
 | ||
|  |         while ( (shadowNode = shadowIterator.nextNode()) ) { | ||
|  |             /* Execute a hook if present */ | ||
|  |             _executeHook('uponSanitizeShadowNode', shadowNode, null); | ||
|  | 
 | ||
|  |             /* Sanitize tags and elements */ | ||
|  |             if (_sanitizeElements(shadowNode)) { | ||
|  |                 continue; | ||
|  |             } | ||
|  | 
 | ||
|  |             /* Deep shadow DOM detected */ | ||
|  |             if (shadowNode.content instanceof DocumentFragment) { | ||
|  |                 _sanitizeShadowDOM(shadowNode.content); | ||
|  |             } | ||
|  | 
 | ||
|  |             /* Check attributes, sanitize if necessary */ | ||
|  |             _sanitizeAttributes(shadowNode); | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Execute a hook if present */ | ||
|  |         _executeHook('afterSanitizeShadowDOM', fragment, null); | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * _executeHook | ||
|  |      * Execute user configurable hooks | ||
|  |      * | ||
|  |      * @param  {String} entryPoint  Name of the hook's entry point | ||
|  |      * @param  {Node} currentNode | ||
|  |      */ | ||
|  |     var _executeHook = function(entryPoint, currentNode, data) { | ||
|  |         if (!hooks[entryPoint]) { return; } | ||
|  | 
 | ||
|  |         hooks[entryPoint].forEach(function(hook) { | ||
|  |             hook.call(DOMPurify, currentNode, data, CONFIG); | ||
|  |         }); | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * sanitize | ||
|  |      * Public method providing core sanitation functionality | ||
|  |      * | ||
|  |      * @param {String} dirty string | ||
|  |      * @param {Object} configuration object | ||
|  |      */ | ||
|  |     DOMPurify.sanitize = function(dirty, cfg) { | ||
|  |         var body, currentNode, oldNode, nodeIterator, returnNode; | ||
|  |         /* Make sure we have a string to sanitize. | ||
|  |            DO NOT return early, as this will return the wrong type if | ||
|  |            the user has requested a DOM object rather than a string */ | ||
|  |         if (!dirty) { | ||
|  |             dirty = ''; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Stringify, in case dirty is an object */ | ||
|  |         if (typeof dirty !== 'string') { | ||
|  |             if (typeof dirty.toString !== 'function') { | ||
|  |                 throw new TypeError('toString is not a function'); | ||
|  |             } else { | ||
|  |                 dirty = dirty.toString(); | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Check we can run. Otherwise fall back or ignore */ | ||
|  |         if (!DOMPurify.isSupported) { | ||
|  |             if (typeof window.toStaticHTML === 'object' | ||
|  |                 || typeof window.toStaticHTML === 'function') { | ||
|  |                 return window.toStaticHTML(dirty); | ||
|  |             } | ||
|  |             return dirty; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Assign config vars */ | ||
|  |         _parseConfig(cfg); | ||
|  | 
 | ||
|  |         /* Exit directly if we have nothing to do */ | ||
|  |         if (!RETURN_DOM && !WHOLE_DOCUMENT && dirty.indexOf('<') === -1) { | ||
|  |             return dirty; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Initialize the document to work on */ | ||
|  |         body = _initDocument(dirty); | ||
|  | 
 | ||
|  |         /* Check we have a DOM node from the data */ | ||
|  |         if (!body) { | ||
|  |             return RETURN_DOM ? null : ''; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Get node iterator */ | ||
|  |         nodeIterator = _createIterator(body); | ||
|  | 
 | ||
|  |         /* Now start iterating over the created document */ | ||
|  |         while ( (currentNode = nodeIterator.nextNode()) ) { | ||
|  | 
 | ||
|  |             /* Fix IE's strange behavior with manipulated textNodes #89 */ | ||
|  |             if (currentNode.nodeType === 3 && currentNode === oldNode) { | ||
|  |                 continue; | ||
|  |             } | ||
|  | 
 | ||
|  |             /* Sanitize tags and elements */ | ||
|  |             if (_sanitizeElements(currentNode)) { | ||
|  |                 continue; | ||
|  |             } | ||
|  | 
 | ||
|  |             /* Shadow DOM detected, sanitize it */ | ||
|  |             if (currentNode.content instanceof DocumentFragment) { | ||
|  |                 _sanitizeShadowDOM(currentNode.content); | ||
|  |             } | ||
|  | 
 | ||
|  |             /* Check attributes, sanitize if necessary */ | ||
|  |             _sanitizeAttributes(currentNode); | ||
|  | 
 | ||
|  |             oldNode = currentNode; | ||
|  |         } | ||
|  | 
 | ||
|  |         /* Return sanitized string or DOM */ | ||
|  |         if (RETURN_DOM) { | ||
|  | 
 | ||
|  |             if (RETURN_DOM_FRAGMENT) { | ||
|  |                 returnNode = createDocumentFragment.call(body.ownerDocument); | ||
|  | 
 | ||
|  |                 while (body.firstChild) { | ||
|  |                     returnNode.appendChild(body.firstChild); | ||
|  |                 } | ||
|  |             } else { | ||
|  |                 returnNode = body; | ||
|  |             } | ||
|  | 
 | ||
|  |             if (RETURN_DOM_IMPORT) { | ||
|  |                 /* adoptNode() is not used because internal state is not reset | ||
|  |                    (e.g. the past names map of a HTMLFormElement), this is safe | ||
|  |                    in theory but we would rather not risk another attack vector. | ||
|  |                    The state that is cloned by importNode() is explicitly defined | ||
|  |                    by the specs. */ | ||
|  |                 returnNode = importNode.call(originalDocument, returnNode, true); | ||
|  |             } | ||
|  | 
 | ||
|  |             return returnNode; | ||
|  |         } | ||
|  | 
 | ||
|  |         return WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * addHook | ||
|  |      * Public method to add DOMPurify hooks | ||
|  |      * | ||
|  |      * @param {String} entryPoint | ||
|  |      * @param {Function} hookFunction | ||
|  |      */ | ||
|  |     DOMPurify.addHook = function(entryPoint, hookFunction) { | ||
|  |         if (typeof hookFunction !== 'function') { return; } | ||
|  |         hooks[entryPoint] = hooks[entryPoint] || []; | ||
|  |         hooks[entryPoint].push(hookFunction); | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * removeHook | ||
|  |      * Public method to remove a DOMPurify hook at a given entryPoint | ||
|  |      * (pops it from the stack of hooks if more are present) | ||
|  |      * | ||
|  |      * @param {String} entryPoint | ||
|  |      * @return void | ||
|  |      */ | ||
|  |     DOMPurify.removeHook = function(entryPoint) { | ||
|  |         if (hooks[entryPoint]) { | ||
|  |             hooks[entryPoint].pop(); | ||
|  |         } | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * removeHooks | ||
|  |      * Public method to remove all DOMPurify hooks at a given entryPoint | ||
|  |      * | ||
|  |      * @param  {String} entryPoint | ||
|  |      * @return void | ||
|  |      */ | ||
|  |     DOMPurify.removeHooks = function(entryPoint) { | ||
|  |         if (hooks[entryPoint]) { | ||
|  |             hooks[entryPoint] = []; | ||
|  |         } | ||
|  |     }; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * removeAllHooks | ||
|  |      * Public method to remove all DOMPurify hooks | ||
|  |      * | ||
|  |      * @return void | ||
|  |      */ | ||
|  |     DOMPurify.removeAllHooks = function() { | ||
|  |         hooks = []; | ||
|  |     }; | ||
|  | 
 | ||
|  |     return DOMPurify; | ||
|  | })); |