# lib/goog/html/safehtml.js

1 | // Copyright 2013 The Closure Library Authors. All Rights Reserved.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS-IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 |
16 | /**
17 | * @fileoverview The SafeHtml type and its builders.
18 | *
19 | * TODO(xtof): Link to document stating type contract.
20 | */
21 |
22 | goog.provide('goog.html.SafeHtml');
23 |
24 | goog.require('goog.array');
25 | goog.require('goog.asserts');
26 | goog.require('goog.dom.TagName');
27 | goog.require('goog.dom.tags');
28 | goog.require('goog.html.SafeStyle');
29 | goog.require('goog.html.SafeStyleSheet');
30 | goog.require('goog.html.SafeUrl');
31 | goog.require('goog.html.TrustedResourceUrl');
32 | goog.require('goog.i18n.bidi.Dir');
33 | goog.require('goog.i18n.bidi.DirectionalString');
34 | goog.require('goog.object');
35 | goog.require('goog.string');
36 | goog.require('goog.string.Const');
37 | goog.require('goog.string.TypedString');
38 |
39 |
40 |
41 | /**
42 | * A string that is safe to use in HTML context in DOM APIs and HTML documents.
43 | *
44 | * A SafeHtml is a string-like object that carries the security type contract
45 | * that its value as a string will not cause untrusted script execution when
46 | * evaluated as HTML in a browser.
47 | *
48 | * Values of this type are guaranteed to be safe to use in HTML contexts,
49 | * such as, assignment to the innerHTML DOM property, or interpolation into
50 | * a HTML template in HTML PC_DATA context, in the sense that the use will not
51 | * result in a Cross-Site-Scripting vulnerability.
52 | *
53 | * Instances of this type must be created via the factory methods
54 | * ({@code goog.html.SafeHtml.create}, {@code goog.html.SafeHtml.htmlEscape}),
55 | * etc and not by invoking its constructor. The constructor intentionally
56 | * takes no parameters and the type is immutable; hence only a default instance
57 | * corresponding to the empty string can be obtained via constructor invocation.
58 | *
59 | * @see goog.html.SafeHtml#create
60 | * @see goog.html.SafeHtml#htmlEscape
61 | * @constructor
62 | * @final
63 | * @struct
64 | * @implements {goog.i18n.bidi.DirectionalString}
65 | * @implements {goog.string.TypedString}
66 | */
67 | goog.html.SafeHtml = function() {
68 | /**
69 | * The contained value of this SafeHtml. The field has a purposely ugly
70 | * name to make (non-compiled) code that attempts to directly access this
71 | * field stand out.
72 | * @private {string}
73 | */
74 | this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ = '';
75 |
76 | /**
77 | * A type marker used to implement additional run-time type checking.
78 | * @see goog.html.SafeHtml#unwrap
79 | * @const
80 | * @private
81 | */
82 | this.SAFE_HTML_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
83 | goog.html.SafeHtml.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
84 |
85 | /**
86 | * This SafeHtml's directionality, or null if unknown.
87 | * @private {?goog.i18n.bidi.Dir}
88 | */
89 | this.dir_ = null;
90 | };
91 |
92 |
93 | /**
94 | * @override
95 | * @const
96 | */
97 | goog.html.SafeHtml.prototype.implementsGoogI18nBidiDirectionalString = true;
98 |
99 |
100 | /** @override */
101 | goog.html.SafeHtml.prototype.getDirection = function() {
102 | return this.dir_;
103 | };
104 |
105 |
106 | /**
107 | * @override
108 | * @const
109 | */
110 | goog.html.SafeHtml.prototype.implementsGoogStringTypedString = true;
111 |
112 |
113 | /**
114 | * Returns this SafeHtml's value a string.
115 | *
116 | * IMPORTANT: In code where it is security relevant that an object's type is
117 | * indeed {@code SafeHtml}, use {@code goog.html.SafeHtml.unwrap} instead of
118 | * this method. If in doubt, assume that it's security relevant. In particular,
119 | * note that goog.html functions which return a goog.html type do not guarantee
120 | * that the returned instance is of the right type. For example:
121 | *
122 | * <pre>
123 | * var fakeSafeHtml = new String('fake');
124 | * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
125 | * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
126 | * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
127 | * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml
128 | * // instanceof goog.html.SafeHtml.
129 | * </pre>
130 | *
131 | * @see goog.html.SafeHtml#unwrap
132 | * @override
133 | */
134 | goog.html.SafeHtml.prototype.getTypedStringValue = function() {
135 | return this.privateDoNotAccessOrElseSafeHtmlWrappedValue_;
136 | };
137 |
138 |
139 | if (goog.DEBUG) {
140 | /**
141 | * Returns a debug string-representation of this value.
142 | *
143 | * To obtain the actual string value wrapped in a SafeHtml, use
144 | * {@code goog.html.SafeHtml.unwrap}.
145 | *
146 | * @see goog.html.SafeHtml#unwrap
147 | * @override
148 | */
149 | goog.html.SafeHtml.prototype.toString = function() {
150 | return 'SafeHtml{' + this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ +
151 | '}';
152 | };
153 | }
154 |
155 |
156 | /**
157 | * Performs a runtime check that the provided object is indeed a SafeHtml
158 | * object, and returns its value.
159 | * @param {!goog.html.SafeHtml} safeHtml The object to extract from.
160 | * @return {string} The SafeHtml object's contained string, unless the run-time
161 | * type check fails. In that case, {@code unwrap} returns an innocuous
162 | * string, or, if assertions are enabled, throws
163 | * {@code goog.asserts.AssertionError}.
164 | */
165 | goog.html.SafeHtml.unwrap = function(safeHtml) {
166 | // Perform additional run-time type-checking to ensure that safeHtml is indeed
167 | // an instance of the expected type. This provides some additional protection
168 | // against security bugs due to application code that disables type checks.
169 | // Specifically, the following checks are performed:
170 | // 1. The object is an instance of the expected type.
171 | // 2. The object is not an instance of a subclass.
172 | // 3. The object carries a type marker for the expected type. "Faking" an
173 | // object requires a reference to the type marker, which has names intended
174 | // to stand out in code reviews.
175 | if (safeHtml instanceof goog.html.SafeHtml &&
176 | safeHtml.constructor === goog.html.SafeHtml &&
177 | safeHtml.SAFE_HTML_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
178 | goog.html.SafeHtml.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
179 | return safeHtml.privateDoNotAccessOrElseSafeHtmlWrappedValue_;
180 | } else {
181 | goog.asserts.fail('expected object of type SafeHtml, got \'' +
182 | safeHtml + '\'');
183 | return 'type_error:SafeHtml';
184 | }
185 | };
186 |
187 |
188 | /**
189 | * Shorthand for union of types that can sensibly be converted to strings
190 | * or might already be SafeHtml (as SafeHtml is a goog.string.TypedString).
191 | * @private
192 | * @typedef {string|number|boolean|!goog.string.TypedString|
193 | * !goog.i18n.bidi.DirectionalString}
194 | */
195 | goog.html.SafeHtml.TextOrHtml_;
196 |
197 |
198 | /**
199 | * Returns HTML-escaped text as a SafeHtml object.
200 | *
201 | * If text is of a type that implements
202 | * {@code goog.i18n.bidi.DirectionalString}, the directionality of the new
203 | * {@code SafeHtml} object is set to {@code text}'s directionality, if known.
204 | * Otherwise, the directionality of the resulting SafeHtml is unknown (i.e.,
205 | * {@code null}).
206 | *
207 | * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text to escape. If
208 | * the parameter is of type SafeHtml it is returned directly (no escaping
209 | * is done).
210 | * @return {!goog.html.SafeHtml} The escaped text, wrapped as a SafeHtml.
211 | */
212 | goog.html.SafeHtml.htmlEscape = function(textOrHtml) {
213 | if (textOrHtml instanceof goog.html.SafeHtml) {
214 | return textOrHtml;
215 | }
216 | var dir = null;
217 | if (textOrHtml.implementsGoogI18nBidiDirectionalString) {
218 | dir = textOrHtml.getDirection();
219 | }
220 | var textAsString;
221 | if (textOrHtml.implementsGoogStringTypedString) {
222 | textAsString = textOrHtml.getTypedStringValue();
223 | } else {
224 | textAsString = String(textOrHtml);
225 | }
226 | return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
227 | goog.string.htmlEscape(textAsString), dir);
228 | };
229 |
230 |
231 | /**
232 | * Returns HTML-escaped text as a SafeHtml object, with newlines changed to
233 | * &lt;br&gt;.
234 | * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text to escape. If
235 | * the parameter is of type SafeHtml it is returned directly (no escaping
236 | * is done).
237 | * @return {!goog.html.SafeHtml} The escaped text, wrapped as a SafeHtml.
238 | */
239 | goog.html.SafeHtml.htmlEscapePreservingNewlines = function(textOrHtml) {
240 | if (textOrHtml instanceof goog.html.SafeHtml) {
241 | return textOrHtml;
242 | }
243 | var html = goog.html.SafeHtml.htmlEscape(textOrHtml);
244 | return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
245 | goog.string.newLineToBr(goog.html.SafeHtml.unwrap(html)),
246 | html.getDirection());
247 | };
248 |
249 |
250 | /**
251 | * Returns HTML-escaped text as a SafeHtml object, with newlines changed to
252 | * &lt;br&gt; and escaping whitespace to preserve spatial formatting. Character
253 | * entity #160 is used to make it safer for XML.
254 | * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text to escape. If
255 | * the parameter is of type SafeHtml it is returned directly (no escaping
256 | * is done).
257 | * @return {!goog.html.SafeHtml} The escaped text, wrapped as a SafeHtml.
258 | */
259 | goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces = function(
260 | textOrHtml) {
261 | if (textOrHtml instanceof goog.html.SafeHtml) {
262 | return textOrHtml;
263 | }
264 | var html = goog.html.SafeHtml.htmlEscape(textOrHtml);
265 | return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
266 | goog.string.whitespaceEscape(goog.html.SafeHtml.unwrap(html)),
267 | html.getDirection());
268 | };
269 |
270 |
271 | /**
272 | * Coerces an arbitrary object into a SafeHtml object.
273 | *
274 | * If {@code textOrHtml} is already of type {@code goog.html.SafeHtml}, the same
275 | * object is returned. Otherwise, {@code textOrHtml} is coerced to string, and
276 | * HTML-escaped. If {@code textOrHtml} is of a type that implements
277 | * {@code goog.i18n.bidi.DirectionalString}, its directionality, if known, is
278 | * preserved.
279 | *
280 | * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text or SafeHtml to
281 | * coerce.
282 | * @return {!goog.html.SafeHtml} The resulting SafeHtml object.
283 | * @deprecated Use goog.html.SafeHtml.htmlEscape.
284 | */
285 | goog.html.SafeHtml.from = goog.html.SafeHtml.htmlEscape;
286 |
287 |
288 | /**
289 | * @const
290 | * @private
291 | */
292 | goog.html.SafeHtml.VALID_NAMES_IN_TAG_ = /^[a-zA-Z0-9-]+$/;
293 |
294 |
295 | /**
296 | * Set of attributes containing URL as defined at
297 | * http://www.w3.org/TR/html5/index.html#attributes-1.
298 | * @private @const {!Object<string,boolean>}
299 | */
300 | goog.html.SafeHtml.URL_ATTRIBUTES_ = goog.object.createSet('action', 'cite',
301 | 'data', 'formaction', 'href', 'manifest', 'poster', 'src');
302 |
303 |
304 | /**
305 | * Tags which are unsupported via create(). They might be supported via a
306 | * tag-specific create method. These are tags which might require a
307 | * TrustedResourceUrl in one of their attributes or a restricted type for
308 | * their content.
309 | * @private @const {!Object<string,boolean>}
310 | */
311 | goog.html.SafeHtml.NOT_ALLOWED_TAG_NAMES_ = goog.object.createSet(
312 | goog.dom.TagName.EMBED, goog.dom.TagName.IFRAME, goog.dom.TagName.LINK,
313 | goog.dom.TagName.OBJECT, goog.dom.TagName.SCRIPT, goog.dom.TagName.STYLE,
314 | goog.dom.TagName.TEMPLATE);
315 |
316 |
317 | /**
318 | * @typedef {string|number|goog.string.TypedString|
319 | * goog.html.SafeStyle.PropertyMap}
320 | * @private
321 | */
322 | goog.html.SafeHtml.AttributeValue_;
323 |
324 |
325 | /**
326 | * Creates a SafeHtml content consisting of a tag with optional attributes and
327 | * optional content.
328 | *
329 | * For convenience tag names and attribute names are accepted as regular
330 | * strings, instead of goog.string.Const. Nevertheless, you should not pass
331 | * user-controlled values to these parameters. Note that these parameters are
332 | * syntactically validated at runtime, and invalid values will result in
333 | * an exception.
334 | *
335 | * Example usage:
336 | *
337 | * goog.html.SafeHtml.create('br');
338 | * goog.html.SafeHtml.create('div', {'class': 'a'});
339 | * goog.html.SafeHtml.create('p', {}, 'a');
340 | * goog.html.SafeHtml.create('p', {}, goog.html.SafeHtml.create('br'));
341 | *
342 | * goog.html.SafeHtml.create('span', {
343 | * 'style': {'margin': '0'}
344 | * });
345 | *
346 | * To guarantee SafeHtml's type contract is upheld there are restrictions on
347 | * attribute values and tag names.
348 | *
349 | * - For attributes which contain script code (on*), a goog.string.Const is
350 | * required.
351 | * - For attributes which contain style (style), a goog.html.SafeStyle or a
352 | * goog.html.SafeStyle.PropertyMap is required.
353 | * - For attributes which are interpreted as URLs (e.g. src, href) a
354 | * goog.html.SafeUrl, goog.string.Const or string is required. If a string
355 | * is passed, it will be sanitized with SafeUrl.sanitize().
356 | * - For tags which can load code, more specific goog.html.SafeHtml.create*()
357 | * functions must be used. Tags which can load code and are not supported by
358 | * this function are embed, iframe, link, object, script, style, and template.
359 | *
360 | * @param {string} tagName The name of the tag. Only tag names consisting of
361 | * [a-zA-Z0-9-] are allowed. Tag names documented above are disallowed.
362 | * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=}
363 | * opt_attributes Mapping from attribute names to their values. Only
364 | * attribute names consisting of [a-zA-Z0-9-] are allowed. Value of null or
365 | * undefined causes the attribute to be omitted.
366 | * @param {!goog.html.SafeHtml.TextOrHtml_|
367 | * !Array<!goog.html.SafeHtml.TextOrHtml_>=} opt_content Content to
368 | * HTML-escape and put inside the tag. This must be empty for void tags
369 | * like <br>. Array elements are concatenated.
370 | * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
371 | * @throws {Error} If invalid tag name, attribute name, or attribute value is
372 | * provided.
373 | * @throws {goog.asserts.AssertionError} If content for void tag is provided.
374 | */
375 | goog.html.SafeHtml.create = function(tagName, opt_attributes, opt_content) {
376 | if (!goog.html.SafeHtml.VALID_NAMES_IN_TAG_.test(tagName)) {
377 | throw Error('Invalid tag name <' + tagName + '>.');
378 | }
379 | if (tagName.toUpperCase() in goog.html.SafeHtml.NOT_ALLOWED_TAG_NAMES_) {
380 | throw Error('Tag name <' + tagName + '> is not allowed for SafeHtml.');
381 | }
382 | return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
383 | tagName, opt_attributes, opt_content);
384 | };
385 |
386 |
387 | /**
388 | * Creates a SafeHtml representing an iframe tag.
389 | *
390 | * By default the sandbox attribute is set to an empty value, which is the most
391 | * secure option, as it confers the iframe the least privileges. If this
392 | * is too restrictive then granting individual privileges is the preferable
393 | * option. Unsetting the attribute entirely is the least secure option and
394 | * should never be done unless it's stricly necessary.
395 | *
396 | * @param {goog.html.TrustedResourceUrl=} opt_src The value of the src
397 | * attribute. If null or undefined src will not be set.
398 | * @param {goog.html.SafeHtml=} opt_srcdoc The value of the srcdoc attribute.
399 | * If null or undefined srcdoc will not be set.
400 | * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=}
401 | * opt_attributes Mapping from attribute names to their values. Only
402 | * attribute names consisting of [a-zA-Z0-9-] are allowed. Value of null or
403 | * undefined causes the attribute to be omitted.
404 | * @param {!goog.html.SafeHtml.TextOrHtml_|
405 | * !Array<!goog.html.SafeHtml.TextOrHtml_>=} opt_content Content to
406 | * HTML-escape and put inside the tag. Array elements are concatenated.
407 | * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
408 | * @throws {Error} If invalid tag name, attribute name, or attribute value is
409 | * provided. If opt_attributes contains the src or srcdoc attributes.
410 | */
411 | goog.html.SafeHtml.createIframe = function(
412 | opt_src, opt_srcdoc, opt_attributes, opt_content) {
413 | var fixedAttributes = {};
414 | fixedAttributes['src'] = opt_src || null;
415 | fixedAttributes['srcdoc'] = opt_srcdoc || null;
416 | var defaultAttributes = {'sandbox': ''};
417 | var attributes = goog.html.SafeHtml.combineAttributes(
418 | fixedAttributes, defaultAttributes, opt_attributes);
419 | return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
420 | 'iframe', attributes, opt_content);
421 | };
422 |
423 |
424 | /**
425 | * Creates a SafeHtml representing a style tag. The type attribute is set
426 | * to "text/css".
427 | * @param {!goog.html.SafeStyleSheet|!Array<!goog.html.SafeStyleSheet>}
428 | * styleSheet Content to put inside the tag. Array elements are
429 | * concatenated.
430 | * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=}
431 | * opt_attributes Mapping from attribute names to their values. Only
432 | * attribute names consisting of [a-zA-Z0-9-] are allowed. Value of null or
433 | * undefined causes the attribute to be omitted.
434 | * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
435 | * @throws {Error} If invalid attribute name or attribute value is provided. If
436 | * opt_attributes contains the type attribute.
437 | */
438 | goog.html.SafeHtml.createStyle = function(styleSheet, opt_attributes) {
439 | var fixedAttributes = {'type': 'text/css'};
440 | var defaultAttributes = {};
441 | var attributes = goog.html.SafeHtml.combineAttributes(
442 | fixedAttributes, defaultAttributes, opt_attributes);
443 |
444 | var content = '';
445 | styleSheet = goog.array.concat(styleSheet);
446 | for (var i = 0; i < styleSheet.length; i++) {
447 | content += goog.html.SafeStyleSheet.unwrap(styleSheet[i]);
448 | }
449 | // Convert to SafeHtml so that it's not HTML-escaped.
450 | var htmlContent = goog.html.SafeHtml
451 | .createSafeHtmlSecurityPrivateDoNotAccessOrElse(
452 | content, goog.i18n.bidi.Dir.NEUTRAL);
453 | return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
454 | 'style', attributes, htmlContent);
455 | };
456 |
457 |
458 | /**
459 | * @param {string} tagName The tag name.
460 | * @param {string} name The attribute name.
461 | * @param {!goog.html.SafeHtml.AttributeValue_} value The attribute value.
462 | * @return {string} A "name=value" string.
463 | * @throws {Error} If attribute value is unsafe for the given tag and attribute.
464 | * @private
465 | */
466 | goog.html.SafeHtml.getAttrNameAndValue_ = function(tagName, name, value) {
467 | // If it's goog.string.Const, allow any valid attribute name.
468 | if (value instanceof goog.string.Const) {
469 | value = goog.string.Const.unwrap(value);
470 | } else if (name.toLowerCase() == 'style') {
471 | value = goog.html.SafeHtml.getStyleValue_(value);
472 | } else if (/^on/i.test(name)) {
473 | // TODO(jakubvrana): Disallow more attributes with a special meaning.
474 | throw Error('Attribute "' + name +
475 | '" requires goog.string.Const value, "' + value + '" given.');
476 | // URL attributes handled differently accroding to tag.
477 | } else if (name.toLowerCase() in goog.html.SafeHtml.URL_ATTRIBUTES_) {
478 | if (value instanceof goog.html.TrustedResourceUrl) {
479 | value = goog.html.TrustedResourceUrl.unwrap(value);
480 | } else if (value instanceof goog.html.SafeUrl) {
481 | value = goog.html.SafeUrl.unwrap(value);
482 | } else if (goog.isString(value)) {
483 | value = goog.html.SafeUrl.sanitize(value).getTypedStringValue();
484 | } else {
485 | throw Error('Attribute "' + name + '" on tag "' + tagName +
486 | '" requires goog.html.SafeUrl, goog.string.Const, or string,' +
487 | ' value "' + value + '" given.');
488 | }
489 | }
490 |
491 | // Accept SafeUrl, TrustedResourceUrl, etc. for attributes which only require
492 | // HTML-escaping.
493 | if (value.implementsGoogStringTypedString) {
494 | // Ok to call getTypedStringValue() since there's no reliance on the type
495 | // contract for security here.
496 | value = value.getTypedStringValue();
497 | }
498 |
499 | goog.asserts.assert(goog.isString(value) || goog.isNumber(value),
500 | 'String or number value expected, got ' +
501 | (typeof value) + ' with value: ' + value);
502 | return name + '="' + goog.string.htmlEscape(String(value)) + '"';
503 | };
504 |
505 |
506 | /**
507 | * Gets value allowed in "style" attribute.
508 | * @param {goog.html.SafeHtml.AttributeValue_} value It could be SafeStyle or a
509 | * map which will be passed to goog.html.SafeStyle.create.
510 | * @return {string} Unwrapped value.
511 | * @throws {Error} If string value is given.
512 | * @private
513 | */
514 | goog.html.SafeHtml.getStyleValue_ = function(value) {
515 | if (!goog.isObject(value)) {
516 | throw Error('The "style" attribute requires goog.html.SafeStyle or map ' +
517 | 'of style properties, ' + (typeof value) + ' given: ' + value);
518 | }
519 | if (!(value instanceof goog.html.SafeStyle)) {
520 | // Process the property bag into a style object.
521 | value = goog.html.SafeStyle.create(value);
522 | }
523 | return goog.html.SafeStyle.unwrap(value);
524 | };
525 |
526 |
527 | /**
528 | * Creates a SafeHtml content with known directionality consisting of a tag with
529 | * optional attributes and optional content.
530 | * @param {!goog.i18n.bidi.Dir} dir Directionality.
531 | * @param {string} tagName
532 | * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=} opt_attributes
533 | * @param {!goog.html.SafeHtml.TextOrHtml_|
534 | * !Array<!goog.html.SafeHtml.TextOrHtml_>=} opt_content
535 | * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
536 | */
537 | goog.html.SafeHtml.createWithDir = function(dir, tagName, opt_attributes,
538 | opt_content) {
539 | var html = goog.html.SafeHtml.create(tagName, opt_attributes, opt_content);
540 | html.dir_ = dir;
541 | return html;
542 | };
543 |
544 |
545 | /**
546 | * Creates a new SafeHtml object by concatenating values.
547 | * @param {...(!goog.html.SafeHtml.TextOrHtml_|
548 | * !Array<!goog.html.SafeHtml.TextOrHtml_>)} var_args Values to concatenate.
549 | * @return {!goog.html.SafeHtml}
550 | */
551 | goog.html.SafeHtml.concat = function(var_args) {
552 | var dir = goog.i18n.bidi.Dir.NEUTRAL;
553 | var content = '';
554 |
555 | /**
556 | * @param {!goog.html.SafeHtml.TextOrHtml_|
557 | * !Array<!goog.html.SafeHtml.TextOrHtml_>} argument
558 | */
559 | var addArgument = function(argument) {
560 | if (goog.isArray(argument)) {
561 | goog.array.forEach(argument, addArgument);
562 | } else {
563 | var html = goog.html.SafeHtml.htmlEscape(argument);
564 | content += goog.html.SafeHtml.unwrap(html);
565 | var htmlDir = html.getDirection();
566 | if (dir == goog.i18n.bidi.Dir.NEUTRAL) {
567 | dir = htmlDir;
568 | } else if (htmlDir != goog.i18n.bidi.Dir.NEUTRAL && dir != htmlDir) {
569 | dir = null;
570 | }
571 | }
572 | };
573 |
574 | goog.array.forEach(arguments, addArgument);
575 | return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
576 | content, dir);
577 | };
578 |
579 |
580 | /**
581 | * Creates a new SafeHtml object with known directionality by concatenating the
582 | * values.
583 | * @param {!goog.i18n.bidi.Dir} dir Directionality.
584 | * @param {...(!goog.html.SafeHtml.TextOrHtml_|
585 | * !Array<!goog.html.SafeHtml.TextOrHtml_>)} var_args Elements of array
586 | * arguments would be processed recursively.
587 | * @return {!goog.html.SafeHtml}
588 | */
589 | goog.html.SafeHtml.concatWithDir = function(dir, var_args) {
590 | var html = goog.html.SafeHtml.concat(goog.array.slice(arguments, 1));
591 | html.dir_ = dir;
592 | return html;
593 | };
594 |
595 |
596 | /**
597 | * Type marker for the SafeHtml type, used to implement additional run-time
598 | * type checking.
599 | * @const {!Object}
600 | * @private
601 | */
602 | goog.html.SafeHtml.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};
603 |
604 |
605 | /**
606 | * Package-internal utility method to create SafeHtml instances.
607 | *
608 | * @param {string} html The string to initialize the SafeHtml object with.
609 | * @param {?goog.i18n.bidi.Dir} dir The directionality of the SafeHtml to be
610 | * constructed, or null if unknown.
611 | * @return {!goog.html.SafeHtml} The initialized SafeHtml object.
612 | * @package
613 | */
614 | goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse = function(
615 | html, dir) {
616 | return new goog.html.SafeHtml().initSecurityPrivateDoNotAccessOrElse_(
617 | html, dir);
618 | };
619 |
620 |
621 | /**
622 | * Called from createSafeHtmlSecurityPrivateDoNotAccessOrElse(). This
623 | * method exists only so that the compiler can dead code eliminate static
624 | * fields (like EMPTY) when they're not accessed.
625 | * @param {string} html
626 | * @param {?goog.i18n.bidi.Dir} dir
627 | * @return {!goog.html.SafeHtml}
628 | * @private
629 | */
630 | goog.html.SafeHtml.prototype.initSecurityPrivateDoNotAccessOrElse_ = function(
631 | html, dir) {
632 | this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ = html;
633 | this.dir_ = dir;
634 | return this;
635 | };
636 |
637 |
638 | /**
639 | * Like create() but does not restrict which tags can be constructed.
640 | *
641 | * @param {string} tagName Tag name. Set or validated by caller.
642 | * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=} opt_attributes
643 | * @param {(!goog.html.SafeHtml.TextOrHtml_|
644 | * !Array<!goog.html.SafeHtml.TextOrHtml_>)=} opt_content
645 | * @return {!goog.html.SafeHtml}
646 | * @throws {Error} If invalid or unsafe attribute name or value is provided.
647 | * @throws {goog.asserts.AssertionError} If content for void tag is provided.
648 | * @package
649 | */
650 | goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse =
651 | function(tagName, opt_attributes, opt_content) {
652 | var dir = null;
653 | var result = '<' + tagName;
654 |
655 | if (opt_attributes) {
656 | for (var name in opt_attributes) {
657 | if (!goog.html.SafeHtml.VALID_NAMES_IN_TAG_.test(name)) {
658 | throw Error('Invalid attribute name "' + name + '".');
659 | }
660 | var value = opt_attributes[name];
661 | if (!goog.isDefAndNotNull(value)) {
662 | continue;
663 | }
664 | result += ' ' +
665 | goog.html.SafeHtml.getAttrNameAndValue_(tagName, name, value);
666 | }
667 | }
668 |
669 | var content = opt_content;
670 | if (!goog.isDefAndNotNull(content)) {
671 | content = [];
672 | } else if (!goog.isArray(content)) {
673 | content = [content];
674 | }
675 |
676 | if (goog.dom.tags.isVoidTag(tagName.toLowerCase())) {
677 | goog.asserts.assert(!content.length,
678 | 'Void tag <' + tagName + '> does not allow content.');
679 | result += '>';
680 | } else {
681 | var html = goog.html.SafeHtml.concat(content);
682 | result += '>' + goog.html.SafeHtml.unwrap(html) + '</' + tagName + '>';
683 | dir = html.getDirection();
684 | }
685 |
686 | var dirAttribute = opt_attributes && opt_attributes['dir'];
687 | if (dirAttribute) {
688 | if (/^(ltr|rtl|auto)$/i.test(dirAttribute)) {
689 | // If the tag has the "dir" attribute specified then its direction is
690 | // neutral because it can be safely used in any context.
691 | dir = goog.i18n.bidi.Dir.NEUTRAL;
692 | } else {
693 | dir = null;
694 | }
695 | }
696 |
697 | return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
698 | result, dir);
699 | };
700 |
701 |
702 | /**
703 | * @param {!Object<string, string>} fixedAttributes
704 | * @param {!Object<string, string>} defaultAttributes
705 | * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=}
706 | * opt_attributes Optional attributes passed to create*().
707 | * @return {!Object<string, goog.html.SafeHtml.AttributeValue_>}
708 | * @throws {Error} If opt_attributes contains an attribute with the same name
709 | * as an attribute in fixedAttributes.
710 | * @package
711 | */
712 | goog.html.SafeHtml.combineAttributes = function(
713 | fixedAttributes, defaultAttributes, opt_attributes) {
714 | var combinedAttributes = {};
715 | var name;
716 |
717 | for (name in fixedAttributes) {
718 | goog.asserts.assert(name.toLowerCase() == name, 'Must be lower case');
719 | combinedAttributes[name] = fixedAttributes[name];
720 | }
721 | for (name in defaultAttributes) {
722 | goog.asserts.assert(name.toLowerCase() == name, 'Must be lower case');
723 | combinedAttributes[name] = defaultAttributes[name];
724 | }
725 |
726 | for (name in opt_attributes) {
727 | var nameLower = name.toLowerCase();
728 | if (nameLower in fixedAttributes) {
729 | throw Error('Cannot override "' + nameLower + '" attribute, got "' +
730 | name + '" with value "' + opt_attributes[name] + '"');
731 | }
732 | if (nameLower in defaultAttributes) {
733 | delete combinedAttributes[nameLower];
734 | }
735 | combinedAttributes[name] = opt_attributes[name];
736 | }
737 |
738 | return combinedAttributes;
739 | };
740 |
741 |
742 | /**
743 | * A SafeHtml instance corresponding to the HTML doctype: "<!DOCTYPE html>".
744 | * @const {!goog.html.SafeHtml}
745 | */
746 | goog.html.SafeHtml.DOCTYPE_HTML =
747 | goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
748 | '<!DOCTYPE html>', goog.i18n.bidi.Dir.NEUTRAL);
749 |
750 |
751 | /**
752 | * A SafeHtml instance corresponding to the empty string.
753 | * @const {!goog.html.SafeHtml}
754 | */
755 | goog.html.SafeHtml.EMPTY =
756 | goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
757 | '', goog.i18n.bidi.Dir.NEUTRAL);