Path: blob/trunk/third_party/closure/goog/labs/html/scrubber.js
2868 views
// Copyright 2013 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.131415/**16* @fileoverview17* HTML tag filtering, and balancing.18* A more user-friendly API is exposed via {@code goog.labs.html.sanitizer}.19* @visibility {//visibility:private}20*/212223goog.provide('goog.labs.html.scrubber');2425goog.require('goog.array');26goog.require('goog.dom.tags');27goog.require('goog.labs.html.attributeRewriterPresubmitWorkaround');28goog.require('goog.string');293031/**32* Replaces tags not on the white-list with empty text nodes, dropping all33* attributes, and drops other non-text nodes such as comments.34*35* @param {!Object<string, boolean>} tagWhitelist a set of lower-case tag names36* following the convention established by {@link goog.object.createSet}.37* @param {!Object<string, Object<string, goog.labs.html.AttributeRewriter>>}38* attrWhitelist39* maps lower-case tag names and the special string {@code "*"} to functions40* from decoded attribute values to sanitized values or {@code null} to41* indicate that the attribute is not allowed with that value.42*43* For example, if {@code attrWhitelist['a']['href']} is defined then it44* is used to sanitize the value of the link's URL.45*46* If {@code attrWhitelist['*']['id']} is defined, and47* {@code attrWhitelist['div']['id']} is not, then the former is used to48* sanitize any {@code id} attribute on a {@code <div>} element.49* @param {string} html a string of HTML50* @return {string} the input but with potentially dangerous tokens removed.51*/52goog.labs.html.scrubber.scrub = function(tagWhitelist, attrWhitelist, html) {53return goog.labs.html.scrubber.render_(54goog.labs.html.scrubber.balance_(55goog.labs.html.scrubber.filter_(56tagWhitelist, attrWhitelist,57goog.labs.html.scrubber.lex_(html))));58};596061/**62* Balances tags in trusted HTML.63* @param {string} html a string of HTML64* @return {string} the input but with an end-tag for each non-void start tag65* and only for non-void start tags, and with start and end tags nesting66* properly.67*/68goog.labs.html.scrubber.balance = function(html) {69return goog.labs.html.scrubber.render_(70goog.labs.html.scrubber.balance_(goog.labs.html.scrubber.lex_(html)));71};727374/** Character code constant for {@code '<'}. @private */75goog.labs.html.scrubber.CC_LT_ = '<'.charCodeAt(0);767778/** Character code constant for {@code '!'}. @private */79goog.labs.html.scrubber.CC_BANG_ = '!'.charCodeAt(0);808182/** Character code constant for {@code '/'}. @private */83goog.labs.html.scrubber.CC_SLASH_ = '/'.charCodeAt(0);848586/** Character code constant for {@code '?'}. @private */87goog.labs.html.scrubber.CC_QMARK_ = '?'.charCodeAt(0);888990/**91* Matches content following a tag name or attribute value, and before the92* beginning of the next attribute value.93* @private94*/95goog.labs.html.scrubber.ATTR_VALUE_PRECEDER_ = '[^=>]+';969798/** @private */99goog.labs.html.scrubber.UNQUOTED_ATTR_VALUE_ = '(?:[^"\'\\s>][^\\s>]*)';100101102/** @private */103goog.labs.html.scrubber.DOUBLE_QUOTED_ATTR_VALUE_ = '(?:"[^"]*"?)';104105106/** @private */107goog.labs.html.scrubber.SINGLE_QUOTED_ATTR_VALUE_ = "(?:'[^']*'?)";108109110/**111* Matches the equals-sign and any attribute value following it, but does not112* capture any {@code >} that would close the tag.113* @private114*/115goog.labs.html.scrubber.ATTR_VALUE_ = '=\\s*(?:' +116goog.labs.html.scrubber.UNQUOTED_ATTR_VALUE_ + '|' +117goog.labs.html.scrubber.DOUBLE_QUOTED_ATTR_VALUE_ + '|' +118goog.labs.html.scrubber.SINGLE_QUOTED_ATTR_VALUE_ + ')?';119120121/**122* The body of a tag between the end of the name and the closing {@code >}123* if any.124* @private125*/126goog.labs.html.scrubber.ATTRS_ = '(?:' +127goog.labs.html.scrubber.ATTR_VALUE_PRECEDER_ + '|' +128goog.labs.html.scrubber.ATTR_VALUE_ + ')*';129130131/**132* A character that continues a tag name as defined at133* http://www.w3.org/html/wg/drafts/html/master/syntax.html#tag-name-state134* @private135*/136goog.labs.html.scrubber.TAG_NAME_CHAR_ = '[^\t\f\n />]';137138139/**140* Matches when the next character cannot continue a tag name.141* @private142*/143goog.labs.html.scrubber.BREAK_ =144'(?!' + goog.labs.html.scrubber.TAG_NAME_CHAR_ + ')';145146147/**148* Matches the open tag and body of a special element :149* one whose body cannot contain nested elements so uses special parsing rules.150* It does not include the end tag.151* @private152*/153goog.labs.html.scrubber.SPECIAL_ELEMENT_ = '<(?:' +154// Special tag name.155'(iframe|script|style|textarea|title|xmp)' +156// End of tag name157goog.labs.html.scrubber.BREAK_ +158// Attributes159goog.labs.html.scrubber.ATTRS_ + '>' +160// Element content includes non '<' characters, and161// '<' that don't start a matching end tag.162// This uses a back-reference to the tag name to determine whether163// the tag names match.164// Since matching is case-insensitive, this can only be used in165// a case-insensitive regular expression.166// JavaScript does not treat Turkish dotted I's as equivalent to their167// ASCII equivalents.168'(?:[^<]|<(?!/\\1' + goog.labs.html.scrubber.BREAK_ + '))*' +169')';170171172/**173* Regexp pattern for an HTML tag.174* @private175*/176goog.labs.html.scrubber.TAG_ = '<[/]?[a-z]' +177goog.labs.html.scrubber.TAG_NAME_CHAR_ + '*' +178goog.labs.html.scrubber.ATTRS_ + '>?';179180181/**182* Regexp pattern for an HTML text node.183* @private184*/185goog.labs.html.scrubber.TEXT_NODE_ = '(?:[^<]|<(?![a-z]|[?!/]))+';186187188/**189* Matches HTML comments including HTML 5 "bogus comments" of the form190* {@code <!...>} or {@code <?...>} or {@code </...>}.191* @private192*/193goog.labs.html.scrubber.COMMENT_ =194'<!--(?:[^\\-]|-+(?![\\->]))*(?:-(?:->?)?)?' +195'|<[!?/][^>]*>?';196197198/**199* Regexp pattern for an HTML token after a doctype.200* Special elements introduces a capturing group for use with a201* back-reference.202* @private203*/204goog.labs.html.scrubber.HTML_TOKENS_RE_ = new RegExp(205'(?:' + goog.labs.html.scrubber.TEXT_NODE_ + '|' +206goog.labs.html.scrubber.SPECIAL_ELEMENT_ + '|' +207goog.labs.html.scrubber.TAG_ + '|' + goog.labs.html.scrubber.COMMENT_ +208')',209'ig');210211212/**213* An HTML tag which captures the name in group 1,214* and any attributes in group 2.215* @private216*/217goog.labs.html.scrubber.TAG_RE_ = new RegExp(218'<[/]?([a-z]' + goog.labs.html.scrubber.TAG_NAME_CHAR_ + '*)' +219'(' + goog.labs.html.scrubber.ATTRS_ + ')>?',220'i');221222223/**224* A global matcher that separates attributes out of the tag body cruft.225* @private226*/227goog.labs.html.scrubber.ATTRS_RE_ = new RegExp(228'[^=\\s]+\\s*(?:' + goog.labs.html.scrubber.ATTR_VALUE_ + ')?', 'ig');229230231/**232* Returns an array of HTML tokens including tags, text nodes and comments.233* "Special" elements, like {@code <script>..</script>} whose bodies cannot234* include nested elements, are returned as single tokens.235*236* @param {string} html a string of HTML237* @return {!Array<string>}238* @private239*/240goog.labs.html.scrubber.lex_ = function(html) {241return ('' + html).match(goog.labs.html.scrubber.HTML_TOKENS_RE_) || [];242};243244245/**246* Replaces tags not on the white-list with empty text nodes, dropping all247* attributes, and drops other non-text nodes such as comments.248*249* @param {!Object<string, boolean>} tagWhitelist a set of lower-case tag names250* following the convention established by {@link goog.object.createSet}.251* @param {!Object<string, Object<string, goog.labs.html.AttributeRewriter>>252* } attrWhitelist253* maps lower-case tag names and the special string {@code "*"} to functions254* from decoded attribute values to sanitized values or {@code null} to255* indicate that the attribute is not allowed with that value.256*257* For example, if {@code attrWhitelist['a']['href']} is defined then it is258* used to sanitize the value of the link's URL.259*260* If {@code attrWhitelist['*']['id']} is defined, and261* {@code attrWhitelist['div']['id']} is not, then the former is used to262* sanitize any {@code id} attribute on a {@code <div>} element.263* @param {!Array<string>} htmlTokens an array of HTML tokens as returned by264* {@link goog.labs.html.scrubber.lex_}.265* @return {!Array<string>} the input array modified in place to have some266* tokens removed.267* @private268*/269goog.labs.html.scrubber.filter_ = function(270tagWhitelist, attrWhitelist, htmlTokens) {271var genericAttrWhitelist = attrWhitelist['*'];272for (var i = 0, n = htmlTokens.length; i < n; ++i) {273var htmlToken = htmlTokens[i];274if (htmlToken.charCodeAt(0) !== goog.labs.html.scrubber.CC_LT_) {275// Definitely not a tag276continue;277}278279var tag = htmlToken.match(goog.labs.html.scrubber.TAG_RE_);280if (tag) {281var lowerCaseTagName = tag[1].toLowerCase();282var isCloseTag =283htmlToken.charCodeAt(1) === goog.labs.html.scrubber.CC_SLASH_;284var attrs = '';285if (!isCloseTag && tag[2]) {286var tagSpecificAttrWhitelist =287/** @type {Object<string, goog.labs.html.AttributeRewriter>} */ (288goog.labs.html.scrubber.readOwnProperty_(289attrWhitelist, lowerCaseTagName));290if (genericAttrWhitelist || tagSpecificAttrWhitelist) {291attrs = goog.labs.html.scrubber.filterAttrs_(292tag[2], genericAttrWhitelist, tagSpecificAttrWhitelist);293}294}295var specialContent = htmlToken.substring(tag[0].length);296htmlTokens[i] = (tagWhitelist[lowerCaseTagName] === true) ?297((isCloseTag ? '</' : '<') + lowerCaseTagName + attrs + '>' +298specialContent) :299'';300} else if (htmlToken.length > 1) {301switch (htmlToken.charCodeAt(1)) {302case goog.labs.html.scrubber.CC_BANG_:303case goog.labs.html.scrubber.CC_SLASH_:304case goog.labs.html.scrubber.CC_QMARK_:305htmlTokens[i] = ''; // Elide comments.306break;307default:308// Otherwise, token is just a text node that starts with '<'.309// Speed up later passes by normalizing the text node.310htmlTokens[i] = htmlTokens[i].replace(/</g, '<');311}312}313}314return htmlTokens;315};316317318/**319* Parses attribute names and values out of a tag body and applies the attribute320* white-list to produce a tag body containing only safe attributes.321*322* @param {string} attrsText the text of a tag between the end of the tag name323* and the beginning of the tag end marker, so {@code " foo bar='baz'"} for324* the tag {@code <tag foo bar='baz'/>}.325* @param {Object<string, goog.labs.html.AttributeRewriter>}326* genericAttrWhitelist327* a whitelist of attribute transformations for attributes that are allowed328* on any element.329* @param {Object<string, goog.labs.html.AttributeRewriter>}330* tagSpecificAttrWhitelist331* a whitelist of attribute transformations for attributes that are allowed332* on the element started by the tag whose body is {@code tagBody}.333* @return {string} a tag-body that consists only of safe attributes.334* @private335*/336goog.labs.html.scrubber.filterAttrs_ = function(337attrsText, genericAttrWhitelist, tagSpecificAttrWhitelist) {338var attrs = attrsText.match(goog.labs.html.scrubber.ATTRS_RE_);339var nAttrs = attrs ? attrs.length : 0;340var safeAttrs = '';341for (var i = 0; i < nAttrs; ++i) {342var attr = attrs[i];343var eq = attr.indexOf('=');344var name, value;345if (eq >= 0) {346name = goog.string.trim(attr.substr(0, eq));347value =348goog.string.stripQuotes(goog.string.trim(attr.substr(eq + 1)), '"\'');349} else {350name = value = attr;351}352name = name.toLowerCase();353var rewriter = /** @type {?goog.labs.html.AttributeRewriter} */ (354tagSpecificAttrWhitelist &&355goog.labs.html.scrubber.readOwnProperty_(356tagSpecificAttrWhitelist, name) ||357genericAttrWhitelist &&358goog.labs.html.scrubber.readOwnProperty_(359genericAttrWhitelist, name));360if (rewriter) {361var safeValue = rewriter(goog.string.unescapeEntities(value));362if (safeValue != null) {363if (safeValue.implementsGoogStringTypedString) {364safeValue = /** @type {goog.string.TypedString} */365(safeValue).getTypedStringValue();366}367safeValue = String(safeValue);368if (safeValue.indexOf('`') >= 0) {369safeValue += ' ';370}371safeAttrs +=372' ' + name + '="' + goog.string.htmlEscape(safeValue, false) + '"';373}374}375}376return safeAttrs;377};378379380/**381* @param {!Object} o the object382* @param {!string} k a key into o383* @return {*}384* @private385*/386goog.labs.html.scrubber.readOwnProperty_ = function(o, k) {387return Object.prototype.hasOwnProperty.call(o, k) ? o[k] : undefined;388};389390391/**392* We limit the nesting limit of balanced HTML to a large but manageable number393* so that built-in browser limits aren't likely to kick in and undo all our394* matching of start and end tags.395* <br>396* This mitigates the HTML parsing equivalent of stack smashing attacks.397* <br>398* Otherwise, crafted inputs like399* {@code <p><p><p><p>...Ad nauseam..</p></p></p></p>} could exploit400* browser bugs, and/or undocumented nesting limit recovery code to misnest401* tags.402* @private403* @const404*/405goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_ = 256;406407408/**409* Ensures that there are end-tags for all and only for non-void start tags.410* @param {Array<string>} htmlTokens an array of HTML tokens as returned by411* {@link goog.labs.html.scrubber.lex}.412* @return {!Array<string>} the input array modified in place to have some413* tokens removed.414* @private415*/416goog.labs.html.scrubber.balance_ = function(htmlTokens) {417var openElementStack = [];418for (var i = 0, n = htmlTokens.length; i < n; ++i) {419var htmlToken = htmlTokens[i];420if (htmlToken.charCodeAt(0) !== goog.labs.html.scrubber.CC_LT_) {421// Definitely not a tag422continue;423}424var tag = htmlToken.match(goog.labs.html.scrubber.TAG_RE_);425if (tag) {426var lowerCaseTagName = tag[1].toLowerCase();427var isCloseTag =428htmlToken.charCodeAt(1) === goog.labs.html.scrubber.CC_SLASH_;429// Special case: HTML5 mandates that </br> be treated as <br>.430if (isCloseTag && lowerCaseTagName == 'br') {431isCloseTag = false;432htmlToken = '<br>';433}434var isVoidTag = goog.dom.tags.isVoidTag(lowerCaseTagName);435if (isVoidTag && isCloseTag) {436htmlTokens[i] = '';437continue;438}439440var prefix = '';441442// Insert implied open tags.443var nOpenElements = openElementStack.length;444if (nOpenElements && !isCloseTag) {445var top = openElementStack[nOpenElements - 1];446var groups = goog.labs.html.scrubber.ELEMENT_GROUPS_[lowerCaseTagName];447if (groups === undefined) {448groups = goog.labs.html.scrubber.Group_.INLINE_;449}450var canContain = goog.labs.html.scrubber.ELEMENT_CONTENTS_[top];451if (!(groups & canContain)) {452var blockContainer = goog.labs.html.scrubber.BLOCK_CONTAINERS_[top];453if ('string' === typeof blockContainer) {454var containerCanContain =455goog.labs.html.scrubber.ELEMENT_CONTENTS_[blockContainer];456if (containerCanContain & groups) {457if (nOpenElements <458goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_) {459prefix = '<' + blockContainer + '>';460}461openElementStack[nOpenElements] = blockContainer;462++nOpenElements;463}464}465}466}467468// Insert any missing close tags we need.469var newStackLen = goog.labs.html.scrubber.pickElementsToClose_(470lowerCaseTagName, isCloseTag, openElementStack);471472var nClosed = nOpenElements - newStackLen;473if (nClosed) { // ["p", "a", "b"] -> "</b></a></p>"474// First, dump anything past the nesting limit.475if (nOpenElements > goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_) {476nClosed -=477nOpenElements - goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_;478nOpenElements = goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_;479}480// Truncate to the new limit, and produce end tags.481var closeTags = openElementStack.splice(newStackLen, nClosed);482if (closeTags.length) {483closeTags.reverse();484prefix += '</' + closeTags.join('></') + '>';485}486}487488// We could do resumption here to handle misnested tags like489// <b><i class="c">Foo</b>Bar</i>490// which is equivalent to491// <b><i class="c">Foo</i></b><i class="c">Bar</i>492// but that requires storing attributes on the open element stack493// which complicates all the code using it for marginal added value.494495if (isCloseTag) {496// If the close tag matched an open tag, then the closed section497// included that tag name.498htmlTokens[i] = prefix;499} else {500if (!isVoidTag) {501openElementStack[openElementStack.length] = lowerCaseTagName;502}503if (openElementStack.length >504goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_) {505htmlToken = '';506}507htmlTokens[i] = prefix + htmlToken;508}509}510}511if (openElementStack.length) {512if (openElementStack.length >513goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_) {514openElementStack.length = goog.labs.html.scrubber.BALANCE_NESTING_LIMIT_;515}516if (openElementStack.length) {517openElementStack.reverse();518htmlTokens[htmlTokens.length] = '</' + openElementStack.join('></') + '>';519}520}521return htmlTokens;522};523524525/**526* Normalizes HTML tokens and concatenates them into a string.527* @param {Array<string>} htmlTokens an array of HTML tokens as returned by528* {@link goog.labs.html.scrubber.lex}.529* @return {string} a string of HTML.530* @private531*/532goog.labs.html.scrubber.render_ = function(htmlTokens) {533for (var i = 0, n = htmlTokens.length; i < n; ++i) {534var htmlToken = htmlTokens[i];535if (htmlToken.charCodeAt(0) === goog.labs.html.scrubber.CC_LT_ &&536goog.labs.html.scrubber.TAG_RE_.test(htmlToken)) {537// The well-formedness and quotedness of attributes must be ensured by538// earlier passes. filter does this.539} else {540if (htmlToken.indexOf('<') >= 0) {541htmlToken = htmlToken.replace(/</g, '<');542}543if (htmlToken.indexOf('>') >= 0) {544htmlToken = htmlToken.replace(/>/g, '>');545}546htmlTokens[i] = htmlToken;547}548}549return htmlTokens.join('');550};551552553/**554* Groups of elements used to specify containment relationships.555* @enum {number}556* @private557*/558goog.labs.html.scrubber.Group_ = {559BLOCK_: (1 << 0),560INLINE_: (1 << 1),561INLINE_MINUS_A_: (1 << 2),562MIXED_: (1 << 3),563TABLE_CONTENT_: (1 << 4),564HEAD_CONTENT_: (1 << 5),565TOP_CONTENT_: (1 << 6),566AREA_ELEMENT_: (1 << 7),567FORM_ELEMENT_: (1 << 8),568LEGEND_ELEMENT_: (1 << 9),569LI_ELEMENT_: (1 << 10),570DL_PART_: (1 << 11),571P_ELEMENT_: (1 << 12),572OPTIONS_ELEMENT_: (1 << 13),573OPTION_ELEMENT_: (1 << 14),574PARAM_ELEMENT_: (1 << 15),575TABLE_ELEMENT_: (1 << 16),576TR_ELEMENT_: (1 << 17),577TD_ELEMENT_: (1 << 18),578COL_ELEMENT_: (1 << 19),579CHARACTER_DATA_: (1 << 20)580};581582583/**584* Element scopes limit where close tags can have effects.585* For example, a table cannot be implicitly closed by a {@code </p>} even if586* the table appears inside a {@code <p>} because the {@code <table>} element587* introduces a scope.588*589* @enum {number}590* @private591*/592goog.labs.html.scrubber.Scope_ = {593COMMON_: (1 << 0),594BUTTON_: (1 << 1),595LIST_ITEM_: (1 << 2),596TABLE_: (1 << 3)597};598599600/** @const @private */601goog.labs.html.scrubber.ALL_SCOPES_ = goog.labs.html.scrubber.Scope_.COMMON_ |602goog.labs.html.scrubber.Scope_.BUTTON_ |603goog.labs.html.scrubber.Scope_.LIST_ITEM_ |604goog.labs.html.scrubber.Scope_.TABLE_;605606607/**608* Picks which open HTML elements to close.609*610* @param {string} lowerCaseTagName The name of the tag.611* @param {boolean} isCloseTag True for a {@code </tagname>} tag.612* @param {Array<string>} openElementStack The names of elements that have been613* opened and not subsequently closed.614* @return {number} the length of openElementStack after closing any tags that615* need to be closed.616* @private617*/618goog.labs.html.scrubber.pickElementsToClose_ = function(619lowerCaseTagName, isCloseTag, openElementStack) {620var nOpenElements = openElementStack.length;621if (isCloseTag) {622// Look for a matching close tag inside blocking scopes.623var topMost;624if (/^h[1-6]$/.test(lowerCaseTagName)) {625// </h1> will close any header.626topMost = -1;627for (var i = nOpenElements; --i >= 0;) {628if (/^h[1-6]$/.test(openElementStack[i])) {629topMost = i;630}631}632} else {633topMost = goog.array.lastIndexOf(openElementStack, lowerCaseTagName);634}635if (topMost >= 0) {636var blockers = goog.labs.html.scrubber.ALL_SCOPES_ &637~(goog.labs.html.scrubber.ELEMENT_SCOPES_[lowerCaseTagName] | 0);638for (var i = nOpenElements; --i > topMost;) {639var blocks =640goog.labs.html.scrubber.ELEMENT_SCOPES_[openElementStack[i]] | 0;641if (blockers & blocks) {642return nOpenElements;643}644}645return topMost;646}647return nOpenElements;648} else {649// Close anything that cannot contain the tag name.650var groups = goog.labs.html.scrubber.ELEMENT_GROUPS_[lowerCaseTagName];651if (groups === undefined) {652groups = goog.labs.html.scrubber.Group_.INLINE_;653}654for (var i = nOpenElements; --i >= 0;) {655var canContain =656goog.labs.html.scrubber.ELEMENT_CONTENTS_[openElementStack[i]];657if (canContain === undefined) {658canContain = goog.labs.html.scrubber.Group_.INLINE_;659}660if (groups & canContain) {661return i + 1;662}663}664return 0;665}666};667668669/**670* The groups into which the element falls.671* The default is an inline element.672* @private673*/674goog.labs.html.scrubber.ELEMENT_GROUPS_ = {675'a': goog.labs.html.scrubber.Group_.INLINE_,676'abbr': goog.labs.html.scrubber.Group_.INLINE_ |677goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,678'acronym': goog.labs.html.scrubber.Group_.INLINE_ |679goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,680'address': goog.labs.html.scrubber.Group_.BLOCK_,681'applet': goog.labs.html.scrubber.Group_.INLINE_ |682goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,683'area': goog.labs.html.scrubber.Group_.AREA_ELEMENT_,684'audio': goog.labs.html.scrubber.Group_.INLINE_ |685goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,686'b': goog.labs.html.scrubber.Group_.INLINE_ |687goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,688'base': goog.labs.html.scrubber.Group_.HEAD_CONTENT_,689'basefont': goog.labs.html.scrubber.Group_.INLINE_ |690goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,691'bdi': goog.labs.html.scrubber.Group_.INLINE_ |692goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,693'bdo': goog.labs.html.scrubber.Group_.INLINE_ |694goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,695'big': goog.labs.html.scrubber.Group_.INLINE_ |696goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,697'blink': goog.labs.html.scrubber.Group_.INLINE_ |698goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,699'blockquote': goog.labs.html.scrubber.Group_.BLOCK_,700'body': goog.labs.html.scrubber.Group_.TOP_CONTENT_,701'br': goog.labs.html.scrubber.Group_.INLINE_ |702goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,703'button': goog.labs.html.scrubber.Group_.INLINE_ |704goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,705'canvas': goog.labs.html.scrubber.Group_.INLINE_ |706goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,707'caption': goog.labs.html.scrubber.Group_.TABLE_CONTENT_,708'center': goog.labs.html.scrubber.Group_.BLOCK_,709'cite': goog.labs.html.scrubber.Group_.INLINE_ |710goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,711'code': goog.labs.html.scrubber.Group_.INLINE_ |712goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,713'col': goog.labs.html.scrubber.Group_.TABLE_CONTENT_ |714goog.labs.html.scrubber.Group_.COL_ELEMENT_,715'colgroup': goog.labs.html.scrubber.Group_.TABLE_CONTENT_,716'dd': goog.labs.html.scrubber.Group_.DL_PART_,717'del': goog.labs.html.scrubber.Group_.BLOCK_ |718goog.labs.html.scrubber.Group_.INLINE_ |719goog.labs.html.scrubber.Group_.MIXED_,720'dfn': goog.labs.html.scrubber.Group_.INLINE_ |721goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,722'dir': goog.labs.html.scrubber.Group_.BLOCK_,723'div': goog.labs.html.scrubber.Group_.BLOCK_,724'dl': goog.labs.html.scrubber.Group_.BLOCK_,725'dt': goog.labs.html.scrubber.Group_.DL_PART_,726'em': goog.labs.html.scrubber.Group_.INLINE_ |727goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,728'fieldset': goog.labs.html.scrubber.Group_.BLOCK_,729'font': goog.labs.html.scrubber.Group_.INLINE_ |730goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,731'form': goog.labs.html.scrubber.Group_.BLOCK_ |732goog.labs.html.scrubber.Group_.FORM_ELEMENT_,733'h1': goog.labs.html.scrubber.Group_.BLOCK_,734'h2': goog.labs.html.scrubber.Group_.BLOCK_,735'h3': goog.labs.html.scrubber.Group_.BLOCK_,736'h4': goog.labs.html.scrubber.Group_.BLOCK_,737'h5': goog.labs.html.scrubber.Group_.BLOCK_,738'h6': goog.labs.html.scrubber.Group_.BLOCK_,739'head': goog.labs.html.scrubber.Group_.TOP_CONTENT_,740'hr': goog.labs.html.scrubber.Group_.BLOCK_,741'html': 0,742'i': goog.labs.html.scrubber.Group_.INLINE_ |743goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,744'iframe': goog.labs.html.scrubber.Group_.INLINE_ |745goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,746'img': goog.labs.html.scrubber.Group_.INLINE_ |747goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,748'input': goog.labs.html.scrubber.Group_.INLINE_ |749goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,750'ins': goog.labs.html.scrubber.Group_.BLOCK_ |751goog.labs.html.scrubber.Group_.INLINE_,752'isindex': goog.labs.html.scrubber.Group_.INLINE_,753'kbd': goog.labs.html.scrubber.Group_.INLINE_ |754goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,755'label': goog.labs.html.scrubber.Group_.INLINE_ |756goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,757'legend': goog.labs.html.scrubber.Group_.LEGEND_ELEMENT_,758'li': goog.labs.html.scrubber.Group_.LI_ELEMENT_,759'link': goog.labs.html.scrubber.Group_.INLINE_ |760goog.labs.html.scrubber.Group_.HEAD_CONTENT_,761'listing': goog.labs.html.scrubber.Group_.BLOCK_,762'map': goog.labs.html.scrubber.Group_.INLINE_,763'meta': goog.labs.html.scrubber.Group_.HEAD_CONTENT_,764'nobr': goog.labs.html.scrubber.Group_.INLINE_ |765goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,766'noframes': goog.labs.html.scrubber.Group_.BLOCK_ |767goog.labs.html.scrubber.Group_.TOP_CONTENT_,768'noscript': goog.labs.html.scrubber.Group_.BLOCK_,769'object': goog.labs.html.scrubber.Group_.INLINE_ |770goog.labs.html.scrubber.Group_.INLINE_MINUS_A_ |771goog.labs.html.scrubber.Group_.HEAD_CONTENT_,772'ol': goog.labs.html.scrubber.Group_.BLOCK_,773'optgroup': goog.labs.html.scrubber.Group_.OPTIONS_ELEMENT_,774'option': goog.labs.html.scrubber.Group_.OPTIONS_ELEMENT_ |775goog.labs.html.scrubber.Group_.OPTION_ELEMENT_,776'p': goog.labs.html.scrubber.Group_.BLOCK_ |777goog.labs.html.scrubber.Group_.P_ELEMENT_,778'param': goog.labs.html.scrubber.Group_.PARAM_ELEMENT_,779'pre': goog.labs.html.scrubber.Group_.BLOCK_,780'q': goog.labs.html.scrubber.Group_.INLINE_ |781goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,782's': goog.labs.html.scrubber.Group_.INLINE_ |783goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,784'samp': goog.labs.html.scrubber.Group_.INLINE_ |785goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,786'script':787(goog.labs.html.scrubber.Group_.BLOCK_ |788goog.labs.html.scrubber.Group_.INLINE_ |789goog.labs.html.scrubber.Group_.INLINE_MINUS_A_ |790goog.labs.html.scrubber.Group_.MIXED_ |791goog.labs.html.scrubber.Group_.TABLE_CONTENT_ |792goog.labs.html.scrubber.Group_.HEAD_CONTENT_ |793goog.labs.html.scrubber.Group_.TOP_CONTENT_ |794goog.labs.html.scrubber.Group_.AREA_ELEMENT_ |795goog.labs.html.scrubber.Group_.FORM_ELEMENT_ |796goog.labs.html.scrubber.Group_.LEGEND_ELEMENT_ |797goog.labs.html.scrubber.Group_.LI_ELEMENT_ |798goog.labs.html.scrubber.Group_.DL_PART_ |799goog.labs.html.scrubber.Group_.P_ELEMENT_ |800goog.labs.html.scrubber.Group_.OPTIONS_ELEMENT_ |801goog.labs.html.scrubber.Group_.OPTION_ELEMENT_ |802goog.labs.html.scrubber.Group_.PARAM_ELEMENT_ |803goog.labs.html.scrubber.Group_.TABLE_ELEMENT_ |804goog.labs.html.scrubber.Group_.TR_ELEMENT_ |805goog.labs.html.scrubber.Group_.TD_ELEMENT_ |806goog.labs.html.scrubber.Group_.COL_ELEMENT_),807'select': goog.labs.html.scrubber.Group_.INLINE_,808'small': goog.labs.html.scrubber.Group_.INLINE_ |809goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,810'span': goog.labs.html.scrubber.Group_.INLINE_ |811goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,812'strike': goog.labs.html.scrubber.Group_.INLINE_ |813goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,814'strong': goog.labs.html.scrubber.Group_.INLINE_ |815goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,816'style': goog.labs.html.scrubber.Group_.INLINE_ |817goog.labs.html.scrubber.Group_.HEAD_CONTENT_,818'sub': goog.labs.html.scrubber.Group_.INLINE_ |819goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,820'sup': goog.labs.html.scrubber.Group_.INLINE_ |821goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,822'table': goog.labs.html.scrubber.Group_.BLOCK_ |823goog.labs.html.scrubber.Group_.TABLE_ELEMENT_,824'tbody': goog.labs.html.scrubber.Group_.TABLE_CONTENT_,825'td': goog.labs.html.scrubber.Group_.TD_ELEMENT_,826'textarea': goog.labs.html.scrubber.Group_.INLINE_,827'tfoot': goog.labs.html.scrubber.Group_.TABLE_CONTENT_,828'th': goog.labs.html.scrubber.Group_.TD_ELEMENT_,829'thead': goog.labs.html.scrubber.Group_.TABLE_CONTENT_,830'title': goog.labs.html.scrubber.Group_.HEAD_CONTENT_,831'tr': goog.labs.html.scrubber.Group_.TABLE_CONTENT_ |832goog.labs.html.scrubber.Group_.TR_ELEMENT_,833'tt': goog.labs.html.scrubber.Group_.INLINE_ |834goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,835'u': goog.labs.html.scrubber.Group_.INLINE_ |836goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,837'ul': goog.labs.html.scrubber.Group_.BLOCK_,838'var': goog.labs.html.scrubber.Group_.INLINE_ |839goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,840'video': goog.labs.html.scrubber.Group_.INLINE_ |841goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,842'wbr': goog.labs.html.scrubber.Group_.INLINE_ |843goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,844'xmp': goog.labs.html.scrubber.Group_.BLOCK_845};846847848/**849* The groups which the element can contain.850* Defaults to 0.851* @private852*/853goog.labs.html.scrubber.ELEMENT_CONTENTS_ = {854'a': goog.labs.html.scrubber.Group_.INLINE_MINUS_A_,855'abbr': goog.labs.html.scrubber.Group_.INLINE_,856'acronym': goog.labs.html.scrubber.Group_.INLINE_,857'address': goog.labs.html.scrubber.Group_.INLINE_ |858goog.labs.html.scrubber.Group_.P_ELEMENT_,859'applet': goog.labs.html.scrubber.Group_.BLOCK_ |860goog.labs.html.scrubber.Group_.INLINE_ |861goog.labs.html.scrubber.Group_.PARAM_ELEMENT_,862'b': goog.labs.html.scrubber.Group_.INLINE_,863'bdi': goog.labs.html.scrubber.Group_.INLINE_,864'bdo': goog.labs.html.scrubber.Group_.INLINE_,865'big': goog.labs.html.scrubber.Group_.INLINE_,866'blink': goog.labs.html.scrubber.Group_.INLINE_,867'blockquote': goog.labs.html.scrubber.Group_.BLOCK_ |868goog.labs.html.scrubber.Group_.INLINE_,869'body': goog.labs.html.scrubber.Group_.BLOCK_ |870goog.labs.html.scrubber.Group_.INLINE_,871'button': goog.labs.html.scrubber.Group_.BLOCK_ |872goog.labs.html.scrubber.Group_.INLINE_,873'canvas': goog.labs.html.scrubber.Group_.INLINE_,874'caption': goog.labs.html.scrubber.Group_.INLINE_,875'center': goog.labs.html.scrubber.Group_.BLOCK_ |876goog.labs.html.scrubber.Group_.INLINE_,877'cite': goog.labs.html.scrubber.Group_.INLINE_,878'code': goog.labs.html.scrubber.Group_.INLINE_,879'colgroup': goog.labs.html.scrubber.Group_.COL_ELEMENT_,880'dd': goog.labs.html.scrubber.Group_.BLOCK_ |881goog.labs.html.scrubber.Group_.INLINE_,882'del': goog.labs.html.scrubber.Group_.BLOCK_ |883goog.labs.html.scrubber.Group_.INLINE_,884'dfn': goog.labs.html.scrubber.Group_.INLINE_,885'dir': goog.labs.html.scrubber.Group_.LI_ELEMENT_,886'div': goog.labs.html.scrubber.Group_.BLOCK_ |887goog.labs.html.scrubber.Group_.INLINE_,888'dl': goog.labs.html.scrubber.Group_.DL_PART_,889'dt': goog.labs.html.scrubber.Group_.INLINE_,890'em': goog.labs.html.scrubber.Group_.INLINE_,891'fieldset': goog.labs.html.scrubber.Group_.BLOCK_ |892goog.labs.html.scrubber.Group_.INLINE_ |893goog.labs.html.scrubber.Group_.LEGEND_ELEMENT_,894'font': goog.labs.html.scrubber.Group_.INLINE_,895'form':896(goog.labs.html.scrubber.Group_.BLOCK_ |897goog.labs.html.scrubber.Group_.INLINE_ |898goog.labs.html.scrubber.Group_.INLINE_MINUS_A_ |899goog.labs.html.scrubber.Group_.TR_ELEMENT_ |900goog.labs.html.scrubber.Group_.TD_ELEMENT_),901'h1': goog.labs.html.scrubber.Group_.INLINE_,902'h2': goog.labs.html.scrubber.Group_.INLINE_,903'h3': goog.labs.html.scrubber.Group_.INLINE_,904'h4': goog.labs.html.scrubber.Group_.INLINE_,905'h5': goog.labs.html.scrubber.Group_.INLINE_,906'h6': goog.labs.html.scrubber.Group_.INLINE_,907'head': goog.labs.html.scrubber.Group_.HEAD_CONTENT_,908'html': goog.labs.html.scrubber.Group_.TOP_CONTENT_,909'i': goog.labs.html.scrubber.Group_.INLINE_,910'iframe': goog.labs.html.scrubber.Group_.BLOCK_ |911goog.labs.html.scrubber.Group_.INLINE_,912'ins': goog.labs.html.scrubber.Group_.BLOCK_ |913goog.labs.html.scrubber.Group_.INLINE_,914'kbd': goog.labs.html.scrubber.Group_.INLINE_,915'label': goog.labs.html.scrubber.Group_.INLINE_,916'legend': goog.labs.html.scrubber.Group_.INLINE_,917'li': goog.labs.html.scrubber.Group_.BLOCK_ |918goog.labs.html.scrubber.Group_.INLINE_,919'listing': goog.labs.html.scrubber.Group_.INLINE_,920'map': goog.labs.html.scrubber.Group_.BLOCK_ |921goog.labs.html.scrubber.Group_.AREA_ELEMENT_,922'nobr': goog.labs.html.scrubber.Group_.INLINE_,923'noframes': goog.labs.html.scrubber.Group_.BLOCK_ |924goog.labs.html.scrubber.Group_.INLINE_ |925goog.labs.html.scrubber.Group_.TOP_CONTENT_,926'noscript': goog.labs.html.scrubber.Group_.BLOCK_ |927goog.labs.html.scrubber.Group_.INLINE_,928'object': goog.labs.html.scrubber.Group_.BLOCK_ |929goog.labs.html.scrubber.Group_.INLINE_ |930goog.labs.html.scrubber.Group_.PARAM_ELEMENT_,931'ol': goog.labs.html.scrubber.Group_.LI_ELEMENT_,932'optgroup': goog.labs.html.scrubber.Group_.OPTIONS_ELEMENT_,933'option': goog.labs.html.scrubber.Group_.CHARACTER_DATA_,934'p': goog.labs.html.scrubber.Group_.INLINE_ |935goog.labs.html.scrubber.Group_.TABLE_ELEMENT_,936'pre': goog.labs.html.scrubber.Group_.INLINE_,937'q': goog.labs.html.scrubber.Group_.INLINE_,938's': goog.labs.html.scrubber.Group_.INLINE_,939'samp': goog.labs.html.scrubber.Group_.INLINE_,940'script': goog.labs.html.scrubber.Group_.CHARACTER_DATA_,941'select': goog.labs.html.scrubber.Group_.OPTIONS_ELEMENT_,942'small': goog.labs.html.scrubber.Group_.INLINE_,943'span': goog.labs.html.scrubber.Group_.INLINE_,944'strike': goog.labs.html.scrubber.Group_.INLINE_,945'strong': goog.labs.html.scrubber.Group_.INLINE_,946'style': goog.labs.html.scrubber.Group_.CHARACTER_DATA_,947'sub': goog.labs.html.scrubber.Group_.INLINE_,948'sup': goog.labs.html.scrubber.Group_.INLINE_,949'table': goog.labs.html.scrubber.Group_.TABLE_CONTENT_ |950goog.labs.html.scrubber.Group_.FORM_ELEMENT_,951'tbody': goog.labs.html.scrubber.Group_.TR_ELEMENT_,952'td': goog.labs.html.scrubber.Group_.BLOCK_ |953goog.labs.html.scrubber.Group_.INLINE_,954'textarea': goog.labs.html.scrubber.Group_.CHARACTER_DATA_,955'tfoot': goog.labs.html.scrubber.Group_.FORM_ELEMENT_ |956goog.labs.html.scrubber.Group_.TR_ELEMENT_ |957goog.labs.html.scrubber.Group_.TD_ELEMENT_,958'th': goog.labs.html.scrubber.Group_.BLOCK_ |959goog.labs.html.scrubber.Group_.INLINE_,960'thead': goog.labs.html.scrubber.Group_.FORM_ELEMENT_ |961goog.labs.html.scrubber.Group_.TR_ELEMENT_ |962goog.labs.html.scrubber.Group_.TD_ELEMENT_,963'title': goog.labs.html.scrubber.Group_.CHARACTER_DATA_,964'tr': goog.labs.html.scrubber.Group_.FORM_ELEMENT_ |965goog.labs.html.scrubber.Group_.TD_ELEMENT_,966'tt': goog.labs.html.scrubber.Group_.INLINE_,967'u': goog.labs.html.scrubber.Group_.INLINE_,968'ul': goog.labs.html.scrubber.Group_.LI_ELEMENT_,969'var': goog.labs.html.scrubber.Group_.INLINE_,970'xmp': goog.labs.html.scrubber.Group_.INLINE_971};972973974/**975* The scopes in which an element falls.976* No property defaults to 0.977* @private978*/979goog.labs.html.scrubber.ELEMENT_SCOPES_ = {980'applet': goog.labs.html.scrubber.Scope_.COMMON_ |981goog.labs.html.scrubber.Scope_.BUTTON_ |982goog.labs.html.scrubber.Scope_.LIST_ITEM_,983'button': goog.labs.html.scrubber.Scope_.BUTTON_,984'caption': goog.labs.html.scrubber.Scope_.COMMON_ |985goog.labs.html.scrubber.Scope_.BUTTON_ |986goog.labs.html.scrubber.Scope_.LIST_ITEM_,987'html': goog.labs.html.scrubber.Scope_.COMMON_ |988goog.labs.html.scrubber.Scope_.BUTTON_ |989goog.labs.html.scrubber.Scope_.LIST_ITEM_ |990goog.labs.html.scrubber.Scope_.TABLE_,991'object': goog.labs.html.scrubber.Scope_.COMMON_ |992goog.labs.html.scrubber.Scope_.BUTTON_ |993goog.labs.html.scrubber.Scope_.LIST_ITEM_,994'ol': goog.labs.html.scrubber.Scope_.LIST_ITEM_,995'table': goog.labs.html.scrubber.Scope_.COMMON_ |996goog.labs.html.scrubber.Scope_.BUTTON_ |997goog.labs.html.scrubber.Scope_.LIST_ITEM_ |998goog.labs.html.scrubber.Scope_.TABLE_,999'td': goog.labs.html.scrubber.Scope_.COMMON_ |1000goog.labs.html.scrubber.Scope_.BUTTON_ |1001goog.labs.html.scrubber.Scope_.LIST_ITEM_,1002'th': goog.labs.html.scrubber.Scope_.COMMON_ |1003goog.labs.html.scrubber.Scope_.BUTTON_ |1004goog.labs.html.scrubber.Scope_.LIST_ITEM_,1005'ul': goog.labs.html.scrubber.Scope_.LIST_ITEM_1006};100710081009/**1010* Per-element, a child that can contain block content.1011* @private1012*/1013goog.labs.html.scrubber.BLOCK_CONTAINERS_ = {1014'dl': 'dd',1015'ol': 'li',1016'table': 'tr',1017'tr': 'td',1018'ul': 'li'1019};102010211022goog.labs.html.attributeRewriterPresubmitWorkaround();102310241025