/* * 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. * *
* The top portion of this file contains utilities for Soy users:
* 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 "ac"} is
// semantically distinct from the plain text string {@code "ac"} 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. Important: This function must always be called with a string constant.
*
* 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.
* Escapes HTML special characters so that the value will not prematurely end
* the body of a tag like {@code }.
*
* Will normalize known safe HTML to make sure that sanitized HTML (which could
* contain an innocuous {@code } 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. and
from
// breaking the layout of containing HTML.
return html + finalCloseTags;
};
/**
* Throw out any close tags that don't correspond to start tags.
* If {@code
} is used for formatting, embedded HTML shouldn't be able
* to use a mismatched {@code
} to break page layout.
*
* @param {Array.
* url ([!#$%&*-~]|{nonascii}|{escape})*
*
*
* @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.SanitizedContentKind.URI)) {
goog.asserts.assert(value.constructor === soydata.SanitizedUri);
return soy.$$normalizeUri(value);
}
return soy.esc.$$filterNormalizeUriHelper(value);
};
/**
* Allows only data-protocol image URI's.
*
* @param {*} value The value to process. May not be a string, but the value
* will be coerced to a string.
* @return {!soydata.SanitizedUri} An escaped version of value.
*/
soy.$$filterImageDataUri = function(value) {
// NOTE: Even if it's a SanitizedUri, we will still filter it.
return soydata.VERY_UNSAFE.ordainSanitizedUri(
soy.esc.$$filterImageDataUriHelper(value));
};
/**
* Escapes a string so it can safely be included inside a quoted CSS string.
*
* @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.$$escapeCssString = function(value) {
return soy.esc.$$escapeCssStringHelper(value);
};
/**
* Encodes a value as a CSS identifier part, keyword, or quantity.
*
* @param {*} value The value to escape. May not be a string, but the value
* will be coerced to a string.
* @return {string} A safe CSS identifier part, keyword, or quanitity.
*/
soy.$$filterCssValue = function(value) {
if (soydata.isContentKind(value, soydata.SanitizedContentKind.CSS)) {
goog.asserts.assert(value.constructor === soydata.SanitizedCss);
return value.content;
}
// Uses == to intentionally match null and undefined for Java compatibility.
if (value == null) {
return '';
}
return soy.esc.$$filterCssValueHelper(value);
};
/**
* Sanity-checks noAutoescape input for explicitly tainted content.
*
* SanitizedContentKind.TEXT is used to explicitly mark input that was never
* meant to be used unescaped.
*
* @param {*} value The value to filter.
* @return {*} The value, that we dearly hope will not cause an attack.
*/
soy.$$filterNoAutoescape = function(value) {
if (soydata.isContentKind(value, soydata.SanitizedContentKind.TEXT)) {
// Fail in development mode.
goog.asserts.fail(
'Tainted SanitizedContentKind.TEXT for |noAutoescape: `%s`',
[value.content]);
// Return innocuous data in production.
return 'zSoyz';
}
return value;
};
// -----------------------------------------------------------------------------
// Basic directives/functions.
/**
* Converts \r\n, \r, and \n to
s
* @param {*} value The string in which to convert newlines.
* @return {string|!soydata.SanitizedHtml} A copy of {@code value} with
* converted newlines. If {@code value} is SanitizedHtml, the return value
* is also SanitizedHtml, of the same known directionality.
*/
soy.$$changeNewlineToBr = function(value) {
var result = goog.string.newLineToBr(String(value), false);
if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) {
return soydata.VERY_UNSAFE.ordainSanitizedHtml(
result, soydata.getContentDir(value));
}
return result;
};
/**
* Inserts word breaks ('wbr' tags) into a HTML string at a given interval. The
* counter is reset if a space is encountered. Word breaks aren't inserted into
* HTML tags or entities. Entites count towards the character count; HTML tags
* do not.
*
* @param {*} value The HTML string to insert word breaks into. Can be other
* types, but the value will be coerced to a string.
* @param {number} maxCharsBetweenWordBreaks Maximum number of non-space
* characters to allow before adding a word break.
* @return {string|!soydata.SanitizedHtml} The string including word
* breaks. If {@code value} is SanitizedHtml, the return value
* is also SanitizedHtml, of the same known directionality.
*/
soy.$$insertWordBreaks = function(value, maxCharsBetweenWordBreaks) {
var result = goog.format.insertWordBreaks(
String(value), maxCharsBetweenWordBreaks);
if (soydata.isContentKind(value, soydata.SanitizedContentKind.HTML)) {
return soydata.VERY_UNSAFE.ordainSanitizedHtml(
result, soydata.getContentDir(value));
}
return result;
};
/**
* Truncates a string to a given max length (if it's currently longer),
* optionally adding ellipsis at the end.
*
* @param {*} str The string to truncate. Can be other types, but the value will
* be coerced to a string.
* @param {number} maxLen The maximum length of the string after truncation
* (including ellipsis, if applicable).
* @param {boolean} doAddEllipsis Whether to add ellipsis if the string needs
* truncation.
* @return {string} The string after truncation.
*/
soy.$$truncate = function(str, maxLen, doAddEllipsis) {
str = String(str);
if (str.length <= maxLen) {
return str; // no need to truncate
}
// If doAddEllipsis, either reduce maxLen to compensate, or else if maxLen is
// too small, just turn off doAddEllipsis.
if (doAddEllipsis) {
if (maxLen > 3) {
maxLen -= 3;
} else {
doAddEllipsis = false;
}
}
// Make sure truncating at maxLen doesn't cut up a unicode surrogate pair.
if (soy.$$isHighSurrogate_(str.charAt(maxLen - 1)) &&
soy.$$isLowSurrogate_(str.charAt(maxLen))) {
maxLen -= 1;
}
// Truncate.
str = str.substring(0, maxLen);
// Add ellipsis.
if (doAddEllipsis) {
str += '...';
}
return str;
};
/**
* Private helper for $$truncate() to check whether a char is a high surrogate.
* @param {string} ch The char to check.
* @return {boolean} Whether the given char is a unicode high surrogate.
* @private
*/
soy.$$isHighSurrogate_ = function(ch) {
return 0xD800 <= ch && ch <= 0xDBFF;
};
/**
* Private helper for $$truncate() to check whether a char is a low surrogate.
* @param {string} ch The char to check.
* @return {boolean} Whether the given char is a unicode low surrogate.
* @private
*/
soy.$$isLowSurrogate_ = function(ch) {
return 0xDC00 <= ch && ch <= 0xDFFF;
};
// -----------------------------------------------------------------------------
// Bidi directives/functions.
/**
* Cache of bidi formatter by context directionality, so we don't keep on
* creating new objects.
* @type {!Object.}
* @private
*/
soy.$$bidiFormatterCache_ = {};
/**
* Returns cached bidi formatter for bidiGlobalDir, or creates a new one.
* @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
* if rtl, 0 if unknown.
* @return {!goog.i18n.BidiFormatter} A formatter for bidiGlobalDir.
* @private
*/
soy.$$getBidiFormatterInstance_ = function(bidiGlobalDir) {
return soy.$$bidiFormatterCache_[bidiGlobalDir] ||
(soy.$$bidiFormatterCache_[bidiGlobalDir] =
new goog.i18n.BidiFormatter(bidiGlobalDir));
};
/**
* Estimate the overall directionality of text. If opt_isHtml, makes sure to
* ignore the LTR nature of the mark-up and escapes in text, making the logic
* suitable for HTML and HTML-escaped text.
* If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
* estimating the directionality.
*
* @param {*} text The content whose directionality is to be estimated.
* @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
* Default: false.
* @return {number} 1 if text is LTR, -1 if it is RTL, and 0 if it is neutral.
*/
soy.$$bidiTextDir = function(text, opt_isHtml) {
var contentDir = soydata.getContentDir(text);
if (contentDir != null) {
return contentDir;
}
var isHtml = opt_isHtml ||
soydata.isContentKind(text, soydata.SanitizedContentKind.HTML);
return goog.i18n.bidi.estimateDirection(text + '', isHtml);
};
/**
* Returns 'dir="ltr"' or 'dir="rtl"', depending on text's estimated
* directionality, if it is not the same as bidiGlobalDir.
* Otherwise, returns the empty string.
* If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
* in text, making the logic suitable for HTML and HTML-escaped text.
* If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
* estimating the directionality.
*
* @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
* if rtl, 0 if unknown.
* @param {*} text The content whose directionality is to be estimated.
* @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
* Default: false.
* @return {!soydata.SanitizedHtmlAttribute} 'dir="rtl"' for RTL text in non-RTL
* context; 'dir="ltr"' for LTR text in non-LTR context;
* else, the empty string.
*/
soy.$$bidiDirAttr = function(bidiGlobalDir, text, opt_isHtml) {
var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
var contentDir = soydata.getContentDir(text);
if (contentDir == null) {
var isHtml = opt_isHtml ||
soydata.isContentKind(text, soydata.SanitizedContentKind.HTML);
contentDir = goog.i18n.bidi.estimateDirection(text + '', isHtml);
}
return soydata.VERY_UNSAFE.ordainSanitizedHtmlAttribute(
formatter.knownDirAttr(contentDir));
};
/**
* Returns a Unicode BiDi mark matching bidiGlobalDir (LRM or RLM) if the
* directionality or the exit directionality of text are opposite to
* bidiGlobalDir. Otherwise returns the empty string.
* If opt_isHtml, makes sure to ignore the LTR nature of the mark-up and escapes
* in text, making the logic suitable for HTML and HTML-escaped text.
* If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
* estimating the directionality.
*
* @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
* if rtl, 0 if unknown.
* @param {*} text The content whose directionality is to be estimated.
* @param {boolean=} opt_isHtml Whether text is HTML/HTML-escaped.
* Default: false.
* @return {string} A Unicode bidi mark matching bidiGlobalDir, or the empty
* string when text's overall and exit directionalities both match
* bidiGlobalDir, or bidiGlobalDir is 0 (unknown).
*/
soy.$$bidiMarkAfter = function(bidiGlobalDir, text, opt_isHtml) {
var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
var isHtml = opt_isHtml ||
soydata.isContentKind(text, soydata.SanitizedContentKind.HTML);
return formatter.markAfterKnownDir(soydata.getContentDir(text), text + '',
isHtml);
};
/**
* Returns text wrapped in a according to its
* directionality - but only if that is neither neutral nor the same as the
* global context. Otherwise, returns text unchanged.
* Always treats text as HTML/HTML-escaped, i.e. ignores mark-up and escapes
* when estimating text's directionality.
* If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
* estimating the directionality.
*
* @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
* if rtl, 0 if unknown.
* @param {*} text The string to be wrapped. Can be other types, but the value
* will be coerced to a string.
* @return {!goog.soy.data.SanitizedContent|string} The wrapped text.
*/
soy.$$bidiSpanWrap = function(bidiGlobalDir, text) {
var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
// We always treat the value as HTML, because span-wrapping is only useful
// when its output will be treated as HTML (without escaping), and because
// |bidiSpanWrap is not itself specified to do HTML escaping in Soy. (Both
// explicit and automatic HTML escaping, if any, is done before calling
// |bidiSpanWrap because the BidiSpanWrapDirective Java class implements
// SanitizedContentOperator, but this does not mean that the input has to be
// HTML SanitizedContent. In legacy usage, a string that is not
// SanitizedContent is often printed in an autoescape="false" template or by
// a print with a |noAutoescape, in which case our input is just SoyData.) If
// the output will be treated as HTML, the input had better be safe
// HTML/HTML-escaped (even if it isn't HTML SanitizedData), or we have an XSS
// opportunity and a much bigger problem than bidi garbling.
var wrappedText = formatter.spanWrapWithKnownDir(
soydata.getContentDir(text), text + '', true /* opt_isHtml */);
// Like other directives whose Java class implements SanitizedContentOperator,
// |bidiSpanWrap is called after the escaping (if any) has already been done,
// and thus there is no need for it to produce actual SanitizedContent.
return wrappedText;
};
/**
* Returns text wrapped in Unicode BiDi formatting characters according to its
* directionality, i.e. either LRE or RLE at the beginning and PDF at the end -
* but only if text's directionality is neither neutral nor the same as the
* global context. Otherwise, returns text unchanged.
* Only treats soydata.SanitizedHtml as HTML/HTML-escaped, i.e. ignores mark-up
* and escapes when estimating text's directionality.
* If text has a goog.i18n.bidi.Dir-valued contentDir, this is used instead of
* estimating the directionality.
*
* @param {number} bidiGlobalDir The global directionality context: 1 if ltr, -1
* if rtl, 0 if unknown.
* @param {*} text The string to be wrapped. Can be other types, but the value
* will be coerced to a string.
* @return {!goog.soy.data.SanitizedContent|string} The wrapped string.
*/
soy.$$bidiUnicodeWrap = function(bidiGlobalDir, text) {
var formatter = soy.$$getBidiFormatterInstance_(bidiGlobalDir);
// We treat the value as HTML if and only if it says it's HTML, even though in
// legacy usage, we sometimes have an HTML string (not SanitizedContent) that
// is passed to an autoescape="false" template or a {print $foo|noAutoescape},
// with the output going into an HTML context without escaping. We simply have
// no way of knowing if this is what is happening when we get
// non-SanitizedContent input, and most of the time it isn't.
var isHtml = soydata.isContentKind(text, soydata.SanitizedContentKind.HTML);
var wrappedText = formatter.unicodeWrapWithKnownDir(
soydata.getContentDir(text), text + '', isHtml);
// Bidi-wrapping a value converts it to the context directionality. Since it
// does not cost us anything, we will indicate this known direction in the
// output SanitizedContent, even though the intended consumer of that
// information - a bidi wrapping directive - has already been run.
var wrappedTextDir = formatter.getContextDir();
// Unicode-wrapping UnsanitizedText gives UnsanitizedText.
// Unicode-wrapping safe HTML or JS string data gives valid, safe HTML or JS
// string data.
// ATTENTION: Do these need to be ...ForInternalBlocks()?
if (soydata.isContentKind(text, soydata.SanitizedContentKind.TEXT)) {
return new soydata.UnsanitizedText(wrappedText, wrappedTextDir);
}
if (isHtml) {
return soydata.VERY_UNSAFE.ordainSanitizedHtml(wrappedText, wrappedTextDir);
}
if (soydata.isContentKind(text, soydata.SanitizedContentKind.JS_STR_CHARS)) {
return soydata.VERY_UNSAFE.ordainSanitizedJsStrChars(
wrappedText, wrappedTextDir);
}
// Unicode-wrapping does not conform to the syntax of the other types of
// content. For lack of anything better to do, we we do not declare a content
// kind at all by falling through to the non-SanitizedContent case below.
// TODO(user): Consider throwing a runtime error on receipt of
// SanitizedContent other than TEXT, HTML, or JS_STR_CHARS.
// The input was not SanitizedContent, so our output isn't SanitizedContent
// either.
return wrappedText;
};
// -----------------------------------------------------------------------------
// Generated code.
// START GENERATED CODE FOR ESCAPERS.
/**
* @type {function (*) : string}
*/
soy.esc.$$escapeUriHelper = function(v) {
return goog.string.urlEncode(String(v));
};
/**
* Maps characters to the escaped versions for the named escape directives.
* @type {Object.