/** * @license * Copyright The Closure Library Authors. * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview The SafeStyle type and its builders. * * TODO(xtof): Link to document stating type contract. */ goog.module('goog.html.SafeStyle'); goog.module.declareLegacyNamespace(); const Const = goog.require('goog.string.Const'); const SafeUrl = goog.require('goog.html.SafeUrl'); const TypedString = goog.require('goog.string.TypedString'); const {AssertionError, assert, fail} = goog.require('goog.asserts'); const {contains, endsWith} = goog.require('goog.string.internal'); /** * Token used to ensure that object is created only from this file. No code * outside of this file can access this token. * @type {!Object} * @const */ const CONSTRUCTOR_TOKEN_PRIVATE = {}; /** * A string-like object which represents a sequence of CSS declarations * (`propertyName1: propertyvalue1; propertyName2: propertyValue2; ...`) * and that carries the security type contract that its value, as a string, * will not cause untrusted script execution (XSS) when evaluated as CSS in a * browser. * * Instances of this type must be created via the factory methods * (`SafeStyle.create` or `SafeStyle.fromConstant`) * and not by invoking its constructor. The constructor intentionally takes an * extra parameter that cannot be constructed outside of this file and the type * is immutable; hence only a default instance corresponding to the empty string * can be obtained via constructor invocation. * * SafeStyle's string representation can safely be: *
width: 1em;*
height:1em;*
width: 1em;height: 1em;*
background:url('http://url');
* background: red(missing a trailing semi-colon) *
background:(missing a value and a trailing semi-colon) *
1em(missing an attribute name, which provides context for * the value) *
* var fakeSafeHtml = new String('fake');
* fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
* var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
* // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
* // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml
* // instanceof goog.html.SafeHtml.
*
*
* @return {string}
* @see SafeStyle#unwrap
* @override
*/
getTypedStringValue() {
'use strict';
return this.privateDoNotAccessOrElseSafeStyleWrappedValue_;
}
/**
* Returns a string-representation of this value.
*
* To obtain the actual string value wrapped in a SafeStyle, use
* `SafeStyle.unwrap`.
*
* @return {string}
* @see SafeStyle#unwrap
* @override
*/
toString() {
'use strict';
return this.privateDoNotAccessOrElseSafeStyleWrappedValue_.toString();
}
/**
* Performs a runtime check that the provided object is indeed a
* SafeStyle object, and returns its value.
*
* @param {!SafeStyle} safeStyle The object to extract from.
* @return {string} The safeStyle object's contained string, unless
* the run-time type check fails. In that case, `unwrap` returns an
* innocuous string, or, if assertions are enabled, throws
* `AssertionError`.
*/
static unwrap(safeStyle) {
'use strict';
// Perform additional Run-time type-checking to ensure that
// safeStyle is indeed an instance of the expected type. This
// provides some additional protection against security bugs due to
// application code that disables type checks.
// Specifically, the following checks are performed:
// 1. The object is an instance of the expected type.
// 2. The object is not an instance of a subclass.
if (safeStyle instanceof SafeStyle && safeStyle.constructor === SafeStyle) {
return safeStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
} else {
fail(
`expected object of type SafeStyle, got '${safeStyle}` +
'\' of type ' + goog.typeOf(safeStyle));
return 'type_error:SafeStyle';
}
}
/**
* Package-internal utility method to create SafeStyle instances.
*
* @param {string} style The string to initialize the SafeStyle object with.
* @return {!SafeStyle} The initialized SafeStyle object.
* @package
*/
static createSafeStyleSecurityPrivateDoNotAccessOrElse(style) {
'use strict';
return new SafeStyle(style, CONSTRUCTOR_TOKEN_PRIVATE);
}
/**
* Creates a new SafeStyle object from the properties specified in the map.
* @param {!SafeStyle.PropertyMap} map Mapping of property names to
* their values, for example {'margin': '1px'}. Names must consist of
* [-_a-zA-Z0-9]. Values might be strings consisting of
* [-,.'"%_!# a-zA-Z0-9[\]], where ", ', and [] must be properly balanced.
* We also allow simple functions like rgb() and url() which sanitizes its
* contents. Other values must be wrapped in Const. URLs might
* be passed as SafeUrl which will be wrapped into url(""). We
* also support array whose elements are joined with ' '. Null value
* causes skipping the property.
* @return {!SafeStyle}
* @throws {!Error} If invalid name is provided.
* @throws {!AssertionError} If invalid value is provided. With
* disabled assertions, invalid value is replaced by
* SafeStyle.INNOCUOUS_STRING.
*/
static create(map) {
'use strict';
let style = '';
for (let name in map) {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty#Using_hasOwnProperty_as_a_property_name
if (Object.prototype.hasOwnProperty.call(map, name)) {
if (!/^[-_a-zA-Z0-9]+$/.test(name)) {
throw new Error(`Name allows only [-_a-zA-Z0-9], got: ${name}`);
}
let value = map[name];
if (value == null) {
continue;
}
if (Array.isArray(value)) {
value = value.map(sanitizePropertyValue).join(' ');
} else {
value = sanitizePropertyValue(value);
}
style += `${name}:${value};`;
}
}
if (!style) {
return SafeStyle.EMPTY;
}
return SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(style);
};
/**
* Creates a new SafeStyle object by concatenating the values.
* @param {...(!SafeStyle|!Array)} var_args
* SafeStyles to concatenate.
* @return {!SafeStyle}
*/
static concat(var_args) {
'use strict';
let style = '';
/**
* @param {!SafeStyle|!Array} argument
*/
const addArgument = argument => {
'use strict';
if (Array.isArray(argument)) {
argument.forEach(addArgument);
} else {
style += SafeStyle.unwrap(argument);
}
};
Array.prototype.forEach.call(arguments, addArgument);
if (!style) {
return SafeStyle.EMPTY;
}
return SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(style);
};
}
/**
* A SafeStyle instance corresponding to the empty string.
* @const {!SafeStyle}
*/
SafeStyle.EMPTY = SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse('');
/**
* The innocuous string generated by SafeStyle.create when passed
* an unsafe value.
* @const {string}
*/
SafeStyle.INNOCUOUS_STRING = 'zClosurez';
/**
* A single property value.
* @typedef {string|!Const|!SafeUrl}
*/
SafeStyle.PropertyValue;
/**
* Mapping of property names to their values.
* We don't support numbers even though some values might be numbers (e.g.
* line-height or 0 for any length). The reason is that most numeric values need
* units (e.g. '1px') and allowing numbers could cause users forgetting about
* them.
* @typedef {!Object