// Copyright 2013 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview * HTML tag filtering, and balancing. * A more user-friendly API is exposed via {@code goog.labs.html.sanitizer}. * @visibility {//visibility:private} */ goog.provide('goog.labs.html.scrubber'); goog.require('goog.array'); goog.require('goog.dom.tags'); goog.require('goog.labs.html.attributeRewriterPresubmitWorkaround'); goog.require('goog.string'); /** * Replaces tags not on the white-list with empty text nodes, dropping all * attributes, and drops other non-text nodes such as comments. * * @param {!Object.<string, boolean>} tagWhitelist a set of lower-case tag names * following the convention established by {@link goog.object.createSet}. * @param {!Object.<string, Object.<string, goog.labs.html.AttributeRewriter>>} * attrWhitelist * maps lower-case tag names and the special string {@code "*"} to functions * from decoded attribute values to sanitized values or {@code null} to * indicate that the attribute is not allowed with that value. * * For example, if {@code attrWhitelist['a']['href']} is defined then it * is used to sanitize the value of the link's URL. * * If {@code attrWhitelist['*']['id']} is defined, and * {@code attrWhitelist['div']['id']} is not, then the former is used to * sanitize any {@code id} attribute on a {@code <div>} element. * @param {string} html a string of HTML * @return {string} the input but with potentially dangerous tokens removed. */ goog.labs.html.scrubber.scrub = function(tagWhitelist, attrWhitelist, html) { return goog.labs.html.scrubber.render_( goog.labs.html.scrubber.balance_( goog.labs.html.scrubber.filter_( tagWhitelist, attrWhitelist, goog.labs.html.scrubber.lex_(html)))); }; /** * Balances tags in trusted HTML. * @param {string} html a string of HTML * @return {string} the input but with an end-tag for each non-void start tag * and only for non-void start tags, and with start and end tags nesting * properly. */ goog.labs.html.scrubber.balance = function(html) { return goog.labs.html.scrubber.render_( goog.labs.html.scrubber.balance_( goog.labs.html.scrubber.lex_(html))); }; /** Character code constant for {@code '<'}. @private */ goog.labs.html.scrubber.CC_LT_ = '<'.charCodeAt(0); /** Character code constant for {@code '!'}. @private */ goog.labs.html.scrubber.CC_BANG_ = '!'.charCodeAt(0); /** Character code constant for {@code '/'}. @private */ goog.labs.html.scrubber.CC_SLASH_ = '/'.charCodeAt(0); /** Character code constant for {@code '?'}. @private */ goog.labs.html.scrubber.CC_QMARK_ = '?'.charCodeAt(0); /** * Matches content following a tag name or attribute value, and before the * beginning of the next attribute value. * @private */ goog.labs.html.scrubber.ATTR_VALUE_PRECEDER_ = '[^=>]+'; /** @private */ goog.labs.html.scrubber.UNQUOTED_ATTR_VALUE_ = '(?:[^"\'\\s>][^\\s>]*)'; /** @private */ goog.labs.html.scrubber.DOUBLE_QUOTED_ATTR_VALUE_ = '(?:"[^"]*"?)'; /** @private */ goog.labs.html.scrubber.SINGLE_QUOTED_ATTR_VALUE_ = "(?:'[^']*'?)"; /** * Matches the equals-sign and any attribute value following it, but does not * capture any {@code >} that would close the tag. * @private */ goog.labs.html.scrubber.ATTR_VALUE_ = '=\\s*(?:' + goog.labs.html.scrubber.UNQUOTED_ATTR_VALUE_ + '|' + goog.labs.html.scrubber.DOUBLE_QUOTED_ATTR_VALUE_ + '|' + goog.labs.html.scrubber.SINGLE_QUOTED_ATTR_VALUE_ + ')?'; /** * The body of a tag between the end of the name and the closing {@code >} * if any. * @private */ goog.labs.html.scrubber.ATTRS_ = '(?:' + goog.labs.html.scrubber.ATTR_VALUE_PRECEDER_ + '|' + goog.labs.html.scrubber.ATTR_VALUE_ + ')*'; /** * A character that continues a tag name as defined at * http://www.w3.org/html/wg/drafts/html/master/syntax.html#tag-name-state * @private */ goog.labs.html.scrubber.TAG_NAME_CHAR_ = '[^\t\f\n />]'; /** * Matches when the next character cannot continue a tag name. * @private */ goog.labs.html.scrubber.BREAK_ = '(?!' + goog.labs.html.scrubber.TAG_NAME_CHAR_ + ')'; /** * Matches the open tag and body of a special element : * one whose body cannot contain nested elements so uses special parsing rules. * It does not include the end tag. * @private */ goog.labs.html.scrubber.SPECIAL_ELEMENT_ = '<(?:' + // Special tag name. '(iframe|script|style|textarea|title|xmp)' + // End of tag name goog.labs.html.scrubber.BREAK_ + // Attributes goog.labs.html.scrubber.ATTRS_ + '>' + // Element content includes non '<' characters, and // '<' that don't start a matching end tag. // This uses a back-reference to the tag name to determine whether // the tag names match. // Since matching is case-insensitive, this can only be used in // a case-insensitive regular expression. // JavaScript does not treat Turkish dotted I's as equivalent to their // ASCII equivalents. '(?:[^<]|<(?!/\\1' + goog.labs.html.scrubber.BREAK_ + '))*' + ')'; /** * Regexp pattern for an HTML tag. * @private */ goog.labs.html.scrubber.TAG_ = '<[/]?[a-z]' + goog.labs.html.scrubber.TAG_NAME_CHAR_ + '*' + goog.labs.html.scrubber.ATTRS_ + '>?'; /** * Regexp pattern for an HTML text node. * @private */ goog.labs.html.scrubber.TEXT_NODE_ = '(?:[^<]|<(?![a-z]|[?!/]))+'; /** * Matches HTML comments including HTML 5 "bogus comments" of the form * {@code <!...>} or {@code <?...>} or {@code </...>}. * @private */ goog.labs.html.scrubber.COMMENT_ = '<!--(?:[^\\-]|-+(?![\\->]))*(?:-(?:->?)?)?' + '|<[!?/][^>]*>?'; /** * Regexp pattern for an HTML token after a doctype. * Special elements introduces a capturing group for use with a * back-reference. * @private */ goog.labs.html.scrubber.HTML_TOKENS_RE_ = new RegExp( '(?:' + goog.labs.html.scrubber.TEXT_NODE_ + '|' + goog.labs.html.scrubber.SPECIAL_ELEMENT_ + '|' + goog.labs.html.scrubber.TAG_ + '|' + goog.labs.html.scrubber.COMMENT_ + ')', 'ig'); /** * An HTML tag which captures the name in group 1, * and any attributes in group 2. * @private */ goog.labs.html.scrubber.TAG_RE_ = new RegExp( '<[/]?([a-z]' + goog.labs.html.scrubber.TAG_NAME_CHAR_ + '*)' + '(' + goog.labs.html.scrubber.ATTRS_ + ')>?', 'i'); /** * A global matcher that separates attributes out of the tag body cruft. * @private */ goog.labs.html.scrubber.ATTRS_RE_ = new RegExp( '[^=\\s]+\\s*(?:' + goog.labs.html.scrubber.ATTR_VALUE_ + ')?', 'ig'); /** * Returns an array of HTML tokens including tags, text nodes and comments. * "Special" elements, like {@code <script>...</script>} whose bodies cannot * include nested elements, are returned as single tokens. * * @param {string} html a string of HTML * @return {!Array.<string>} * @private */ goog.labs.html.scrubber.lex_ = function(html) { return ('' + html).match(goog.labs.html.scrubber.HTML_TOKENS_RE_) || []; }; /** * Replaces tags not on the white-list with empty text nodes, dropping all * attributes, and drops other non-text nodes such as comments. * * @param {!Object.<string, boolean>} tagWhitelist a set of lower-case tag names * following the convention established by {@link goog.object.createSet}. * @param {!Object.<string, Object.<string, goog.labs.html.AttributeRewriter>> * } attrWhitelist * maps lower-case tag names and the special string {@code "*"} to functions * from decoded attribute values to sanitized values or {@code null} to * indicate that the attribute is not allowed with that value. * * For example, if {@code attrWhitelist['a']['href']} is defined then it is * used to sanitize the value of the link's URL. * * If {@code attrWhitelist['*']['id']} is defined, and * {@code attrWhitelist['div']['id']} is not, then the former is used to * sanitize any {@code id} attribute on a {@code <div>} element. * @param {!Array.<string>} htmlTokens an array of HTML tokens as returned by * {@link goog.labs.html.scrubber.lex_}. * @return {!Array.<string>} the input array modified in place to have some * tokens removed. * @private */ goog.labs.html.scrubber.filter_ = function( tagWhitelist, attrWhitelist, htmlTokens) { var genericAttrWhitelist = attrWhitelist['*']; for (var i = 0, n = htmlTokens.length; i < n; ++i) { var htmlToken = htmlTokens[i]; if (htmlToken.charCodeAt(0) !== goog.labs.html.scrubber.CC_LT_) { // Definitely not a tag continue; } var tag = htmlToken.match(goog.labs.html.scrubber.TAG_RE_); if (tag) { var lowerCaseTagName = tag[1].toLowerCase(); var isCloseTag = htmlToken.charCodeAt(1) === goog.labs.html.scrubber.CC_SLASH_; var attrs = ''; if (!isCloseTag && tag[2]) { var tagSpecificAttrWhitelist = /** @type {Object.<string, goog.labs.html.AttributeRewriter>} */ ( goog.labs.html.scrubber.readOwnProperty_( attrWhitelist, lowerCaseTagName)); if (genericAttrWhitelist || tagSpecificAttrWhitelist) { attrs = goog.labs.html.scrubber.filterAttrs_( tag[2], genericAttrWhitelist, tagSpecificAttrWhitelist); } } var specialContent = htmlToken.substring(tag[0].length); htmlTokens[i] = (tagWhitelist[lowerCaseTagName] === true) ? ( (isCloseTag ? '</' : '<') + lowerCaseTagName + attrs + '>' + specialContent ) : ''; } else if (htmlToken.length > 1) { switch (htmlToken.charCodeAt(1)) { case goog.labs.html.scrubber.CC_BANG_: case goog.labs.html.scrubber.CC_SLASH_: case goog.labs.html.scrubber.CC_QMARK_: htmlTokens[i] = ''; // Elide comments. break; default: // Otherwise, token is just a text node that starts with '<'. // Speed up later passes by normalizing the text node. htmlTokens[i] = htmlTokens[i].replace(/</g, '<'); } } } return htmlTokens; }; /** * Parses attribute names and values out of a tag body and applies the attribute * white-list to produce a tag body containing only safe attributes. * * @param {string} attrsText the text of a tag between the end of the tag name * and the beginning of the tag end marker, so {@code " foo bar='baz'"} for * the tag {@code <tag foo bar='baz'/>}. * @param {Object.<string, goog.labs.html.AttributeRewriter>} * genericAttrWhitelist * a whitelist of attribute transformations for attributes that are allowed * on any element. * @param {Object.<string, goog.labs.html.AttributeRewriter>} * tagSpecificAttrWhitelist * a whitelist of attribute transformations for attributes that are allowed * on the element started by the tag whose body is {@code tagBody}. * @return {string} a tag-body that consists only of safe attributes. * @private */ goog.labs.html.scrubber.filterAttrs_ = function(attrsText, genericAttrWhitelist, tagSpecificAttrWhitelist) { var attrs = attrsText.match(goog.labs.html.scrubber.ATTRS_RE_); var nAttrs = attrs ? attrs.length : 0; var safeAttrs = ''; for (var i = 0; i < nAttrs; ++i) { var attr = attrs[i]; var eq = attr.indexOf('='); var name, value; if (eq >= 0) { name = goog.string.trim(attr.substr(0, eq)); value = goog.string.stripQuotes( goog.string.trim(attr.substr(eq + 1)), '"\''); } else { name = value = attr; } name = name.toLowerCase(); var rewriter = /** @type {?goog.labs.html.AttributeRewriter} */ ( tagSpecificAttrWhitelist && goog.labs.html.scrubber.readOwnProperty_( tagSpecificAttrWhitelist, name) || genericAttrWhitelist && goog.labs.html.scrubber.readOwnProperty_(genericAttrWhitelist, name)); if (rewriter) { var safeValue = rewriter(goog.string.unescapeEntities(value)); if (safeValue != null) { if (safeValue.implementsGoogStringTypedString) { safeValue = /** @type {goog.string.TypedString} */ (safeValue).getTypedStringValue(); } safeValue = String(safeValue); if (safeValue.indexOf('`') >= 0) { safeValue += ' '; } safeAttrs += ' ' + name + '="' + goog.string.htmlEscape(safeValue, false) + '"'; } } } return safeAttrs; }; /** * @param {!Object} o the object * @param {!string} k a key into o * @return {*} * @private */ goog.labs.html.scrubber.readOwnProperty_ = function(o, k) { return Object.prototype.hasOwnProperty.call(o, k) ? o[k] : undefined; }; /** * We limit the nesting limit of balanced HTML to a large but manageable number * so that built-in browser limits aren't likely to kick in and undo all our * matching of start and end tags. * <br> * This mitigates the HTML parsing equivalent of stack smashing attacks. * <br> * Otherwise, crafted inputs like * {@code <p><p><p><p>...Ad nauseam...</p></p></p></p>} could exploit * browser bugs, and/or undocumented nesting limit recovery code to misnest * tags. * @private * @const */ goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_ = 256; /** * Ensures that there are end-tags for all and only for non-void start tags. * @param {Array.<string>} htmlTokens an array of HTML tokens as returned by * {@link goog.labs.html.scrubber.lex}. * @return {!Array.<string>} the input array modified in place to have some * tokens removed. * @private */ goog.labs.html.scrubber.balance_ = function(htmlTokens) { var openElementStack = []; for (var i = 0, n = htmlTokens.length; i < n; ++i) { var htmlToken = htmlTokens[i]; if (htmlToken.charCodeAt(0) !== goog.labs.html.scrubber.CC_LT_) { // Definitely not a tag continue; } var tag = htmlToken.match(goog.labs.html.scrubber.TAG_RE_); if (tag) { var lowerCaseTagName = tag[1].toLowerCase(); var isCloseTag = htmlToken.charCodeAt(1) === goog.labs.html.scrubber.CC_SLASH_; // Special case: HTML5 mandates that </br> be treated as <br>. if (isCloseTag && lowerCaseTagName == 'br') { isCloseTag = false; htmlToken = '<br>'; } var isVoidTag = goog.dom.tags.isVoidTag(lowerCaseTagName); if (isVoidTag && isCloseTag) { htmlTokens[i] = ''; continue; } var prefix = ''; // Insert implied open tags. var nOpenElements = openElementStack.length; if (nOpenElements && !isCloseTag) { var top = openElementStack[nOpenElements - 1]; var groups = goog.labs.html.scrubber.ELEMENT_GROUPS_[lowerCaseTagName]; if (groups === undefined) { groups = goog.labs.html.scrubber.Group_.INLINE_; } var canContain = goog.labs.html.scrubber.ELEMENT_CONTENTS_[top]; if (!(groups & canContain)) { var blockContainer = goog.labs.html.scrubber.BLOCK_CONTAINERS_[top]; if ('string' === typeof blockContainer) { var containerCanContain = goog.labs.html.scrubber.ELEMENT_CONTENTS_[blockContainer]; if (containerCanContain & groups) { if (nOpenElements < goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_) { prefix = '<' + blockContainer + '>'; } openElementStack[nOpenElements] = blockContainer; ++nOpenElements; } } } } // Insert any missing close tags we need. var newStackLen = goog.labs.html.scrubber.pickElementsToClose_( lowerCaseTagName, isCloseTag, openElementStack); var nClosed = nOpenElements - newStackLen; if (nClosed) { // ["p", "a", "b"] -> "</b></a></p>" // First, dump anything past the nesting limit. if (nOpenElements > goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_) { nClosed -= nOpenElements - goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_; nOpenElements = goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_; } // Truncate to the new limit, and produce end tags. var closeTags = openElementStack.splice(newStackLen, nClosed); if (closeTags.length) { closeTags.reverse(); prefix += '</' + closeTags.join('></') + '>'; } } // We could do resumption here to handle misnested tags like // <b><i class="c">Foo</b>Bar</i> // which is equivalent to // <b><i class="c">Foo</i></b><i class="c">Bar</i> // but that requires storing attributes on the open element stack // which complicates all the code using it for marginal added value. if (isCloseTag) { // If the close tag matched an open tag, then the closed section // included that tag name. htmlTokens[i] = prefix; } else { if (!isVoidTag) { openElementStack[openElementStack.length] = lowerCaseTagName; } if (openElementStack.length > goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_) { htmlToken = ''; } htmlTokens[i] = prefix + htmlToken; } } } if (openElementStack.length) { if (openElementStack.length > goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_) { openElementStack.length = goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_; } if (openElementStack.length) { openElementStack.reverse(); htmlTokens[htmlTokens.length] = '</' + openElementStack.join('></') + '>'; } } return htmlTokens; }; /** * Normalizes HTML tokens and concatenates them into a string. * @param {Array.<string>} htmlTokens an array of HTML tokens as returned by * {@link goog.labs.html.scrubber.lex}. * @return {string} a string of HTML. * @private */ goog.labs.html.scrubber.render_ = function(htmlTokens) { for (var i = 0, n = htmlTokens.length; i < n; ++i) { var htmlToken = htmlTokens[i]; if (htmlToken.charCodeAt(0) === goog.labs.html.scrubber.CC_LT_ && goog.labs.html.scrubber.TAG_RE_.test(htmlToken)) { // The well-formedness and quotedness of attributes must be ensured by // earlier passes. filter does this. } else { if (htmlToken.indexOf('<') >= 0) { htmlToken = htmlToken.replace(/</g, '<'); } if (htmlToken.indexOf('>') >= 0) { htmlToken = htmlToken.replace(/>/g, '>'); } htmlTokens[i] = htmlToken; } } return htmlTokens.join(''); }; /** * Groups of elements used to specify containment relationships. * @enum {number} * @private */ goog.labs.html.scrubber.Group_ = { BLOCK_: (1 << 0), INLINE_: (1 << 1), INLINE_MINUS_A_: (1 << 2), MIXED_: (1 << 3), TABLE_CONTENT_: (1 << 4), HEAD_CONTENT_: (1 << 5), TOP_CONTENT_: (1 << 6), AREA_ELEMENT_: (1 << 7), FORM_ELEMENT_: (1 << 8), LEGEND_ELEMENT_: (1 << 9), LI_ELEMENT_: (1 << 10), DL_PART_: (1 << 11), P_ELEMENT_: (1 << 12), OPTIONS_ELEMENT_: (1 << 13), OPTION_ELEMENT_: (1 << 14), PARAM_ELEMENT_: (1 << 15), TABLE_ELEMENT_: (1 << 16), TR_ELEMENT_: (1 << 17), TD_ELEMENT_: (1 << 18), COL_ELEMENT_: (1 << 19), CHARACTER_DATA_: (1 << 20) }; /** * Element scopes limit where close tags can have effects. * For example, a table cannot be implicitly closed by a {@code </p>} even if * the table appears inside a {@code <p>} because the {@code <table>} element * introduces a scope. * * @enum {number} * @private */ goog.labs.html.scrubber.Scope_ = { COMMON_: (1 << 0), BUTTON_: (1 << 1), LIST_ITEM_: (1 << 2), TABLE_: (1 << 3) }; /** @const @private */ goog.labs.html.scrubber.ALL_SCOPES_ = goog.labs.html.scrubber.Scope_.COMMON_ | goog.labs.html.scrubber.Scope_.BUTTON_ | goog.labs.html.scrubber.Scope_.LIST_ITEM_ | goog.labs.html.scrubber.Scope_.TABLE_; /** * Picks which open HTML elements to close. * * @param {string} lowerCaseTagName The name of the tag. * @param {boolean} isCloseTag True for a {@code </tagname>} tag. * @param {Array.<string>} openElementStack The names of elements that have been * opened and not subsequently closed. * @return {number} the length of openElementStack after closing any tags that * need to be closed. * @private */ goog.labs.html.scrubber.pickElementsToClose_ = function(lowerCaseTagName, isCloseTag, openElementStack) { var nOpenElements = openElementStack.length; if (isCloseTag) { // Look for a matching close tag inside blocking scopes. var topMost; if (/^h[1-6]$/.test(lowerCaseTagName)) { // </h1> will close any header. topMost = -1; for (var i = nOpenElements; --i >= 0;) { if (/^h[1-6]$/.test(openElementStack[i])) { topMost = i; } } } else { topMost = goog.array.lastIndexOf(openElementStack, lowerCaseTagName); } if (topMost >= 0) { var blockers = goog.labs.html.scrubber.ALL_SCOPES_ & ~(goog.labs.html.scrubber.ELEMENT_SCOPES_[lowerCaseTagName] | 0); for (var i = nOpenElements; --i > topMost;) { var blocks = goog.labs.html.scrubber.ELEMENT_SCOPES_[openElementStack[i]] | 0; if (blockers & blocks) { return nOpenElements; } } return topMost; } return nOpenElements; } else { // Close anything that cannot contain the tag name. var groups = goog.labs.html.scrubber.ELEMENT_GROUPS_[lowerCaseTagName]; if (groups === undefined) { groups = goog.labs.html.scrubber.Group_.INLINE_; } for (var i = nOpenElements; --i >= 0;) { var canContain = goog.labs.html.scrubber.ELEMENT_CONTENTS_[openElementStack[i]]; if (canContain === undefined) { canContain = goog.labs.html.scrubber.Group_.INLINE_; } if (groups & canContain) { return i + 1; } } return 0; } }; /** * The groups into which the element falls. * The default is an inline element. * @private */ goog.labs.html.scrubber.ELEMENT_GROUPS_ = { 'a': goog.labs.html.scrubber.Group_.INLINE_, 'abbr': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'acronym': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'address': goog.labs.html.scrubber.Group_.BLOCK_, 'applet': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'area': goog.labs.html.scrubber.Group_.AREA_ELEMENT_, 'audio': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'b': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'base': goog.labs.html.scrubber.Group_.HEAD_CONTENT_, 'basefont': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'bdi': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'bdo': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'big': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'blink': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'blockquote': goog.labs.html.scrubber.Group_.BLOCK_, 'body': goog.labs.html.scrubber.Group_.TOP_CONTENT_, 'br': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'button': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'canvas': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'caption': goog.labs.html.scrubber.Group_.TABLE_CONTENT_, 'center': goog.labs.html.scrubber.Group_.BLOCK_, 'cite': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'code': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'col': goog.labs.html.scrubber.Group_.TABLE_CONTENT_ | goog.labs.html.scrubber.Group_.COL_ELEMENT_, 'colgroup': goog.labs.html.scrubber.Group_.TABLE_CONTENT_, 'dd': goog.labs.html.scrubber.Group_.DL_PART_, 'del': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.MIXED_, 'dfn': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'dir': goog.labs.html.scrubber.Group_.BLOCK_, 'div': goog.labs.html.scrubber.Group_.BLOCK_, 'dl': goog.labs.html.scrubber.Group_.BLOCK_, 'dt': goog.labs.html.scrubber.Group_.DL_PART_, 'em': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'fieldset': goog.labs.html.scrubber.Group_.BLOCK_, 'font': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'form': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.FORM_ELEMENT_, 'h1': goog.labs.html.scrubber.Group_.BLOCK_, 'h2': goog.labs.html.scrubber.Group_.BLOCK_, 'h3': goog.labs.html.scrubber.Group_.BLOCK_, 'h4': goog.labs.html.scrubber.Group_.BLOCK_, 'h5': goog.labs.html.scrubber.Group_.BLOCK_, 'h6': goog.labs.html.scrubber.Group_.BLOCK_, 'head': goog.labs.html.scrubber.Group_.TOP_CONTENT_, 'hr': goog.labs.html.scrubber.Group_.BLOCK_, 'html': 0, 'i': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'iframe': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'img': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'input': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'ins': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'isindex': goog.labs.html.scrubber.Group_.INLINE_, 'kbd': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'label': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'legend': goog.labs.html.scrubber.Group_.LEGEND_ELEMENT_, 'li': goog.labs.html.scrubber.Group_.LI_ELEMENT_, 'link': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.HEAD_CONTENT_, 'listing': goog.labs.html.scrubber.Group_.BLOCK_, 'map': goog.labs.html.scrubber.Group_.INLINE_, 'meta': goog.labs.html.scrubber.Group_.HEAD_CONTENT_, 'nobr': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'noframes': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.TOP_CONTENT_, 'noscript': goog.labs.html.scrubber.Group_.BLOCK_, 'object': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_ | goog.labs.html.scrubber.Group_.HEAD_CONTENT_, 'ol': goog.labs.html.scrubber.Group_.BLOCK_, 'optgroup': goog.labs.html.scrubber.Group_.OPTIONS_ELEMENT_, 'option': goog.labs.html.scrubber.Group_.OPTIONS_ELEMENT_ | goog.labs.html.scrubber.Group_.OPTION_ELEMENT_, 'p': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.P_ELEMENT_, 'param': goog.labs.html.scrubber.Group_.PARAM_ELEMENT_, 'pre': goog.labs.html.scrubber.Group_.BLOCK_, 'q': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 's': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'samp': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'script': (goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_ | goog.labs.html.scrubber.Group_.MIXED_ | goog.labs.html.scrubber.Group_.TABLE_CONTENT_ | goog.labs.html.scrubber.Group_.HEAD_CONTENT_ | goog.labs.html.scrubber.Group_.TOP_CONTENT_ | goog.labs.html.scrubber.Group_.AREA_ELEMENT_ | goog.labs.html.scrubber.Group_.FORM_ELEMENT_ | goog.labs.html.scrubber.Group_.LEGEND_ELEMENT_ | goog.labs.html.scrubber.Group_.LI_ELEMENT_ | goog.labs.html.scrubber.Group_.DL_PART_ | goog.labs.html.scrubber.Group_.P_ELEMENT_ | goog.labs.html.scrubber.Group_.OPTIONS_ELEMENT_ | goog.labs.html.scrubber.Group_.OPTION_ELEMENT_ | goog.labs.html.scrubber.Group_.PARAM_ELEMENT_ | goog.labs.html.scrubber.Group_.TABLE_ELEMENT_ | goog.labs.html.scrubber.Group_.TR_ELEMENT_ | goog.labs.html.scrubber.Group_.TD_ELEMENT_ | goog.labs.html.scrubber.Group_.COL_ELEMENT_), 'select': goog.labs.html.scrubber.Group_.INLINE_, 'small': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'span': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'strike': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'strong': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'style': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.HEAD_CONTENT_, 'sub': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'sup': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'table': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.TABLE_ELEMENT_, 'tbody': goog.labs.html.scrubber.Group_.TABLE_CONTENT_, 'td': goog.labs.html.scrubber.Group_.TD_ELEMENT_, 'textarea': goog.labs.html.scrubber.Group_.INLINE_, 'tfoot': goog.labs.html.scrubber.Group_.TABLE_CONTENT_, 'th': goog.labs.html.scrubber.Group_.TD_ELEMENT_, 'thead': goog.labs.html.scrubber.Group_.TABLE_CONTENT_, 'title': goog.labs.html.scrubber.Group_.HEAD_CONTENT_, 'tr': goog.labs.html.scrubber.Group_.TABLE_CONTENT_ | goog.labs.html.scrubber.Group_.TR_ELEMENT_, 'tt': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'u': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'ul': goog.labs.html.scrubber.Group_.BLOCK_, 'var': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'video': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'wbr': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'xmp': goog.labs.html.scrubber.Group_.BLOCK_ }; /** * The groups which the element can contain. * Defaults to 0. * @private */ goog.labs.html.scrubber.ELEMENT_CONTENTS_ = { 'a': goog.labs.html.scrubber.Group_.INLINE_MINUS_A_, 'abbr': goog.labs.html.scrubber.Group_.INLINE_, 'acronym': goog.labs.html.scrubber.Group_.INLINE_, 'address': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.P_ELEMENT_, 'applet': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.PARAM_ELEMENT_, 'b': goog.labs.html.scrubber.Group_.INLINE_, 'bdi': goog.labs.html.scrubber.Group_.INLINE_, 'bdo': goog.labs.html.scrubber.Group_.INLINE_, 'big': goog.labs.html.scrubber.Group_.INLINE_, 'blink': goog.labs.html.scrubber.Group_.INLINE_, 'blockquote': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'body': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'button': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'canvas': goog.labs.html.scrubber.Group_.INLINE_, 'caption': goog.labs.html.scrubber.Group_.INLINE_, 'center': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'cite': goog.labs.html.scrubber.Group_.INLINE_, 'code': goog.labs.html.scrubber.Group_.INLINE_, 'colgroup': goog.labs.html.scrubber.Group_.COL_ELEMENT_, 'dd': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'del': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'dfn': goog.labs.html.scrubber.Group_.INLINE_, 'dir': goog.labs.html.scrubber.Group_.LI_ELEMENT_, 'div': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'dl': goog.labs.html.scrubber.Group_.DL_PART_, 'dt': goog.labs.html.scrubber.Group_.INLINE_, 'em': goog.labs.html.scrubber.Group_.INLINE_, 'fieldset': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.LEGEND_ELEMENT_, 'font': goog.labs.html.scrubber.Group_.INLINE_, 'form': (goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.INLINE_MINUS_A_ | goog.labs.html.scrubber.Group_.TR_ELEMENT_ | goog.labs.html.scrubber.Group_.TD_ELEMENT_), 'h1': goog.labs.html.scrubber.Group_.INLINE_, 'h2': goog.labs.html.scrubber.Group_.INLINE_, 'h3': goog.labs.html.scrubber.Group_.INLINE_, 'h4': goog.labs.html.scrubber.Group_.INLINE_, 'h5': goog.labs.html.scrubber.Group_.INLINE_, 'h6': goog.labs.html.scrubber.Group_.INLINE_, 'head': goog.labs.html.scrubber.Group_.HEAD_CONTENT_, 'html': goog.labs.html.scrubber.Group_.TOP_CONTENT_, 'i': goog.labs.html.scrubber.Group_.INLINE_, 'iframe': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'ins': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'kbd': goog.labs.html.scrubber.Group_.INLINE_, 'label': goog.labs.html.scrubber.Group_.INLINE_, 'legend': goog.labs.html.scrubber.Group_.INLINE_, 'li': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'listing': goog.labs.html.scrubber.Group_.INLINE_, 'map': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.AREA_ELEMENT_, 'nobr': goog.labs.html.scrubber.Group_.INLINE_, 'noframes': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.TOP_CONTENT_, 'noscript': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'object': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.PARAM_ELEMENT_, 'ol': goog.labs.html.scrubber.Group_.LI_ELEMENT_, 'optgroup': goog.labs.html.scrubber.Group_.OPTIONS_ELEMENT_, 'option': goog.labs.html.scrubber.Group_.CHARACTER_DATA_, 'p': goog.labs.html.scrubber.Group_.INLINE_ | goog.labs.html.scrubber.Group_.TABLE_ELEMENT_, 'pre': goog.labs.html.scrubber.Group_.INLINE_, 'q': goog.labs.html.scrubber.Group_.INLINE_, 's': goog.labs.html.scrubber.Group_.INLINE_, 'samp': goog.labs.html.scrubber.Group_.INLINE_, 'script': goog.labs.html.scrubber.Group_.CHARACTER_DATA_, 'select': goog.labs.html.scrubber.Group_.OPTIONS_ELEMENT_, 'small': goog.labs.html.scrubber.Group_.INLINE_, 'span': goog.labs.html.scrubber.Group_.INLINE_, 'strike': goog.labs.html.scrubber.Group_.INLINE_, 'strong': goog.labs.html.scrubber.Group_.INLINE_, 'style': goog.labs.html.scrubber.Group_.CHARACTER_DATA_, 'sub': goog.labs.html.scrubber.Group_.INLINE_, 'sup': goog.labs.html.scrubber.Group_.INLINE_, 'table': goog.labs.html.scrubber.Group_.TABLE_CONTENT_ | goog.labs.html.scrubber.Group_.FORM_ELEMENT_, 'tbody': goog.labs.html.scrubber.Group_.TR_ELEMENT_, 'td': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'textarea': goog.labs.html.scrubber.Group_.CHARACTER_DATA_, 'tfoot': goog.labs.html.scrubber.Group_.FORM_ELEMENT_ | goog.labs.html.scrubber.Group_.TR_ELEMENT_ | goog.labs.html.scrubber.Group_.TD_ELEMENT_, 'th': goog.labs.html.scrubber.Group_.BLOCK_ | goog.labs.html.scrubber.Group_.INLINE_, 'thead': goog.labs.html.scrubber.Group_.FORM_ELEMENT_ | goog.labs.html.scrubber.Group_.TR_ELEMENT_ | goog.labs.html.scrubber.Group_.TD_ELEMENT_, 'title': goog.labs.html.scrubber.Group_.CHARACTER_DATA_, 'tr': goog.labs.html.scrubber.Group_.FORM_ELEMENT_ | goog.labs.html.scrubber.Group_.TD_ELEMENT_, 'tt': goog.labs.html.scrubber.Group_.INLINE_, 'u': goog.labs.html.scrubber.Group_.INLINE_, 'ul': goog.labs.html.scrubber.Group_.LI_ELEMENT_, 'var': goog.labs.html.scrubber.Group_.INLINE_, 'xmp': goog.labs.html.scrubber.Group_.INLINE_ }; /** * The scopes in which an element falls. * No property defaults to 0. * @private */ goog.labs.html.scrubber.ELEMENT_SCOPES_ = { 'applet': goog.labs.html.scrubber.Scope_.COMMON_ | goog.labs.html.scrubber.Scope_.BUTTON_ | goog.labs.html.scrubber.Scope_.LIST_ITEM_, 'button': goog.labs.html.scrubber.Scope_.BUTTON_, 'caption': goog.labs.html.scrubber.Scope_.COMMON_ | goog.labs.html.scrubber.Scope_.BUTTON_ | goog.labs.html.scrubber.Scope_.LIST_ITEM_, 'html': goog.labs.html.scrubber.Scope_.COMMON_ | goog.labs.html.scrubber.Scope_.BUTTON_ | goog.labs.html.scrubber.Scope_.LIST_ITEM_ | goog.labs.html.scrubber.Scope_.TABLE_, 'object': goog.labs.html.scrubber.Scope_.COMMON_ | goog.labs.html.scrubber.Scope_.BUTTON_ | goog.labs.html.scrubber.Scope_.LIST_ITEM_, 'ol': goog.labs.html.scrubber.Scope_.LIST_ITEM_, 'table': goog.labs.html.scrubber.Scope_.COMMON_ | goog.labs.html.scrubber.Scope_.BUTTON_ | goog.labs.html.scrubber.Scope_.LIST_ITEM_ | goog.labs.html.scrubber.Scope_.TABLE_, 'td': goog.labs.html.scrubber.Scope_.COMMON_ | goog.labs.html.scrubber.Scope_.BUTTON_ | goog.labs.html.scrubber.Scope_.LIST_ITEM_, 'th': goog.labs.html.scrubber.Scope_.COMMON_ | goog.labs.html.scrubber.Scope_.BUTTON_ | goog.labs.html.scrubber.Scope_.LIST_ITEM_, 'ul': goog.labs.html.scrubber.Scope_.LIST_ITEM_ }; /** * Per-element, a child that can contain block content. * @private */ goog.labs.html.scrubber.BLOCK_CONTAINERS_ = { 'dl': 'dd', 'ol': 'li', 'table': 'tr', 'tr': 'td', 'ul': 'li' }; goog.labs.html.attributeRewriterPresubmitWorkaround();