/** * @license * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at * http://polymer.github.io/LICENSE.txt * The complete set of authors may be found at * http://polymer.github.io/AUTHORS.txt * The complete set of contributors may be found at * http://polymer.github.io/CONTRIBUTORS.txt * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at * http://polymer.github.io/PATENTS.txt */ /** * @module lit-html */ import {isDirective} from './directive.js'; import {removeNodes} from './dom.js'; import {noChange, nothing, Part} from './part.js'; import {RenderOptions} from './render-options.js'; import {TemplateInstance} from './template-instance.js'; import {TemplateResult} from './template-result.js'; import {createMarker} from './template.js'; // https://tc39.github.io/ecma262/#sec-typeof-operator export type Primitive = null|undefined|boolean|number|string|Symbol|bigint; export const isPrimitive = (value: unknown): value is Primitive => { return ( value === null || !(typeof value === 'object' || typeof value === 'function')); }; export const isIterable = (value: unknown): value is Iterable => { return Array.isArray(value) || // tslint:disable-next-line:no-any !!(value && (value as any)[Symbol.iterator]); }; /** * A global callback used to sanitize any value before it is written into the * DOM. This can be used to implement a security policy of allowed and * disallowed values. * * One way of using this callback would be to check attributes and properties * against a list of high risk fields, and require that values written to such * fields be instances of a class which is safe by construction. Closure's Safe * HTML Types is one implementation of this technique ( * https://github.com/google/safe-html-types/blob/master/doc/safehtml-types.md). * The TrustedTypes polyfill in API-only mode could also be used as a basis * for this technique (https://github.com/WICG/trusted-types). * * @param value The value to sanitize. Will be the actual value passed into the * lit-html template literal, so this could be of any type. * @param name The name of an attribute or property (for example, 'href'). * @param type Indicates whether the write that's about to be performed will * be to a property or a node. * @param node The HTML node (usually either a #text node or an Element) that * is being written to. * @returns The value to write. Typically this is `value`, unless * `value` is determined to be unsafe, in which case a harmless sentinel value * should be returned instead. */ export type DOMSanitizer = (value: unknown, name: string, type: ('property'|'attribute'), node: Node) => unknown; /** * A global callback used to sanitize any value before inserting it into the * DOM. */ let sanitizeDOMValueImpl: DOMSanitizer|undefined; /** Sets the global DOM sanitization callback. */ export const __testOnlySetSanitizeDOMValueExperimentalMayChangeWithoutWarning = (newSanitizer: DOMSanitizer) => { if (sanitizeDOMValueImpl !== undefined) { throw new Error( `Attempted to overwrite existing lit-html security policy.` + ` setSanitizeDOMValue should be called at most once.`); } sanitizeDOMValueImpl = newSanitizer; }; const sanitizeDOMValue: DOMSanitizer = (value: unknown, name: string, type: ('property'|'attribute'), node: Node) => { if (sanitizeDOMValueImpl !== undefined) { return sanitizeDOMValueImpl(value, name, type, node); } return value; }; export const __testOnlyClearSanitizerDoNotCallOrElse = () => { sanitizeDOMValueImpl = undefined; }; /** * Writes attribute values to the DOM for a group of AttributeParts bound to a * single attribute. The value is only set once even if there are multiple parts * for an attribute. */ export class AttributeCommitter { readonly element: Element; readonly name: string; readonly strings: ReadonlyArray; readonly parts: ReadonlyArray; dirty = true; constructor(element: Element, name: string, strings: ReadonlyArray) { this.element = element; this.name = name; this.strings = strings; this.parts = []; for (let i = 0; i < strings.length - 1; i++) { (this.parts as AttributePart[])[i] = this._createPart(); } } /** * Creates a single part. Override this to create a differnt type of part. */ protected _createPart(): AttributePart { return new AttributePart(this); } protected _getValue(): unknown { const strings = this.strings; const parts = this.parts; const l = strings.length - 1; // If we're assigning an attribute via syntax like: // attr="${foo}" or attr=${foo} // but not // attr="${foo} ${bar}" or attr="${foo} baz" // then we don't want to coerce the attribute value into one long // string. Instead we want to just return the value itself directly, // so that sanitizeDOMValue can get the actual value rather than // String(value) // The exception is if v is an array, in which case we do want to smash // it together into a string without calling String() on the array. // // This also allows trusted values (when using TrustedTypes) being // assigned to DOM sinks without being stringified in the process. if (l === 1 && strings[0] === '' && strings[1] === '' && parts[0] !== undefined) { const v = parts[0].value; if (!isIterable(v)) { return v; } } let text = ''; for (let i = 0; i < l; i++) { text += strings[i]; const part = parts[i]; if (part !== undefined) { const v = part.value; if (isPrimitive(v) || !isIterable(v)) { text += typeof v === 'string' ? v : String(v); } else { for (const t of v) { text += typeof t === 'string' ? t : String(t); } } } } text += strings[l]; return text; } commit(): void { if (this.dirty) { this.dirty = false; let value = this._getValue(); value = sanitizeDOMValue(value, this.name, 'attribute', this.element); if (typeof value === 'symbol') { // Native Symbols throw if they're coerced to string. value = String(value); } this.element.setAttribute(this.name, value as string); } } } /** * A Part that controls all or part of an attribute value. */ export class AttributePart implements Part { readonly committer: AttributeCommitter; value: unknown = undefined; constructor(committer: AttributeCommitter) { this.committer = committer; } setValue(value: unknown): void { if (value !== noChange && (!isPrimitive(value) || value !== this.value)) { this.value = value; // If the value is a not a directive, dirty the committer so that it'll // call setAttribute. If the value is a directive, it'll dirty the // committer if it calls setValue(). if (!isDirective(value)) { this.committer.dirty = true; } } } commit() { while (isDirective(this.value)) { const directive = this.value; this.value = noChange; directive(this); } if (this.value === noChange) { return; } this.committer.commit(); } } /** * A Part that controls a location within a Node tree. Like a Range, NodePart * has start and end locations and can set and update the Nodes between those * locations. * * NodeParts support several value types: primitives, Nodes, TemplateResults, * as well as arrays and iterables of those types. */ export class NodePart implements Part { readonly options: RenderOptions; startNode!: Node; endNode!: Node; value: unknown = undefined; private __pendingValue: unknown = undefined; constructor(options: RenderOptions) { this.options = options; } /** * Appends this part into a container. * * This part must be empty, as its contents are not automatically moved. */ appendInto(container: Node) { this.startNode = container.appendChild(createMarker()); this.endNode = container.appendChild(createMarker()); } /** * Inserts this part after the `ref` node (between `ref` and `ref`'s next * sibling). Both `ref` and its next sibling must be static, unchanging nodes * such as those that appear in a literal section of a template. * * This part must be empty, as its contents are not automatically moved. */ insertAfterNode(ref: Node) { this.startNode = ref; this.endNode = ref.nextSibling!; } /** * Appends this part into a parent part. * * This part must be empty, as its contents are not automatically moved. */ appendIntoPart(part: NodePart) { part.__insert(this.startNode = createMarker()); part.__insert(this.endNode = createMarker()); } /** * Inserts this part after the `ref` part. * * This part must be empty, as its contents are not automatically moved. */ insertAfterPart(ref: NodePart) { ref.__insert(this.startNode = createMarker()); this.endNode = ref.endNode; ref.endNode = this.startNode; } setValue(value: unknown): void { this.__pendingValue = value; } commit() { while (isDirective(this.__pendingValue)) { const directive = this.__pendingValue; this.__pendingValue = noChange; directive(this); } const value = this.__pendingValue; if (value === noChange) { return; } if (isPrimitive(value)) { if (value !== this.value) { this.__commitText(value); } } else if (value instanceof TemplateResult) { this.__commitTemplateResult(value); } else if (value instanceof Node) { this.__commitNode(value); } else if (isIterable(value)) { this.__commitIterable(value); } else if (value === nothing) { this.value = nothing; this.clear(); } else { // Fallback, will render the string representation this.__commitText(value); } } private __insert(node: Node) { this.endNode.parentNode!.insertBefore(node, this.endNode); } private __commitNode(value: Node): void { if (this.value === value) { return; } this.clear(); this.__insert(value); this.value = value; } private __commitText(value: unknown): void { const node = this.startNode.nextSibling!; value = value == null ? '' : value; if (node === this.endNode.previousSibling && node.nodeType === 3 /* Node.TEXT_NODE */) { // If we only have a single text node between the markers, we can just // set its value, rather than replacing it. const renderedValue = sanitizeDOMValue(value, 'data', 'property', node); (node as Text).data = typeof renderedValue === 'string' ? renderedValue : String(renderedValue); } else { // When setting text content, for security purposes it matters a lot what // the parent is. For example,