UNPKG

closure-builder

Version:

Simple Closure, Soy and JavaScript Build system

1,379 lines (1,221 loc) 80.3 kB
/* * Copyright 2008 Google Inc. * * 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 * Utility functions and classes for Soy. * * <p> * The top portion of this file contains utilities for Soy users:<ul> * <li> soy.StringBuilder: Compatible with the 'stringbuilder' code style. * <li> soy.renderElement: Render template and set as innerHTML of an element. * <li> soy.renderAsFragment: Render template and return as HTML fragment. * </ul> * * <p> * The bottom portion of this file contains utilities that should only be called * by Soy-generated JS code. Please do not use these functions directly from * your hand-writen code. Their names all start with '$$'. * * @author Garrett Boyer * @author Mike Samuel * @author Kai Huang * @author Aharon Lanin */ goog.provide('soy'); goog.provide('soy.StringBuilder'); goog.provide('soy.esc'); goog.provide('soydata'); goog.provide('soydata.SanitizedHtml'); goog.provide('soydata.SanitizedHtmlAttribute'); goog.provide('soydata.SanitizedJs'); goog.provide('soydata.SanitizedJsStrChars'); goog.provide('soydata.SanitizedUri'); goog.provide('soydata.VERY_UNSAFE'); goog.require('goog.asserts'); goog.require('goog.dom.DomHelper'); goog.require('goog.format'); goog.require('goog.i18n.BidiFormatter'); goog.require('goog.i18n.bidi'); goog.require('goog.soy'); goog.require('goog.soy.data.SanitizedContentKind'); goog.require('goog.string'); goog.require('goog.string.StringBuffer'); // ----------------------------------------------------------------------------- // StringBuilder (compatible with the 'stringbuilder' code style). /** * Utility class to facilitate much faster string concatenation in IE, * using Array.join() rather than the '+' operator. For other browsers * we simply use the '+' operator. * * @param {Object} var_args Initial items to append, * e.g., new soy.StringBuilder('foo', 'bar'). * @constructor */ soy.StringBuilder = goog.string.StringBuffer; // ----------------------------------------------------------------------------- // soydata: Defines typed strings, e.g. an HTML string {@code "a<b>c"} is // semantically distinct from the plain text string {@code "a<b>c"} and smart // templates can take that distinction into account. /** * A type of textual content. * * This is an enum of type Object so that these values are unforgeable. * * @enum {!Object} */ soydata.SanitizedContentKind = goog.soy.data.SanitizedContentKind; /** * Checks whether a given value is of a given content kind. * * @param {*} value The value to be examined. * @param {soydata.SanitizedContentKind} contentKind The desired content * kind. * @return {boolean} Whether the given value is of the given kind. * @private */ soydata.isContentKind = function(value, contentKind) { // TODO(user): This function should really include the assert on // value.constructor that is currently sprinkled at most of the call sites. // Unfortunately, that would require a (debug-mode-only) switch statement. // TODO(user): Perhaps we should get rid of the contentKind property // altogether and only at the constructor. return value != null && value.contentKind === contentKind; }; /** * Returns a given value's contentDir property, constrained to a * goog.i18n.bidi.Dir value or null. Returns null if the value is null, * undefined, a primitive or does not have a contentDir property, or the * property's value is not 1 (for LTR), -1 (for RTL), or 0 (for neutral). * * @param {*} value The value whose contentDir property, if any, is to * be returned. * @return {?goog.i18n.bidi.Dir} The contentDir property. */ soydata.getContentDir = function(value) { if (value != null) { switch (value.contentDir) { case goog.i18n.bidi.Dir.LTR: return goog.i18n.bidi.Dir.LTR; case goog.i18n.bidi.Dir.RTL: return goog.i18n.bidi.Dir.RTL; case goog.i18n.bidi.Dir.NEUTRAL: return goog.i18n.bidi.Dir.NEUTRAL; } } return null; }; /** * Content of type {@link soydata.SanitizedContentKind.HTML}. * * The content is a string of HTML that can safely be embedded in a PCDATA * context in your app. If you would be surprised to find that an HTML * sanitizer produced {@code s} (e.g. it runs code or fetches bad URLs) and * you wouldn't write a template that produces {@code s} on security or privacy * grounds, then don't pass {@code s} here. The default content direction is * unknown, i.e. to be estimated when necessary. * * @constructor * @extends {goog.soy.data.SanitizedContent} */ soydata.SanitizedHtml = function() { goog.soy.data.SanitizedContent.call(this); // Throws an exception. }; goog.inherits(soydata.SanitizedHtml, goog.soy.data.SanitizedContent); /** @override */ soydata.SanitizedHtml.prototype.contentKind = soydata.SanitizedContentKind.HTML; /** * Returns a SanitizedHtml object for a particular value. The content direction * is preserved. * * This HTML-escapes the value unless it is already SanitizedHtml. * * @param {*} value The value to convert. If it is already a SanitizedHtml * object, it is left alone. * @return {!soydata.SanitizedHtml} A SanitizedHtml object derived from the * stringified value. It is escaped unless the input is SanitizedHtml. */ soydata.SanitizedHtml.from = function(value) { // The check is soydata.isContentKind() inlined for performance. if (value != null && value.contentKind === soydata.SanitizedContentKind.HTML) { goog.asserts.assert(value.constructor === soydata.SanitizedHtml); return /** @type {!soydata.SanitizedHtml} */ (value); } return soydata.VERY_UNSAFE.ordainSanitizedHtml( soy.esc.$$escapeHtmlHelper(String(value)), soydata.getContentDir(value)); }; /** * Content of type {@link soydata.SanitizedContentKind.JS}. * * The content is Javascript source that when evaluated does not execute any * attacker-controlled scripts. The content direction is LTR. * * @constructor * @extends {goog.soy.data.SanitizedContent} */ soydata.SanitizedJs = function() { goog.soy.data.SanitizedContent.call(this); // Throws an exception. }; goog.inherits(soydata.SanitizedJs, goog.soy.data.SanitizedContent); /** @override */ soydata.SanitizedJs.prototype.contentKind = soydata.SanitizedContentKind.JS; /** @override */ soydata.SanitizedJs.prototype.contentDir = goog.i18n.bidi.Dir.LTR; /** * Content of type {@link soydata.SanitizedContentKind.JS_STR_CHARS}. * * The content can be safely inserted as part of a single- or double-quoted * string without terminating the string. The default content direction is * unknown, i.e. to be estimated when necessary. * * @constructor * @extends {goog.soy.data.SanitizedContent} */ soydata.SanitizedJsStrChars = function() { goog.soy.data.SanitizedContent.call(this); // Throws an exception. }; goog.inherits(soydata.SanitizedJsStrChars, goog.soy.data.SanitizedContent); /** @override */ soydata.SanitizedJsStrChars.prototype.contentKind = soydata.SanitizedContentKind.JS_STR_CHARS; /** * Content of type {@link soydata.SanitizedContentKind.URI}. * * The content is a URI chunk that the caller knows is safe to emit in a * template. The content direction is LTR. * * @constructor * @extends {goog.soy.data.SanitizedContent} */ soydata.SanitizedUri = function() { goog.soy.data.SanitizedContent.call(this); // Throws an exception. }; goog.inherits(soydata.SanitizedUri, goog.soy.data.SanitizedContent); /** @override */ soydata.SanitizedUri.prototype.contentKind = soydata.SanitizedContentKind.URI; /** @override */ soydata.SanitizedUri.prototype.contentDir = goog.i18n.bidi.Dir.LTR; /** * Content of type {@link soydata.SanitizedContentKind.ATTRIBUTES}. * * The content should be safely embeddable within an open tag, such as a * key="value" pair. The content direction is LTR. * * @constructor * @extends {goog.soy.data.SanitizedContent} */ soydata.SanitizedHtmlAttribute = function() { goog.soy.data.SanitizedContent.call(this); // Throws an exception. }; goog.inherits(soydata.SanitizedHtmlAttribute, goog.soy.data.SanitizedContent); /** @override */ soydata.SanitizedHtmlAttribute.prototype.contentKind = soydata.SanitizedContentKind.ATTRIBUTES; /** @override */ soydata.SanitizedHtmlAttribute.prototype.contentDir = goog.i18n.bidi.Dir.LTR; /** * Content of type {@link soydata.SanitizedContentKind.CSS}. * * The content is non-attacker-exploitable CSS, such as {@code color:#c3d9ff}. * The content direction is LTR. * * @constructor * @extends {goog.soy.data.SanitizedContent} */ soydata.SanitizedCss = function() { goog.soy.data.SanitizedContent.call(this); // Throws an exception. }; goog.inherits(soydata.SanitizedCss, goog.soy.data.SanitizedContent); /** @override */ soydata.SanitizedCss.prototype.contentKind = soydata.SanitizedContentKind.CSS; /** @override */ soydata.SanitizedCss.prototype.contentDir = goog.i18n.bidi.Dir.LTR; /** * Unsanitized plain text string. * * While all strings are effectively safe to use as a plain text, there are no * guarantees about safety in any other context such as HTML. This is * sometimes used to mark that should never be used unescaped. * * @param {*} content Plain text with no guarantees. * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if * unknown and thus to be estimated when necessary. Default: null. * @constructor * @extends {goog.soy.data.SanitizedContent} */ soydata.UnsanitizedText = function(content, opt_contentDir) { /** @override */ this.content = String(content); this.contentDir = opt_contentDir != null ? opt_contentDir : null; }; goog.inherits(soydata.UnsanitizedText, goog.soy.data.SanitizedContent); /** @override */ soydata.UnsanitizedText.prototype.contentKind = soydata.SanitizedContentKind.TEXT; /** * Empty string, used as a type in Soy templates. * @enum {string} * @private */ soydata.$$EMPTY_STRING_ = { VALUE: '' }; /** * Creates a factory for SanitizedContent types. * * This is a hack so that the soydata.VERY_UNSAFE.ordainSanitized* can * instantiate Sanitized* classes, without making the Sanitized* constructors * publicly usable. Requiring all construction to use the VERY_UNSAFE names * helps callers and their reviewers easily tell that creating SanitizedContent * is not always safe and calls for careful review. * * @param {function(new: T)} ctor A constructor. * @return {!function(*, ?goog.i18n.bidi.Dir=): T} A factory that takes * content and an optional content direction and returns a new instance. If * the content direction is undefined, ctor.prototype.contentDir is used. * @template T * @private */ soydata.$$makeSanitizedContentFactory_ = function(ctor) { /** @type {function(new: goog.soy.data.SanitizedContent)} */ function InstantiableCtor() {} InstantiableCtor.prototype = ctor.prototype; /** * Creates a ctor-type SanitizedContent instance. * * @param {*} content The content to put in the instance. * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction. If * undefined, ctor.prototype.contentDir is used. * @return {!goog.soy.data.SanitizedContent} The new instance. It is actually * of type T above (ctor's type, a descendant of SanitizedContent), but * there is no way to express that here. */ function sanitizedContentFactory(content, opt_contentDir) { var result = new InstantiableCtor(); result.content = String(content); if (opt_contentDir !== undefined) { result.contentDir = opt_contentDir; } return result; } return sanitizedContentFactory; }; /** * Creates a factory for SanitizedContent types that should always have their * default directionality. * * This is a hack so that the soydata.VERY_UNSAFE.ordainSanitized* can * instantiate Sanitized* classes, without making the Sanitized* constructors * publicly usable. Requiring all construction to use the VERY_UNSAFE names * helps callers and their reviewers easily tell that creating SanitizedContent * is not always safe and calls for careful review. * * @param {function(new: T, string)} ctor A constructor. * @return {!function(*): T} A factory that takes content and returns a new * instance (with default directionality, i.e. ctor.prototype.contentDir). * @template T * @private */ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_ = function(ctor) { /** @type {function(new: goog.soy.data.SanitizedContent)} */ function InstantiableCtor() {} InstantiableCtor.prototype = ctor.prototype; /** * Creates a ctor-type SanitizedContent instance. * * @param {*} content The content to put in the instance. * @return {!goog.soy.data.SanitizedContent} The new instance. It is actually * of type T above (ctor's type, a descendant of SanitizedContent), but * there is no way to express that here. */ function sanitizedContentFactory(content) { var result = new InstantiableCtor(); result.content = String(content); return result; } return sanitizedContentFactory; }; // ----------------------------------------------------------------------------- // Sanitized content ordainers. Please use these with extreme caution (with the // exception of markUnsanitizedText). A good recommendation is to limit usage // of these to just a handful of files in your source tree where usages can be // carefully audited. /** * Protects a string from being used in an noAutoescaped context. * * This is useful for content where there is significant risk of accidental * unescaped usage in a Soy template. A great case is for user-controlled * data that has historically been a source of vulernabilities. * * @param {*} content Text to protect. * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if * unknown and thus to be estimated when necessary. Default: null. * @return {!soydata.UnsanitizedText} A wrapper that is rejected by the * Soy noAutoescape print directive. */ soydata.markUnsanitizedText = function(content, opt_contentDir) { return new soydata.UnsanitizedText(content, opt_contentDir); }; /** * Takes a leap of faith that the provided content is "safe" HTML. * * @param {*} content A string of HTML that can safely be embedded in * a PCDATA context in your app. If you would be surprised to find that an * HTML sanitizer produced {@code s} (e.g. it runs code or fetches bad URLs) * and you wouldn't write a template that produces {@code s} on security or * privacy grounds, then don't pass {@code s} here. * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if * unknown and thus to be estimated when necessary. Default: null. * @return {!soydata.SanitizedHtml} Sanitized content wrapper that * indicates to Soy not to escape when printed as HTML. */ soydata.VERY_UNSAFE.ordainSanitizedHtml = soydata.$$makeSanitizedContentFactory_(soydata.SanitizedHtml); /** * Takes a leap of faith that the provided content is "safe" (non-attacker- * controlled, XSS-free) Javascript. * * @param {*} content Javascript source that when evaluated does not * execute any attacker-controlled scripts. * @return {!soydata.SanitizedJs} Sanitized content wrapper that indicates to * Soy not to escape when printed as Javascript source. */ soydata.VERY_UNSAFE.ordainSanitizedJs = soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_( soydata.SanitizedJs); // TODO: This function is probably necessary, either externally or internally // as an implementation detail. Generally, plain text will always work here, // as there's no harm to unescaping the string and then re-escaping when // finally printed. /** * Takes a leap of faith that the provided content can be safely embedded in * a Javascript string without re-esacping. * * @param {*} content Content that can be safely inserted as part of a * single- or double-quoted string without terminating the string. * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if * unknown and thus to be estimated when necessary. Default: null. * @return {!soydata.SanitizedJsStrChars} Sanitized content wrapper that * indicates to Soy not to escape when printed in a JS string. */ soydata.VERY_UNSAFE.ordainSanitizedJsStrChars = soydata.$$makeSanitizedContentFactory_(soydata.SanitizedJsStrChars); /** * Takes a leap of faith that the provided content is "safe" to use as a URI * in a Soy template. * * This creates a Soy SanitizedContent object which indicates to Soy there is * no need to escape it when printed as a URI (e.g. in an href or src * attribute), such as if it's already been encoded or if it's a Javascript: * URI. * * @param {*} content A chunk of URI that the caller knows is safe to * emit in a template. * @return {!soydata.SanitizedUri} Sanitized content wrapper that indicates to * Soy not to escape or filter when printed in URI context. */ soydata.VERY_UNSAFE.ordainSanitizedUri = soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_( soydata.SanitizedUri); /** * Takes a leap of faith that the provided content is "safe" to use as an * HTML attribute. * * @param {*} content An attribute name and value, such as * {@code dir="ltr"}. * @return {!soydata.SanitizedHtmlAttribute} Sanitized content wrapper that * indicates to Soy not to escape when printed as an HTML attribute. */ soydata.VERY_UNSAFE.ordainSanitizedHtmlAttribute = soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_( soydata.SanitizedHtmlAttribute); /** * Takes a leap of faith that the provided content is "safe" to use as CSS * in a style attribute or block. * * @param {*} content CSS, such as {@code color:#c3d9ff}. * @return {!soydata.SanitizedCss} Sanitized CSS wrapper that indicates to * Soy there is no need to escape or filter when printed in CSS context. */ soydata.VERY_UNSAFE.ordainSanitizedCss = soydata.$$makeSanitizedContentFactoryWithDefaultDirOnly_( soydata.SanitizedCss); // ----------------------------------------------------------------------------- // Public utilities. /** * Helper function to render a Soy template and then set the output string as * the innerHTML of an element. It is recommended to use this helper function * instead of directly setting innerHTML in your hand-written code, so that it * will be easier to audit the code for cross-site scripting vulnerabilities. * * NOTE: New code should consider using goog.soy.renderElement instead. * * @param {Element} element The element whose content we are rendering. * @param {null|function(ARG_TYPES, null=, Object.<string, *>=):*} template * The Soy template defining the element's content. * @param {ARG_TYPES} opt_templateData The data for the template. * @param {Object=} opt_injectedData The injected data for the template. * @template ARG_TYPES */ soy.renderElement = goog.soy.renderElement; /** * Helper function to render a Soy template into a single node or a document * fragment. If the rendered HTML string represents a single node, then that * node is returned (note that this is *not* a fragment, despite them name of * the method). Otherwise a document fragment is returned containing the * rendered nodes. * * NOTE: New code should consider using goog.soy.renderAsFragment * instead (note that the arguments are different). * * @param {null|function(ARG_TYPES, null=, Object.<string, *>=):*} template * The Soy template defining the element's content. * @param {ARG_TYPES} opt_templateData The data for the template. * @param {Document=} opt_document The document used to create DOM nodes. If not * specified, global document object is used. * @param {Object=} opt_injectedData The injected data for the template. * @return {!Node} The resulting node or document fragment. * @template ARG_TYPES */ soy.renderAsFragment = function( template, opt_templateData, opt_document, opt_injectedData) { return goog.soy.renderAsFragment( template, opt_templateData, opt_injectedData, new goog.dom.DomHelper(opt_document)); }; /** * Helper function to render a Soy template into a single node. If the rendered * HTML string represents a single node, then that node is returned. Otherwise, * a DIV element is returned containing the rendered nodes. * * NOTE: New code should consider using goog.soy.renderAsElement * instead (note that the arguments are different). * * @param {null|function(ARG_TYPES, null=, Object.<string, *>=):*} template * The Soy template defining the element's content. * @param {ARG_TYPES} opt_templateData The data for the template. * @param {Document=} opt_document The document used to create DOM nodes. If not * specified, global document object is used. * @param {Object=} opt_injectedData The injected data for the template. * @return {!Element} Rendered template contents, wrapped in a parent DIV * element if necessary. * @template ARG_TYPES */ soy.renderAsElement = function( template, opt_templateData, opt_document, opt_injectedData) { return goog.soy.renderAsElement( template, opt_templateData, opt_injectedData, new goog.dom.DomHelper(opt_document)); }; // ----------------------------------------------------------------------------- // Below are private utilities to be used by Soy-generated code only. /** * Whether the locale is right-to-left. * * @type {boolean} */ soy.$$IS_LOCALE_RTL = goog.i18n.bidi.IS_RTL; /** * Builds an augmented map. The returned map will contain mappings from both * the base map and the additional map. If the same key appears in both, then * the value from the additional map will be visible, while the value from the * base map will be hidden. The base map will be used, but not modified. * * @param {!Object} baseMap The original map to augment. * @param {!Object} additionalMap A map containing the additional mappings. * @return {!Object} An augmented map containing both the original and * additional mappings. */ soy.$$augmentMap = function(baseMap, additionalMap) { // Create a new map whose '__proto__' field is set to baseMap. /** @constructor */ function TempCtor() {} TempCtor.prototype = baseMap; var augmentedMap = new TempCtor(); // Add the additional mappings to the new map. for (var key in additionalMap) { augmentedMap[key] = additionalMap[key]; } return augmentedMap; }; /** * Checks that the given map key is a string. * @param {*} key Key to check. * @return {string} The given key. */ soy.$$checkMapKey = function(key) { // TODO: Support map literal with nonstring key. if ((typeof key) != 'string') { throw Error( 'Map literal\'s key expression must evaluate to string' + ' (encountered type "' + (typeof key) + '").'); } return key; }; /** * Gets the keys in a map as an array. There are no guarantees on the order. * @param {Object} map The map to get the keys of. * @return {!Array.<string>} The array of keys in the given map. */ soy.$$getMapKeys = function(map) { var mapKeys = []; for (var key in map) { mapKeys.push(key); } return mapKeys; }; /** * Gets a consistent unique id for the given delegate template name. Two calls * to this function will return the same id if and only if the input names are * the same. * * <p> Important: This function must always be called with a string constant. * * <p> If Closure Compiler is not being used, then this is just this identity * function. If Closure Compiler is being used, then each call to this function * will be replaced with a short string constant, which will be consistent per * input name. * * @param {string} delTemplateName The delegate template name for which to get a * consistent unique id. * @return {string} A unique id that is consistent per input name. * * @consistentIdGenerator */ soy.$$getDelTemplateId = function(delTemplateName) { return delTemplateName; }; /** * Map from registered delegate template key to the priority of the * implementation. * @type {Object} * @private */ soy.$$DELEGATE_REGISTRY_PRIORITIES_ = {}; /** * Map from registered delegate template key to the implementation function. * @type {Object} * @private */ soy.$$DELEGATE_REGISTRY_FUNCTIONS_ = {}; /** * Registers a delegate implementation. If the same delegate template key (id * and variant) has been registered previously, then priority values are * compared and only the higher priority implementation is stored (if * priorities are equal, an error is thrown). * * @param {string} delTemplateId The delegate template id. * @param {string} delTemplateVariant The delegate template variant (can be * empty string). * @param {number} delPriority The implementation's priority value. * @param {Function} delFn The implementation function. */ soy.$$registerDelegateFn = function( delTemplateId, delTemplateVariant, delPriority, delFn) { var mapKey = 'key_' + delTemplateId + ':' + delTemplateVariant; var currPriority = soy.$$DELEGATE_REGISTRY_PRIORITIES_[mapKey]; if (currPriority === undefined || delPriority > currPriority) { // Registering new or higher-priority function: replace registry entry. soy.$$DELEGATE_REGISTRY_PRIORITIES_[mapKey] = delPriority; soy.$$DELEGATE_REGISTRY_FUNCTIONS_[mapKey] = delFn; } else if (delPriority == currPriority) { // Registering same-priority function: error. throw Error( 'Encountered two active delegates with the same priority ("' + delTemplateId + ':' + delTemplateVariant + '").'); } else { // Registering lower-priority function: do nothing. } }; /** * Retrieves the (highest-priority) implementation that has been registered for * a given delegate template key (id and variant). If no implementation has * been registered for the key, then the fallback is the same id with empty * variant. If the fallback is also not registered, and allowsEmptyDefault is * true, then returns an implementation that is equivalent to an empty template * (i.e. rendered output would be empty string). * * @param {string} delTemplateId The delegate template id. * @param {string} delTemplateVariant The delegate template variant (can be * empty string). * @param {boolean} allowsEmptyDefault Whether to default to the empty template * function if there's no active implementation. * @return {Function} The retrieved implementation function. */ soy.$$getDelegateFn = function( delTemplateId, delTemplateVariant, allowsEmptyDefault) { var delFn = soy.$$DELEGATE_REGISTRY_FUNCTIONS_[ 'key_' + delTemplateId + ':' + delTemplateVariant]; if (! delFn && delTemplateVariant != '') { // Fallback to empty variant. delFn = soy.$$DELEGATE_REGISTRY_FUNCTIONS_['key_' + delTemplateId + ':']; } if (delFn) { return delFn; } else if (allowsEmptyDefault) { return soy.$$EMPTY_TEMPLATE_FN_; } else { throw Error( 'Found no active impl for delegate call to "' + delTemplateId + ':' + delTemplateVariant + '" (and not allowemptydefault="true").'); } }; /** * Private helper soy.$$getDelegateFn(). This is the empty template function * that is returned whenever there's no delegate implementation found. * * @param {Object.<string, *>=} opt_data * @param {soy.StringBuilder=} opt_sb * @param {Object.<string, *>=} opt_ijData * @return {string} * @private */ soy.$$EMPTY_TEMPLATE_FN_ = function(opt_data, opt_sb, opt_ijData) { return ''; }; // ----------------------------------------------------------------------------- // Internal sanitized content wrappers. /** * Creates a SanitizedContent factory for SanitizedContent types for internal * Soy let and param blocks. * * This is a hack within Soy so that SanitizedContent objects created via let * and param blocks will truth-test as false if they are empty string. * Tricking the Javascript runtime to treat empty SanitizedContent as falsey is * not possible, and changing the Soy compiler to wrap every boolean statement * for just this purpose is impractical. Instead, we just avoid wrapping empty * string as SanitizedContent, since it's a no-op for empty strings anyways. * * @param {function(new: T)} ctor A constructor. * @return {!function(*, ?goog.i18n.bidi.Dir=): (T|soydata.$$EMPTY_STRING_)} * A factory that takes content and an optional content direction and * returns a new instance, or an empty string. If the content direction is * undefined, ctor.prototype.contentDir is used. * @template T * @private */ soydata.$$makeSanitizedContentFactoryForInternalBlocks_ = function(ctor) { /** @type {function(new: goog.soy.data.SanitizedContent)} */ function InstantiableCtor() {} InstantiableCtor.prototype = ctor.prototype; /** * Creates a ctor-type SanitizedContent instance. * * @param {*} content The content to put in the instance. * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction. If * undefined, ctor.prototype.contentDir is used. * @return {!goog.soy.data.SanitizedContent|soydata.$$EMPTY_STRING_} The new * instance, or an empty string. A new instance is actually of type T * above (ctor's type, a descendant of SanitizedContent), but there's no * way to express that here. * a descendant of SanitizedContent), but there's no way to express that here. */ function sanitizedContentFactory(content, opt_contentDir) { var contentString = String(content); if (!contentString) { return soydata.$$EMPTY_STRING_.VALUE; } var result = new InstantiableCtor(); result.content = String(content); if (opt_contentDir !== undefined) { result.contentDir = opt_contentDir; } return result; } return sanitizedContentFactory; }; /** * Creates a SanitizedContent factory for SanitizedContent types that should * always have their default directionality for internal Soy let and param * blocks. * * This is a hack within Soy so that SanitizedContent objects created via let * and param blocks will truth-test as false if they are empty string. * Tricking the Javascript runtime to treat empty SanitizedContent as falsey is * not possible, and changing the Soy compiler to wrap every boolean statement * for just this purpose is impractical. Instead, we just avoid wrapping empty * string as SanitizedContent, since it's a no-op for empty strings anyways. * * @param {function(new: T)} ctor A constructor. * @return {!function(*): (T|soydata.$$EMPTY_STRING_)} A * factory that takes content and returns a * new instance (with default directionality, i.e. * ctor.prototype.contentDir), or an empty string. * @template T * @private */ soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_ = function(ctor) { /** @type {function(new: goog.soy.data.SanitizedContent)} */ function InstantiableCtor() {} InstantiableCtor.prototype = ctor.prototype; /** * Creates a ctor-type SanitizedContent instance. * * @param {*} content The content to put in the instance. * @return {!goog.soy.data.SanitizedContent|soydata.$$EMPTY_STRING_} The new * instance, or an empty string. A new instance is actually of type T * above (ctor's type, a descendant of SanitizedContent), but there's no * way to express that here. * a descendant of SanitizedContent), but there's no way to express that here. */ function sanitizedContentFactory(content) { var contentString = String(content); if (!contentString) { return soydata.$$EMPTY_STRING_.VALUE; } var result = new InstantiableCtor(); result.content = String(content); return result; } return sanitizedContentFactory; }; /** * Creates kind="text" block contents (internal use only). * * @param {*} content Text. * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if * unknown and thus to be estimated when necessary. Default: null. * @return {!soydata.UnsanitizedText|soydata.$$EMPTY_STRING_} Wrapped result. */ soydata.$$markUnsanitizedTextForInternalBlocks = function( content, opt_contentDir) { var contentString = String(content); if (!contentString) { return soydata.$$EMPTY_STRING_.VALUE; } return new soydata.UnsanitizedText(contentString, opt_contentDir); }; /** * Creates kind="html" block contents (internal use only). * * @param {*} content Text. * @param {?goog.i18n.bidi.Dir=} opt_contentDir The content direction; null if * unknown and thus to be estimated when necessary. Default: null. * @return {!soydata.SanitizedHtml|soydata.$$EMPTY_STRING_} Wrapped result. */ soydata.VERY_UNSAFE.$$ordainSanitizedHtmlForInternalBlocks = soydata.$$makeSanitizedContentFactoryForInternalBlocks_( soydata.SanitizedHtml); /** * Creates kind="js" block contents (internal use only). * * @param {*} content Text. * @return {!soydata.SanitizedJs|soydata.$$EMPTY_STRING_} Wrapped result. */ soydata.VERY_UNSAFE.$$ordainSanitizedJsForInternalBlocks = soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_( soydata.SanitizedJs); /** * Creates kind="uri" block contents (internal use only). * * @param {*} content Text. * @return {soydata.SanitizedUri|soydata.$$EMPTY_STRING_} Wrapped result. */ soydata.VERY_UNSAFE.$$ordainSanitizedUriForInternalBlocks = soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_( soydata.SanitizedUri); /** * Creates kind="attributes" block contents (internal use only). * * @param {*} content Text. * @return {soydata.SanitizedHtmlAttribute|soydata.$$EMPTY_STRING_} Wrapped * result. */ soydata.VERY_UNSAFE.$$ordainSanitizedAttributesForInternalBlocks = soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_( soydata.SanitizedHtmlAttribute); /** * Creates kind="css" block contents (internal use only). * * @param {*} content Text. * @return {soydata.SanitizedCss|soydata.$$EMPTY_STRING_} Wrapped result. */ soydata.VERY_UNSAFE.$$ordainSanitizedCssForInternalBlocks = soydata.$$makeSanitizedContentFactoryWithDefaultDirOnlyForInternalBlocks_( soydata.SanitizedCss); // ----------------------------------------------------------------------------- // Escape/filter/normalize. /** * Returns a SanitizedHtml object for a particular value. The content direction * is preserved. * * This HTML-escapes the value unless it is already SanitizedHtml. Escapes * double quote '"' in addition to '&', '<', and '>' so that a string can be * included in an HTML tag attribute value within double quotes. * * @param {*} value The value to convert. If it is already a SanitizedHtml * object, it is left alone. * @return {!soydata.SanitizedHtml} An escaped version of value. */ soy.$$escapeHtml = function(value) { return soydata.SanitizedHtml.from(value); }; /** * Strips unsafe tags to convert a string of untrusted HTML into HTML that * is safe to embed. The content direction is preserved. * * @param {*} value The string-like value to be escaped. May not be a string, * but the value will be coerced to a string. * @return {!soydata.SanitizedHtml} A sanitized and normalized version of value. */ soy.$$cleanHtml = function(value) { if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) { goog.asserts.assert(value.constructor === soydata.SanitizedHtml); return /** @type {!soydata.SanitizedHtml} */ (value); } return soydata.VERY_UNSAFE.ordainSanitizedHtml( soy.$$stripHtmlTags(value, soy.esc.$$SAFE_TAG_WHITELIST_), soydata.getContentDir(value)); }; /** * Escapes HTML special characters in a string so that it can be embedded in * RCDATA. * <p> * Escapes HTML special characters so that the value will not prematurely end * the body of a tag like {@code <textarea>} or {@code <title>}. RCDATA tags * cannot contain other HTML entities, so it is not strictly necessary to escape * HTML special characters except when part of that text looks like an HTML * entity or like a close tag : {@code </textarea>}. * <p> * Will normalize known safe HTML to make sure that sanitized HTML (which could * contain an innocuous {@code </textarea>} don't prematurely end an RCDATA * element. * * @param {*} value The string-like value to be escaped. May not be a string, * but the value will be coerced to a string. * @return {string} An escaped version of value. */ soy.$$escapeHtmlRcdata = function(value) { if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) { goog.asserts.assert(value.constructor === soydata.SanitizedHtml); return soy.esc.$$normalizeHtmlHelper(value.content); } return soy.esc.$$escapeHtmlHelper(value); }; /** * Matches any/only HTML5 void elements' start tags. * See http://www.w3.org/TR/html-markup/syntax.html#syntax-elements * @type {RegExp} * @private */ soy.$$HTML5_VOID_ELEMENTS_ = new RegExp( '^<(?:area|base|br|col|command|embed|hr|img|input' + '|keygen|link|meta|param|source|track|wbr)\\b'); /** * Removes HTML tags from a string of known safe HTML. * If opt_tagWhitelist is not specified or is empty, then * the result can be used as an attribute value. * * @param {*} value The HTML to be escaped. May not be a string, but the * value will be coerced to a string. * @param {Object.<string, number>=} opt_tagWhitelist Has an own property whose * name is a lower-case tag name and whose value is {@code 1} for * each element that is allowed in the output. * @return {string} A representation of value without disallowed tags, * HTML comments, or other non-text content. */ soy.$$stripHtmlTags = function(value, opt_tagWhitelist) { if (!opt_tagWhitelist) { // If we have no white-list, then use a fast track which elides all tags. return String(value).replace(soy.esc.$$HTML_TAG_REGEX_, '') // This is just paranoia since callers should normalize the result // anyway, but if they didn't, it would be necessary to ensure that // after the first replace non-tag uses of < do not recombine into // tags as in "<<foo>script>alert(1337)</<foo>script>". .replace(soy.esc.$$LT_REGEX_, '&lt;'); } // Escapes '[' so that we can use [123] below to mark places where tags // have been removed. var html = String(value).replace(/\[/g, '&#91;'); // Consider all uses of '<' and replace whitelisted tags with markers like // [1] which are indices into a list of approved tag names. // Replace all other uses of < and > with entities. var tags = []; html = html.replace( soy.esc.$$HTML_TAG_REGEX_, function(tok, tagName) { if (tagName) { tagName = tagName.toLowerCase(); if (opt_tagWhitelist.hasOwnProperty(tagName) && opt_tagWhitelist[tagName]) { var start = tok.charAt(1) === '/' ? '</' : '<'; var index = tags.length; tags[index] = start + tagName + '>'; return '[' + index + ']'; } } return ''; }); // Escape HTML special characters. Now there are no '<' in html that could // start a tag. html = soy.esc.$$normalizeHtmlHelper(html); var finalCloseTags = soy.$$balanceTags_(tags); // Now html contains no tags or less-than characters that could become // part of a tag via a replacement operation and tags only contains // approved tags. // Reinsert the white-listed tags. html = html.replace( /\[(\d+)\]/g, function(_, index) { return tags[index]; }); // Close any still open tags. // This prevents unclosed formatting elements like <ol> and <table> from // breaking the layout of containing HTML. return html + finalCloseTags; }; /** * Throw out any close tags that don't correspond to start tags. * If {@code <table>} is used for formatting, embedded HTML shouldn't be able * to use a mismatched {@code </table>} to break page layout. * * @param {Array.<string>} tags an array of tags that will be modified in place * include tags, the empty string, or concatenations of empty tags. * @return {string} zero or more closed tags that close all elements that are * opened in tags but not closed. * @private */ soy.$$balanceTags_ = function(tags) { var open = []; for (var i = 0, n = tags.length; i < n; ++i) { var tag = tags[i]; if (tag.charAt(1) === '/') { var openTagIndex = open.length - 1; // NOTE: This is essentially lastIndexOf, but it's not supported in IE. while (openTagIndex >= 0 && open[openTagIndex] != tag) { openTagIndex--; } if (openTagIndex < 0) { tags[i] = ''; // Drop close tag. } else { tags[i] = open.slice(openTagIndex).reverse().join(''); open.length = openTagIndex; } } else if (!soy.$$HTML5_VOID_ELEMENTS_.test(tag)) { open.push('</' + tag.substring(1)); } } return open.reverse().join(''); }; /** * Escapes HTML special characters in an HTML attribute value. * * @param {*} value The HTML to be escaped. May not be a string, but the * value will be coerced to a string. * @return {string} An escaped version of value. */ soy.$$escapeHtmlAttribute = function(value) { // NOTE: We don't accept ATTRIBUTES here because ATTRIBUTES is actually not // the attribute value context, but instead k/v pairs. if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) { // NOTE: After removing tags, we also escape quotes ("normalize") so that // the HTML can be embedded in attribute context. goog.asserts.assert(value.constructor === soydata.SanitizedHtml); return soy.esc.$$normalizeHtmlHelper(soy.$$stripHtmlTags(value.content)); } return soy.esc.$$escapeHtmlHelper(value); }; /** * Escapes HTML special characters in a string including space and other * characters that can end an unquoted HTML attribute value. * * @param {*} value The HTML to be escaped. May not be a string, but the * value will be coerced to a string. * @return {string} An escaped version of value. */ soy.$$escapeHtmlAttributeNospace = function(value) { if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) { goog.asserts.assert(value.constructor === soydata.SanitizedHtml); return soy.esc.$$normalizeHtmlNospaceHelper( soy.$$stripHtmlTags(value.content)); } return soy.esc.$$escapeHtmlNospaceHelper(value); }; /** * Filters out strings that cannot be a substring of a valid HTML attribute. * * Note the input is expected to be key=value pairs. * * @param {*} value The value to escape. May not be a string, but the value * will be coerced to a string. * @return {string} A valid HTML attribute name part or name/value pair. * {@code "zSoyz"} if the input is invalid. */ soy.$$filterHtmlAttributes = function(value) { // NOTE: Explicitly no support for SanitizedContentKind.HTML, since that is // meaningless in this context, which is generally *between* html attributes. if (soydata.isContentKind(value, soydata.SanitizedContentKind.ATTRIBUTES)) { goog.asserts.assert(value.constructor === soydata.SanitizedHtmlAttribute); // Add a space at the end to ensure this won't get merged into following // attributes, unless the interpretation is unambiguous (ending with quotes // or a space). return value.content.replace(/([^"'\s])$/, '$1 '); } // TODO: Dynamically inserting attributes that aren't marked as trusted is // probably unnecessary. Any filtering done here will either be inadequate // for security or not flexible enough. Having clients use kind="attributes" // in parameters seems like a wiser idea. return soy.esc.$$filterHtmlAttributesHelper(value); }; /** * Filters out strings that cannot be a substring of a valid HTML element name. * * @param {*} value The value to escape. May not be a string, but the value * will be coerced to a string. * @return {string} A valid HTML element name part. * {@code "zSoyz"} if the input is invalid. */ soy.$$filterHtmlElementName = function(value) { // NOTE: We don't accept any SanitizedContent here. HTML indicates valid // PCDATA, not tag names. A sloppy developer shouldn't be able to cause an // exploit: // ... {let userInput}script src=http://evil.com/evil.js{/let} ... // ... {param tagName kind="html"}{$userInput}{/param} ... // ... <{$tagName}>Hello World</{$tagName}> return soy.esc.$$filterHtmlElementNameHelper(value); }; /** * Escapes characters in the value to make it valid content for a JS string * literal. * * @param {*} value The value to escape. May not be a string, but the value * will be coerced to a string. * @return {string} An escaped version of value. * @deprecated */ soy.$$escapeJs = function(value) { return soy.$$escapeJsString(value); }; /** * Escapes characters in the value to make it valid content for a JS string * literal. * * @param {*} value The value to escape. May not be a string, but the value * will be coerced to a string. * @return {string} An escaped version of value. */ soy.$$escapeJsString = function(value) { if (soydata.isContentKind(value, soydata.SanitizedContentKind.JS_STR_CHARS)) { // TODO: It might still be worthwhile to normalize it to remove // unescaped quotes, null, etc: replace(/(?:^|[^\])['"]/g, '\\$ goog.asserts.assert(value.constructor === soydata.SanitizedJsStrChars); return value.content; } return soy.esc.$$escapeJsStringHelper(value); }; /** * Encodes a value as a JavaScript literal. * * @param {*} value The value to escape. May not be a string, but the value * will be coerced to a string. * @return {string} A JavaScript code representation of the input. */ soy.$$escapeJsValue = function(value) { // We surround values with spaces so that they can't be interpolated into // identifiers by accident. // We could use parentheses but those might be interpreted as a function call. if (value == null) { // Intentionally matches undefined. // Java returns null from maps where there is no corresponding key while // JS returns undefined. // We always output null for compatibility with Java which does not have a // distinct undefined value. return ' null '; } if (soydata.isContentKind(value, soydata.SanitizedContentKind.JS)) { goog.asserts.assert(value.constructor === soydata.SanitizedJs); return value.content; } switch (typeof value) { case 'boolean': case 'number': return ' ' + value + ' '; default: return "'" + soy.esc.$$escapeJsStringHelper(String(value)) + "'"; } }; /** * Escapes characters in the string to make it valid content for a JS regular * expression literal. * * @param {*} value The value to escape. May not be a string, but the value * will be coerced to a string. * @return {string} An escaped version of value. */ soy.$$escapeJsRegex = function(value) { return soy.esc.$$escapeJsRegexHelper(value); }; /** * Matches all URI mark characters that conflict with HTML attribute delimiters * or that cannot appear in a CSS uri. * From <a href="http://www.w3.org/TR/CSS2/grammar.html">G.2: CSS grammar</a> * <pre> * url ([!#$%&*-~]|{nonascii}|{escape})* * </pre> * * @type {RegExp} * @private */ soy.$$problematicUriMarks_ = /['()]/g; /** * @param {string} ch A single character in {@link soy.$$problematicUriMarks_}. * @return {string} * @private */ soy.$$pctEncode_ = function(ch) { return '%' + ch.charCodeAt(0).toString(16); }; /** * Escapes a string so that it can be safely included in a URI. * * @param {*} value The value to escape. May not be a string, but the value * will be coerced to a string. * @return {string} An escaped version of value. */ soy.$$escapeUri = function(value) { if (soydata.isContentKind(value, soydata.SanitizedContentKind.URI)) { goog.asserts.assert(value.constructor === soydata.SanitizedUri); return soy.$$normalizeUri(value); } // Apostophes and parentheses are not matched by encodeURIComponent. // They are technically special in URIs, but only appear in the obsolete mark // production in Appendix D.2 of RFC 3986, so can be encoded without changing // semantics. var encoded = soy.esc.$$escapeUriHelper(value); soy.$$problematicUriMarks_.lastIndex = 0; if (soy.$$problematicUriMarks_.test(encoded)) { return encoded.replace(soy.$$problematicUriMarks_, soy.$$pctEncode_); } return encoded; }; /** * Removes rough edges from a URI by escaping any raw HTML/JS string delimiters. * * @param {*} value The value to escape. May not be a string, but the value * will be coerced to a string. * @return {string} An escaped version of value. */ soy.$$normalizeUri = function(value) { return soy.esc.$$normalizeUriHelper(value); }; /** * Vets a URI's protocol and removes rough edges from a URI by escaping * any raw HTML/JS string delimiters. * * @param {*} value The value to escape. May not be a string, but the value * will be coerced to a string. * @return {string} An escaped version of value. */ soy.$$filterNormalizeUri = function(value) { if (soydata.isContentKind(value, soydata.SanitizedContentK