diff --git a/package.json b/package.json
index bdc9991183a4..02f59da54aa0 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,7 @@
"@types/semver": "^6.0.2",
"@types/shelljs": "^0.8.6",
"@types/systemjs": "0.19.32",
+ "@types/trusted-types": "^1.0.6",
"@types/yaml": "^1.9.7",
"@types/yargs": "^15.0.5",
"@webcomponents/custom-elements": "^1.1.0",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index ff8c68c4fa6b..5ad13618a512 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -14,6 +14,7 @@ ts_library(
deps = [
"//packages/zone.js/lib:zone_d_ts",
"@npm//@types/hammerjs",
+ "@npm//@types/trusted-types",
],
)
diff --git a/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts
index 8cb3e68310e1..4326c8243931 100644
--- a/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts
+++ b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts
@@ -66,7 +66,7 @@ export class Esm5RenderingFormatter extends EsmRenderingFormatter {
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
const node = translateStatement(
stmt, importManager,
- {downlevelLocalizedStrings: true, downlevelVariableDeclarations: true});
+ {downlevelTaggedTemplates: true, downlevelVariableDeclarations: true});
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
return code;
diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts
index 78121b5dada5..ebe2d9fd4c2a 100644
--- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts
+++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts
@@ -66,7 +66,7 @@ class TestRenderingFormatter implements RenderingFormatter {
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
const node = translateStatement(
stmt, importManager,
- {downlevelLocalizedStrings: this.isEs5, downlevelVariableDeclarations: this.isEs5});
+ {downlevelTaggedTemplates: this.isEs5, downlevelVariableDeclarations: this.isEs5});
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
return `// TRANSPILED\n${code}`;
diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts
index 96ea10d553d1..76ebcef30feb 100644
--- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts
+++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts
@@ -280,7 +280,7 @@ function transformIvySourceFile(
const constants =
constantPool.statements.map(stmt => translateStatement(stmt, importManager, {
recordWrappedNodeExpr,
- downlevelLocalizedStrings: downlevelTranslatedCode,
+ downlevelTaggedTemplates: downlevelTranslatedCode,
downlevelVariableDeclarations: downlevelTranslatedCode,
}));
diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts
index d644ec4cfcc7..7010ca4afebd 100644
--- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts
+++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts
@@ -38,21 +38,21 @@ const BINARY_OPERATORS = new Map([
export type RecordWrappedNodeExprFn = (expr: TExpression) => void;
export interface TranslatorOptions {
- downlevelLocalizedStrings?: boolean;
+ downlevelTaggedTemplates?: boolean;
downlevelVariableDeclarations?: boolean;
recordWrappedNodeExpr?: RecordWrappedNodeExprFn;
}
export class ExpressionTranslatorVisitor implements o.ExpressionVisitor,
o.StatementVisitor {
- private downlevelLocalizedStrings: boolean;
+ private downlevelTaggedTemplates: boolean;
private downlevelVariableDeclarations: boolean;
private recordWrappedNodeExpr: RecordWrappedNodeExprFn;
constructor(
private factory: AstFactory,
private imports: ImportGenerator, options: TranslatorOptions) {
- this.downlevelLocalizedStrings = options.downlevelLocalizedStrings === true;
+ this.downlevelTaggedTemplates = options.downlevelTaggedTemplates === true;
this.downlevelVariableDeclarations = options.downlevelVariableDeclarations === true;
this.recordWrappedNodeExpr = options.recordWrappedNodeExpr || (() => {});
}
@@ -168,6 +168,19 @@ export class ExpressionTranslatorVisitor implements o.E
ast.sourceSpan);
}
+ visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): TExpression {
+ const elements: TemplateElement[] = [];
+ for (let i = 0; i < ast.template.elements.length; i++) {
+ elements.push(createTemplateElement(ast.serializeTemplateElement(i)));
+ }
+ return this.setSourceMapRange(
+ this.createTaggedTemplateFunctionCall(ast.tag.visitExpression(this, context), {
+ elements,
+ expressions: ast.template.expressions.map(e => e.visitExpression(this, context))
+ }),
+ ast.sourceSpan);
+ }
+
visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): TExpression {
return this.factory.createNewExpression(
ast.classExpr.visitExpression(this, context),
@@ -202,13 +215,15 @@ export class ExpressionTranslatorVisitor implements o.E
}
const localizeTag = this.factory.createIdentifier('$localize');
+ return this.setSourceMapRange(
+ this.createTaggedTemplateFunctionCall(localizeTag, {elements, expressions}),
+ ast.sourceSpan);
+ }
- // Now choose which implementation to use to actually create the necessary AST nodes.
- const localizeCall = this.downlevelLocalizedStrings ?
- this.createES5TaggedTemplateFunctionCall(localizeTag, {elements, expressions}) :
- this.factory.createTaggedTemplate(localizeTag, {elements, expressions});
-
- return this.setSourceMapRange(localizeCall, ast.sourceSpan);
+ private createTaggedTemplateFunctionCall(
+ tag: TExpression, template: TemplateLiteral): TExpression {
+ return this.downlevelTaggedTemplates ? this.createES5TaggedTemplateFunctionCall(tag, template) :
+ this.factory.createTaggedTemplate(tag, template);
}
/**
diff --git a/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts
index eb898e05af7e..1ac8954fc671 100644
--- a/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts
+++ b/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts
@@ -98,6 +98,10 @@ export class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor
throw new Error('Method not implemented.');
}
+ visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): never {
+ throw new Error('Method not implemented.');
+ }
+
visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): never {
throw new Error('Method not implemented.');
}
diff --git a/packages/compiler-cli/src/transformers/node_emitter.ts b/packages/compiler-cli/src/transformers/node_emitter.ts
index 333c324d50ad..1183bb24ba9c 100644
--- a/packages/compiler-cli/src/transformers/node_emitter.ts
+++ b/packages/compiler-cli/src/transformers/node_emitter.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LeadingComment, leadingComment, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, LocalizedString, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, TypeofExpr, UnaryOperator, UnaryOperatorExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
+import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LeadingComment, leadingComment, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, LocalizedString, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, TaggedTemplateExpr, ThrowStmt, TryCatchStmt, TypeofExpr, UnaryOperator, UnaryOperatorExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {attachComments} from '../ngtsc/translator';
@@ -544,6 +544,25 @@ export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
expr.args.map(arg => arg.visitExpression(this, null))));
}
+ visitTaggedTemplateExpr(expr: TaggedTemplateExpr): RecordedNode {
+ const elementCount = expr.template.elements.length;
+ const templateSpans: ts.TemplateSpan[] = [];
+ for (let i = 1; i < elementCount; i++) {
+ const element = expr.serializeTemplateElement(i),
+ literal = i < elementCount - 1 ? ts.createTemplateMiddle(element.cooked, element.raw) :
+ ts.createTemplateTail(element.cooked, element.raw);
+ templateSpans.push(ts.createTemplateSpan(
+ expr.template.expressions[i - 1].visitExpression(this, null), literal));
+ }
+ const headElement = expr.serializeTemplateElement(0);
+ return this.postProcess(
+ expr,
+ ts.createTaggedTemplate(
+ expr.tag.visitExpression(this, null), /* typeArguments */ undefined,
+ ts.createTemplateExpression(
+ ts.createTemplateHead(headElement.cooked, headElement.raw), templateSpans)));
+ }
+
visitInstantiateExpr(expr: InstantiateExpr): RecordedNode {
return this.postProcess(
expr,
diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts
index c734932b8856..9529d263ed33 100644
--- a/packages/compiler/src/compiler.ts
+++ b/packages/compiler/src/compiler.ts
@@ -78,7 +78,7 @@ export * from './ml_parser/tags';
export {LexerRange} from './ml_parser/lexer';
export * from './ml_parser/xml_parser';
export {NgModuleCompiler} from './ng_module_compiler';
-export {ArrayType, AssertNotNull, DYNAMIC_TYPE, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, NONE_TYPE, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, STRING_TYPE, TypeofExpr, collectExternalReferences, jsDocComment, leadingComment, LeadingComment, JSDocComment, UnaryOperator, UnaryOperatorExpr, LocalizedString} from './output/output_ast';
+export {ArrayType, AssertNotNull, DYNAMIC_TYPE, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, NONE_TYPE, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, TaggedTemplateExpr, TemplateLiteral, TemplateLiteralElement, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, STRING_TYPE, TypeofExpr, collectExternalReferences, jsDocComment, leadingComment, LeadingComment, JSDocComment, UnaryOperator, UnaryOperatorExpr, LocalizedString} from './output/output_ast';
export {EmitterVisitorContext} from './output/abstract_emitter';
export {JitEvaluator} from './output/output_jit';
export * from './output/ts_emitter';
diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts
index 0046f7cc8790..af5f2f2f4f02 100644
--- a/packages/compiler/src/constant_pool.ts
+++ b/packages/compiler/src/constant_pool.ts
@@ -324,6 +324,7 @@ class KeyVisitor implements o.ExpressionVisitor {
visitWritePropExpr = invalid;
visitInvokeMethodExpr = invalid;
visitInvokeFunctionExpr = invalid;
+ visitTaggedTemplateExpr = invalid;
visitInstantiateExpr = invalid;
visitConditionalExpr = invalid;
visitNotExpr = invalid;
diff --git a/packages/compiler/src/output/abstract_emitter.ts b/packages/compiler/src/output/abstract_emitter.ts
index bbb561bacc8a..7abb1f240798 100644
--- a/packages/compiler/src/output/abstract_emitter.ts
+++ b/packages/compiler/src/output/abstract_emitter.ts
@@ -344,6 +344,17 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
ctx.print(expr, `)`);
return null;
}
+ visitTaggedTemplateExpr(expr: o.TaggedTemplateExpr, ctx: EmitterVisitorContext): any {
+ expr.tag.visitExpression(this, ctx);
+ ctx.print(expr, '`' + expr.serializeTemplateElement(0).raw);
+ for (let i = 1; i < expr.template.elements.length; i++) {
+ ctx.print(expr, '${');
+ expr.template.expressions[i].visitExpression(this, ctx);
+ ctx.print(expr, `}${expr.serializeTemplateElement(i - 1).raw}`);
+ }
+ ctx.print(expr, '`');
+ return null;
+ }
visitWrappedNodeExpr(ast: o.WrappedNodeExpr, ctx: EmitterVisitorContext): any {
throw new Error('Abstract emitter cannot visit WrappedNodeExpr.');
}
diff --git a/packages/compiler/src/output/abstract_js_emitter.ts b/packages/compiler/src/output/abstract_js_emitter.ts
index 7afd8159a033..c89bf9f1383c 100644
--- a/packages/compiler/src/output/abstract_js_emitter.ts
+++ b/packages/compiler/src/output/abstract_js_emitter.ts
@@ -10,6 +10,21 @@
import {AbstractEmitterVisitor, CATCH_ERROR_VAR, CATCH_STACK_VAR, EmitterVisitorContext, escapeIdentifier} from './abstract_emitter';
import * as o from './output_ast';
+/**
+ * In TypeScript, tagged template functions expect a "template object", which is an array of
+ * "cooked" strings plus a `raw` property that contains an array of "raw" strings. This is
+ * typically constructed with a function called `__makeTemplateObject(cooked, raw)`, but it may not
+ * be available in all environments.
+ *
+ * This is a JavaScript polyfill that uses __makeTemplateObject when it available, but otherwise
+ * creates an inline helper with the same functionality.
+ *
+ * In the inline function, if `Object.defineProperty` is available we use that to attach the `raw`
+ * array.
+ */
+const makeTemplateObjectPolyfill =
+ '(this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})';
+
export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
constructor() {
super(false);
@@ -115,6 +130,30 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
}
return null;
}
+ visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, ctx: EmitterVisitorContext): any {
+ // The following convoluted piece of code is effectively the downlevelled equivalent of
+ // ```
+ // tag`...`
+ // ```
+ // which is effectively like:
+ // ```
+ // tag(__makeTemplateObject(cooked, raw), expression1, expression2, ...);
+ // ```
+ ast.tag.visitExpression(this, ctx);
+ ctx.print(ast, `(${makeTemplateObjectPolyfill}(`);
+ const parts: o.CookedRawString[] = [];
+ for (let i = 0; i < ast.template.elements.length; i++) {
+ parts.push(ast.serializeTemplateElement(i));
+ }
+ ctx.print(ast, `[${parts.map(part => escapeIdentifier(part.cooked, false)).join(', ')}], `);
+ ctx.print(ast, `[${parts.map(part => escapeIdentifier(part.raw, false)).join(', ')}])`);
+ ast.template.expressions.forEach(expression => {
+ ctx.print(ast, ', ');
+ expression.visitExpression(this, ctx);
+ });
+ ctx.print(ast, ')');
+ return null;
+ }
visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any {
ctx.print(ast, `function${ast.name ? ' ' + ast.name : ''}(`);
this._visitParams(ast.params, ctx);
@@ -161,19 +200,7 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
// ```
// $localize(__makeTemplateObject(cooked, raw), expression1, expression2, ...);
// ```
- //
- // The `$localize` function expects a "template object", which is an array of "cooked" strings
- // plus a `raw` property that contains an array of "raw" strings.
- //
- // In some environments a helper function called `__makeTemplateObject(cooked, raw)` might be
- // available, in which case we use that. Otherwise we must create our own helper function
- // inline.
- //
- // In the inline function, if `Object.defineProperty` is available we use that to attach the
- // `raw` array.
- ctx.print(
- ast,
- '$localize((this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})(');
+ ctx.print(ast, `$localize(${makeTemplateObjectPolyfill}(`);
const parts = [ast.serializeI18nHead()];
for (let i = 1; i < ast.messageParts.length; i++) {
parts.push(ast.serializeI18nTemplatePart(i));
diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts
index d74bff43891b..dc849f05f0b3 100644
--- a/packages/compiler/src/output/output_ast.ts
+++ b/packages/compiler/src/output/output_ast.ts
@@ -126,20 +126,26 @@ export function nullSafeIsEquivalent(
- base: T[], other: T[]) {
+function areAllEquivalentPredicate(
+ base: T[], other: T[], equivalentPredicate: (baseElement: T, otherElement: T) => boolean) {
const len = base.length;
if (len !== other.length) {
return false;
}
for (let i = 0; i < len; i++) {
- if (!base[i].isEquivalent(other[i])) {
+ if (!equivalentPredicate(base[i], other[i])) {
return false;
}
}
return true;
}
+export function areAllEquivalent(
+ base: T[], other: T[]) {
+ return areAllEquivalentPredicate(
+ base, other, (baseElement: T, otherElement: T) => baseElement.isEquivalent(otherElement));
+}
+
export abstract class Expression {
public type: Type|null;
public sourceSpan: ParseSourceSpan|null;
@@ -468,6 +474,40 @@ export class InvokeFunctionExpr extends Expression {
}
+export class TaggedTemplateExpr extends Expression {
+ constructor(
+ public tag: Expression, public template: TemplateLiteral, type?: Type|null,
+ sourceSpan?: ParseSourceSpan|null) {
+ super(type, sourceSpan);
+ }
+
+ isEquivalent(e: Expression): boolean {
+ return e instanceof TaggedTemplateExpr && this.tag.isEquivalent(e.tag) &&
+ areAllEquivalentPredicate(
+ this.template.elements, e.template.elements, (a, b) => a.text === b.text) &&
+ areAllEquivalent(this.template.expressions, e.template.expressions);
+ }
+
+ isConstant() {
+ return false;
+ }
+
+ visitExpression(visitor: ExpressionVisitor, context: any): any {
+ return visitor.visitTaggedTemplateExpr(this, context);
+ }
+
+ getTemplateElementSourceSpan(elementIndex: number): ParseSourceSpan|null {
+ return this.template.elements[elementIndex].sourceSpan ?? this.sourceSpan;
+ }
+
+ serializeTemplateElement(elementIndex: number): CookedRawString {
+ const templatePart = this.template.elements[elementIndex];
+ return createCookedRawString(
+ '', templatePart.text, this.getTemplateElementSourceSpan(elementIndex));
+ }
+}
+
+
export class InstantiateExpr extends Expression {
constructor(
public classExpr: Expression, public args: Expression[], type?: Type|null,
@@ -510,6 +550,13 @@ export class LiteralExpr extends Expression {
}
}
+export class TemplateLiteral {
+ constructor(public elements: TemplateLiteralElement[], public expressions: Expression[]) {}
+}
+export class TemplateLiteralElement {
+ constructor(public text: string, public sourceSpan?: ParseSourceSpan) {}
+}
+
export abstract class MessagePiece {
constructor(public text: string, public sourceSpan: ParseSourceSpan) {}
}
@@ -953,6 +1000,7 @@ export interface ExpressionVisitor {
visitWritePropExpr(expr: WritePropExpr, context: any): any;
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): any;
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any;
+ visitTaggedTemplateExpr(ast: TaggedTemplateExpr, context: any): any;
visitInstantiateExpr(ast: InstantiateExpr, context: any): any;
visitLiteralExpr(ast: LiteralExpr, context: any): any;
visitLocalizedString(ast: LocalizedString, context: any): any;
@@ -1275,6 +1323,17 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor {
context);
}
+ visitTaggedTemplateExpr(ast: TaggedTemplateExpr, context: any): any {
+ return this.transformExpr(
+ new TaggedTemplateExpr(
+ ast.tag.visitExpression(this, context),
+ new TemplateLiteral(
+ ast.template.elements,
+ ast.template.expressions.map((e) => e.visitExpression(this, context))),
+ ast.type, ast.sourceSpan),
+ context);
+ }
+
visitInstantiateExpr(ast: InstantiateExpr, context: any): any {
return this.transformExpr(
new InstantiateExpr(
@@ -1378,7 +1437,7 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor {
return this.transformExpr(
new CommaExpr(this.visitAllExpressions(ast.parts, context), ast.sourceSpan), context);
}
- visitAllExpressions(exprs: Expression[], context: any): Expression[] {
+ visitAllExpressions(exprs: T[], context: any): T[] {
return exprs.map(expr => expr.visitExpression(this, context));
}
@@ -1524,6 +1583,11 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor
this.visitAllExpressions(ast.args, context);
return this.visitExpression(ast, context);
}
+ visitTaggedTemplateExpr(ast: TaggedTemplateExpr, context: any): any {
+ ast.tag.visitExpression(this, context);
+ this.visitAllExpressions(ast.template.expressions, context);
+ return this.visitExpression(ast, context);
+ }
visitInstantiateExpr(ast: InstantiateExpr, context: any): any {
ast.classExpr.visitExpression(this, context);
this.visitAllExpressions(ast.args, context);
@@ -1808,6 +1872,12 @@ export function ifStmt(
return new IfStmt(condition, thenClause, elseClause, sourceSpan, leadingComments);
}
+export function taggedTemplate(
+ tag: Expression, template: TemplateLiteral, type?: Type|null,
+ sourceSpan?: ParseSourceSpan|null): TaggedTemplateExpr {
+ return new TaggedTemplateExpr(tag, template, type, sourceSpan);
+}
+
export function literal(
value: any, type?: Type|null, sourceSpan?: ParseSourceSpan|null): LiteralExpr {
return new LiteralExpr(value, type, sourceSpan);
diff --git a/packages/compiler/src/output/output_interpreter.ts b/packages/compiler/src/output/output_interpreter.ts
index 72ae1871ca44..2b039fa8787a 100644
--- a/packages/compiler/src/output/output_interpreter.ts
+++ b/packages/compiler/src/output/output_interpreter.ts
@@ -197,6 +197,10 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
return fn.apply(null, args);
}
}
+ visitTaggedTemplateExpr(expr: o.TaggedTemplateExpr, ctx: _ExecutionContext): any {
+ // TODO: Do we need to implement this?
+ return null;
+ }
visitReturnStmt(stmt: o.ReturnStatement, ctx: _ExecutionContext): any {
return new ReturnValue(stmt.value.visitExpression(this, ctx));
}
diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts
index 3ea863e09d71..d0335d196121 100644
--- a/packages/compiler/src/render3/r3_identifiers.ts
+++ b/packages/compiler/src/render3/r3_identifiers.ts
@@ -320,4 +320,7 @@ export class Identifiers {
static sanitizeUrl: o.ExternalReference = {name: 'ɵɵsanitizeUrl', moduleName: CORE};
static sanitizeUrlOrResourceUrl:
o.ExternalReference = {name: 'ɵɵsanitizeUrlOrResourceUrl', moduleName: CORE};
+ static trustHtml: o.ExternalReference = {name: 'ɵɵtrustHtml', moduleName: CORE};
+ static trustScript: o.ExternalReference = {name: 'ɵɵtrustScript', moduleName: CORE};
+ static trustResourceUrl: o.ExternalReference = {name: 'ɵɵtrustResourceUrl', moduleName: CORE};
}
diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts
index eb92a1590c4d..2aaaea135e21 100644
--- a/packages/compiler/src/render3/view/template.ts
+++ b/packages/compiler/src/render3/view/template.ts
@@ -568,7 +568,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
const nonContentSelectAttributes =
ngContent.attributes.filter(attr => attr.name.toLowerCase() !== NG_CONTENT_SELECT_ATTR);
- const attributes = this.getAttributeExpressions(nonContentSelectAttributes, [], []);
+ const attributes =
+ this.getAttributeExpressions(ngContent.name, nonContentSelectAttributes, [], []);
if (attributes.length > 0) {
parameters.push(o.literal(projectionSlotIdx), o.literalArr(attributes));
@@ -635,7 +636,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// add attributes for directive and projection matching purposes
const attributes: o.Expression[] = this.getAttributeExpressions(
- outputAttrs, allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs);
+ element.name, outputAttrs, allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs);
parameters.push(this.addAttrsToConsts(attributes));
// local refs (ex.: )
@@ -867,8 +868,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// prepare attributes parameter (including attributes used for directive matching)
const [i18nStaticAttrs, staticAttrs] = partitionArray(template.attributes, hasI18nMeta);
const attrsExprs: o.Expression[] = this.getAttributeExpressions(
- staticAttrs, template.inputs, template.outputs, undefined /* styles */,
- template.templateAttrs, i18nStaticAttrs);
+ NG_TEMPLATE_TAG_NAME, staticAttrs, template.inputs, template.outputs,
+ undefined /* styles */, template.templateAttrs, i18nStaticAttrs);
parameters.push(this.addAttrsToConsts(attrsExprs));
// local refs (ex.: )
@@ -1285,8 +1286,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
* because those values are intended to always be generated as property instructions.
*/
private getAttributeExpressions(
- renderAttributes: t.TextAttribute[], inputs: t.BoundAttribute[], outputs: t.BoundEvent[],
- styles?: StylingBuilder, templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [],
+ elementName: string, renderAttributes: t.TextAttribute[], inputs: t.BoundAttribute[],
+ outputs: t.BoundEvent[], styles?: StylingBuilder,
+ templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [],
i18nAttrs: (t.BoundAttribute|t.TextAttribute)[] = []): o.Expression[] {
const alreadySeen = new Set();
const attrExprs: o.Expression[] = [];
@@ -1296,7 +1298,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
if (attr.name === NG_PROJECT_AS_ATTR_NAME) {
ngProjectAsAttr = attr;
}
- attrExprs.push(...getAttributeNameLiterals(attr.name), asLiteral(attr.value));
+ attrExprs.push(
+ ...getAttributeNameLiterals(attr.name), trustedConstAttribute(elementName, attr));
});
// Keep ngProjectAs next to the other name, value pairs so we can verify that we match
@@ -2128,6 +2131,33 @@ export function resolveSanitizationFn(context: core.SecurityContext, isAttribute
}
}
+export function resolveTrustFn(context: core.SecurityContext) {
+ switch (context) {
+ case core.SecurityContext.HTML:
+ return o.importExpr(R3.trustHtml);
+ case core.SecurityContext.SCRIPT:
+ return o.importExpr(R3.trustScript);
+ case core.SecurityContext.RESOURCE_URL:
+ return o.importExpr(R3.trustResourceUrl);
+ default:
+ return null;
+ }
+}
+
+function trustedConstAttribute(tagName: string, attr: t.TextAttribute): o.Expression {
+ const value = asLiteral(attr.value);
+ for (const isAttribute of [true, false]) {
+ const trustFn =
+ resolveTrustFn(elementRegistry.securityContext(tagName, attr.name, isAttribute));
+ if (trustFn) {
+ return o.taggedTemplate(
+ trustFn,
+ new o.TemplateLiteral([new o.TemplateLiteralElement(attr.value, attr.valueSpan)], []));
+ }
+ }
+ return value;
+}
+
function isSingleElementTemplate(children: t.Node[]): children is[t.Element] {
return children.length === 1 && children[0] instanceof t.Element;
}
diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts
index 91a0d77b0e81..e193ca454867 100644
--- a/packages/core/src/core_render3_private_export.ts
+++ b/packages/core/src/core_render3_private_export.ts
@@ -290,6 +290,9 @@ export {
ɵɵsanitizeStyle,
ɵɵsanitizeUrl,
ɵɵsanitizeUrlOrResourceUrl,
+ ɵɵtrustHtml,
+ ɵɵtrustResourceUrl,
+ ɵɵtrustScript,
} from './sanitization/sanitization';
export {
noSideEffects as ɵnoSideEffects,
diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts
index 37949652438c..82cf77d3aac8 100644
--- a/packages/core/src/render3/i18n/i18n_parse.ts
+++ b/packages/core/src/render3/i18n/i18n_parse.ts
@@ -10,6 +10,7 @@ import '../../util/ng_i18n_closure_mode';
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../../sanitization/html_sanitizer';
import {getInertBodyHelper} from '../../sanitization/inert_body';
+import {getTrustedTypesPolicy, trustedConstSanitizer} from '../../sanitization/trusted_types';
import {_sanitizeUrl, sanitizeSrcset} from '../../sanitization/url_sanitizer';
import {addAllToArray} from '../../util/array_utils';
import {assertEqual} from '../../util/assert';
@@ -242,7 +243,7 @@ export function i18nAttributesFirstPass(
// Set attributes for Elements only, for other types (like ElementContainer),
// only set inputs below
if (tNode.type === TNodeType.Element) {
- elementAttributeInternal(tNode, lView, attrName, value, null, null);
+ elementAttributeInternal(tNode, lView, attrName, value, trustedConstSanitizer, null);
}
// Check if that attribute is a directive input
const dataValue = tNode.inputs !== null && tNode.inputs[attrName];
@@ -524,7 +525,7 @@ export function parseICUBlock(pattern: string): IcuExpression {
function parseIcuCase(
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
expandoStartIndex: number): IcuCase {
- const inertBodyHelper = getInertBodyHelper(getDocument());
+ const inertBodyHelper = getInertBodyHelper(getDocument(), getTrustedTypesPolicy());
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
if (!inertBodyElement) {
throw new Error('Unable to generate inert body element');
diff --git a/packages/core/src/render3/interfaces/renderer.ts b/packages/core/src/render3/interfaces/renderer.ts
index f2cd45492eed..69e8c9c67c83 100644
--- a/packages/core/src/render3/interfaces/renderer.ts
+++ b/packages/core/src/render3/interfaces/renderer.ts
@@ -80,7 +80,9 @@ export interface ProceduralRenderer3 {
parentNode(node: RNode): RElement|null;
nextSibling(node: RNode): RNode|null;
- setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void;
+ setAttribute(
+ el: RElement, name: string, value: string|TrustedHTML|TrustedScript|TrustedScriptURL,
+ namespace?: string|null): void;
removeAttribute(el: RElement, name: string, namespace?: string|null): void;
addClass(el: RElement, name: string): void;
removeClass(el: RElement, name: string): void;
@@ -88,7 +90,8 @@ export interface ProceduralRenderer3 {
el: RElement, style: string, value: any,
flags?: RendererStyleFlags2|RendererStyleFlags3): void;
removeStyle(el: RElement, style: string, flags?: RendererStyleFlags2|RendererStyleFlags3): void;
- setProperty(el: RElement, name: string, value: any): void;
+ setProperty(el: RElement, name: string|TrustedHTML|TrustedScript|TrustedScriptURL, value: any):
+ void;
setValue(node: RText|RComment, value: string): void;
// TODO(misko): Deprecate in favor of addEventListener/removeEventListener
@@ -157,9 +160,11 @@ export interface RElement extends RNode {
classList: RDomTokenList;
className: string;
textContent: string|null;
- setAttribute(name: string, value: string): void;
+ setAttribute(name: string, value: string|TrustedHTML|TrustedScript|TrustedScriptURL): void;
removeAttribute(name: string): void;
- setAttributeNS(namespaceURI: string, qualifiedName: string, value: string): void;
+ setAttributeNS(
+ namespaceURI: string, qualifiedName: string,
+ value: string|TrustedHTML|TrustedScript|TrustedScriptURL): void;
addEventListener(type: string, listener: EventListener, useCapture?: boolean): void;
removeEventListener(type: string, listener?: EventListener, options?: boolean): void;
diff --git a/packages/core/src/render3/interfaces/sanitization.ts b/packages/core/src/render3/interfaces/sanitization.ts
index 886ff809c956..d911e9b0cebc 100644
--- a/packages/core/src/render3/interfaces/sanitization.ts
+++ b/packages/core/src/render3/interfaces/sanitization.ts
@@ -9,4 +9,5 @@
/**
* Function used to sanitize the value before writing it into the renderer.
*/
-export type SanitizerFn = (value: any, tagName?: string, propName?: string) => string;
+export type SanitizerFn = (value: any, tagName?: string, propName?: string) =>
+ string|TrustedHTML|TrustedScript|TrustedScriptURL;
diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts
index 894fe7711a55..e6a0f58a1ce1 100644
--- a/packages/core/src/render3/jit/environment.ts
+++ b/packages/core/src/render3/jit/environment.ts
@@ -166,4 +166,5 @@ export const angularCoreEnv: {[name: string]: Function} =
'ɵɵsanitizeScript': sanitization.ɵɵsanitizeScript,
'ɵɵsanitizeUrl': sanitization.ɵɵsanitizeUrl,
'ɵɵsanitizeUrlOrResourceUrl': sanitization.ɵɵsanitizeUrlOrResourceUrl,
+ 'ɵɵtrustHtml': sanitization.ɵɵtrustHtml,
}))();
diff --git a/packages/core/src/sanitization/bypass.ts b/packages/core/src/sanitization/bypass.ts
index 0d16a1098487..39d6d946936f 100644
--- a/packages/core/src/sanitization/bypass.ts
+++ b/packages/core/src/sanitization/bypass.ts
@@ -58,8 +58,8 @@ export interface SafeUrl extends SafeValue {}
export interface SafeResourceUrl extends SafeValue {}
-abstract class SafeValueImpl implements SafeValue {
- constructor(public changingThisBreaksApplicationSecurity: string) {}
+abstract class SafeValueImpl implements SafeValue {
+ constructor(public changingThisBreaksApplicationSecurity: string|T) {}
abstract getTypeName(): string;
@@ -69,35 +69,35 @@ abstract class SafeValueImpl implements SafeValue {
}
}
-class SafeHtmlImpl extends SafeValueImpl implements SafeHtml {
+class SafeHtmlImpl extends SafeValueImpl implements SafeHtml {
getTypeName() {
return BypassType.Html;
}
}
-class SafeStyleImpl extends SafeValueImpl implements SafeStyle {
+class SafeStyleImpl extends SafeValueImpl implements SafeStyle {
getTypeName() {
return BypassType.Style;
}
}
-class SafeScriptImpl extends SafeValueImpl implements SafeScript {
+class SafeScriptImpl extends SafeValueImpl implements SafeScript {
getTypeName() {
return BypassType.Script;
}
}
-class SafeUrlImpl extends SafeValueImpl implements SafeUrl {
+class SafeUrlImpl extends SafeValueImpl implements SafeUrl {
getTypeName() {
return BypassType.Url;
}
}
-class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl {
+class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl {
getTypeName() {
return BypassType.ResourceUrl;
}
}
-export function unwrapSafeValue(value: SafeValue): string;
+export function unwrapSafeValue(value: SafeValue): string|T;
export function unwrapSafeValue(value: T): T;
-export function unwrapSafeValue(value: T|SafeValue): T {
+export function unwrapSafeValue(value: any): string|T {
return value instanceof SafeValueImpl ? value.changingThisBreaksApplicationSecurity as any as T :
value as any as T;
}
@@ -137,7 +137,7 @@ export function getSanitizationBypassType(value: any): BypassType|null {
* @param trustedHtml `html` string which needs to be implicitly trusted.
* @returns a `html` which has been branded to be implicitly trusted.
*/
-export function bypassSanitizationTrustHtml(trustedHtml: string): SafeHtml {
+export function bypassSanitizationTrustHtml(trustedHtml: string|TrustedHTML): SafeHtml {
return new SafeHtmlImpl(trustedHtml);
}
/**
@@ -161,7 +161,7 @@ export function bypassSanitizationTrustStyle(trustedStyle: string): SafeStyle {
* @param trustedScript `script` string which needs to be implicitly trusted.
* @returns a `script` which has been branded to be implicitly trusted.
*/
-export function bypassSanitizationTrustScript(trustedScript: string): SafeScript {
+export function bypassSanitizationTrustScript(trustedScript: string|TrustedScript): SafeScript {
return new SafeScriptImpl(trustedScript);
}
/**
@@ -185,6 +185,7 @@ export function bypassSanitizationTrustUrl(trustedUrl: string): SafeUrl {
* @param trustedResourceUrl `url` string which needs to be implicitly trusted.
* @returns a `url` which has been branded to be implicitly trusted.
*/
-export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string): SafeResourceUrl {
+export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string|
+ TrustedScriptURL): SafeResourceUrl {
return new SafeResourceUrlImpl(trustedResourceUrl);
}
diff --git a/packages/core/src/sanitization/html_sanitizer.ts b/packages/core/src/sanitization/html_sanitizer.ts
index ab7bbbe8369d..49a535e8bba5 100644
--- a/packages/core/src/sanitization/html_sanitizer.ts
+++ b/packages/core/src/sanitization/html_sanitizer.ts
@@ -8,6 +8,7 @@
import {isDevMode} from '../util/is_dev_mode';
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
+import {getTrustedTypesPolicy} from './trusted_types';
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
function tagSet(tags: string): {[k: string]: boolean} {
@@ -242,10 +243,10 @@ let inertBodyHelper: InertBodyHelper;
* Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to
* the DOM in a browser environment.
*/
-export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
+export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string|TrustedHTML {
let inertBodyElement: HTMLElement|null = null;
try {
- inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc);
+ inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc, getTrustedTypesPolicy());
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
@@ -274,7 +275,7 @@ export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string
'WARNING: sanitizing HTML stripped some content, see http://g.co/ng/security#xss');
}
- return safeHtml;
+ return getTrustedTypesPolicy()?.createHTML(safeHtml) ?? safeHtml;
} finally {
// In case anything goes wrong, clear out inertElement to reset the entire DOM structure.
if (inertBodyElement) {
diff --git a/packages/core/src/sanitization/inert_body.ts b/packages/core/src/sanitization/inert_body.ts
index 0d7173f01a52..28ed38153e59 100644
--- a/packages/core/src/sanitization/inert_body.ts
+++ b/packages/core/src/sanitization/inert_body.ts
@@ -13,8 +13,10 @@
* Default: DOMParser strategy
* Fallback: InertDocument strategy
*/
-export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper {
- return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc);
+export function getInertBodyHelper(
+ defaultDoc: Document, trustedTypePolicy?: TrustedTypePolicy|null): InertBodyHelper {
+ return isDOMParserAvailable() ? new DOMParserHelper(trustedTypePolicy) :
+ new InertDocumentHelper(defaultDoc, trustedTypePolicy);
}
export interface InertBodyHelper {
@@ -29,6 +31,8 @@ export interface InertBodyHelper {
* This is the default strategy used in browsers that support it.
*/
class DOMParserHelper implements InertBodyHelper {
+ constructor(private readonly trustedTypePolicy?: TrustedTypePolicy|null) {}
+
getInertBodyElement(html: string): HTMLElement|null {
// We add these extra elements to ensure that the rest of the content is parsed as expected
// e.g. leading whitespace is maintained and tags like `` do not get hoisted to the
@@ -36,8 +40,11 @@ class DOMParserHelper implements InertBodyHelper {
// in `html` from consuming the otherwise explicit `
' + html;
try {
- const body = new (window as any).DOMParser().parseFromString(html, 'text/html').body as
- HTMLBodyElement;
+ const body =
+ new (window as any)
+ .DOMParser()
+ .parseFromString(this.trustedTypePolicy?.createHTML(html) ?? html, 'text/html')
+ .body as HTMLBodyElement;
body.removeChild(body.firstChild!);
return body;
} catch {
@@ -54,7 +61,8 @@ class DOMParserHelper implements InertBodyHelper {
class InertDocumentHelper implements InertBodyHelper {
private inertDocument: Document;
- constructor(private defaultDoc: Document) {
+ constructor(
+ private defaultDoc: Document, private readonly trustedTypePolicy?: TrustedTypePolicy|null) {
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
if (this.inertDocument.body == null) {
@@ -71,7 +79,8 @@ class InertDocumentHelper implements InertBodyHelper {
// Prefer using element if supported.
const templateEl = this.inertDocument.createElement('template');
if ('content' in templateEl) {
- templateEl.innerHTML = html;
+ templateEl.innerHTML =
+ (this.trustedTypePolicy?.createHTML(html) ?? html) as unknown as string;
return templateEl;
}
@@ -83,7 +92,7 @@ class InertDocumentHelper implements InertBodyHelper {
// down the line. This has been worked around by creating a new inert `body` and using it as
// the root node in which we insert the HTML.
const inertBody = this.inertDocument.createElement('body');
- inertBody.innerHTML = html;
+ inertBody.innerHTML = (this.trustedTypePolicy?.createHTML(html) ?? html) as unknown as string;
// Support: IE 9-11 only
// strip custom-namespaced attributes on IE<=11
@@ -129,7 +138,9 @@ class InertDocumentHelper implements InertBodyHelper {
*/
export function isDOMParserAvailable() {
try {
- return !!new (window as any).DOMParser().parseFromString('', 'text/html');
+ return !!new (window as any)
+ .DOMParser()
+ .parseFromString(window.trustedTypes?.emptyHTML ?? '', 'text/html');
} catch {
return false;
}
diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts
index b32c1e1c3e4f..5a981c417ae5 100644
--- a/packages/core/src/sanitization/sanitization.ts
+++ b/packages/core/src/sanitization/sanitization.ts
@@ -15,10 +15,10 @@ import {allowSanitizationBypassAndThrow, BypassType, unwrapSafeValue} from './by
import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer';
import {Sanitizer} from './sanitizer';
import {SecurityContext} from './security';
+import {getTrustedTypesPolicy} from './trusted_types';
import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer';
-
/**
* An `html` sanitizer which converts untrusted `html` **string** into trusted string by removing
* dangerous content.
@@ -34,10 +34,11 @@ import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer';
*
* @codeGenApi
*/
-export function ɵɵsanitizeHtml(unsafeHtml: any): string {
+export function ɵɵsanitizeHtml(unsafeHtml: any): string|TrustedHTML {
const sanitizer = getSanitizer();
if (sanitizer) {
- return sanitizer.sanitize(SecurityContext.HTML, unsafeHtml) || '';
+ return sanitizer.sanitize(SecurityContext.HTML, unsafeHtml) ||
+ (window.trustedTypes?.emptyHTML ?? '');
}
if (allowSanitizationBypassAndThrow(unsafeHtml, BypassType.Html)) {
return unwrapSafeValue(unsafeHtml);
@@ -105,7 +106,7 @@ export function ɵɵsanitizeUrl(unsafeUrl: any): string {
*
* @codeGenApi
*/
-export function ɵɵsanitizeResourceUrl(unsafeResourceUrl: any): string {
+export function ɵɵsanitizeResourceUrl(unsafeResourceUrl: any): string|TrustedScriptURL {
const sanitizer = getSanitizer();
if (sanitizer) {
return sanitizer.sanitize(SecurityContext.RESOURCE_URL, unsafeResourceUrl) || '';
@@ -128,10 +129,11 @@ export function ɵɵsanitizeResourceUrl(unsafeResourceUrl: any): string {
*
* @codeGenApi
*/
-export function ɵɵsanitizeScript(unsafeScript: any): string {
+export function ɵɵsanitizeScript(unsafeScript: any): string|TrustedScript {
const sanitizer = getSanitizer();
if (sanitizer) {
- return sanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript) || '';
+ return sanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript) ||
+ (window.trustedTypes?.emptyScript ?? '');
}
if (allowSanitizationBypassAndThrow(unsafeScript, BypassType.Script)) {
return unwrapSafeValue(unsafeScript);
@@ -139,6 +141,27 @@ export function ɵɵsanitizeScript(unsafeScript: any): string {
throw new Error('unsafe value used in a script context');
}
+export function ɵɵtrustHtml(html: TemplateStringsArray): string|TrustedHTML {
+ if (html.length !== 1) {
+ throw new Error('unexpected interpolation in trustHtml');
+ }
+ return getTrustedTypesPolicy()?.createHTML(html[0]) ?? html[0];
+}
+
+export function ɵɵtrustScript(script: TemplateStringsArray): string|TrustedScript {
+ if (script.length !== 1) {
+ throw new Error('unexpected interpolation in trustScript');
+ }
+ return getTrustedTypesPolicy()?.createScript(script[0]) ?? script[0];
+}
+
+export function ɵɵtrustResourceUrl(url: TemplateStringsArray): string|TrustedScriptURL {
+ if (url.length !== 1) {
+ throw new Error('unexpected interpolation in trustResourceUrl');
+ }
+ return getTrustedTypesPolicy()?.createScriptURL(url[0]) ?? url[0];
+}
+
/**
* Detects which sanitizer to use for URL property, based on tag name and prop name.
*
diff --git a/packages/core/src/sanitization/sanitizer.ts b/packages/core/src/sanitization/sanitizer.ts
index 026813cb7c80..e6344a829594 100644
--- a/packages/core/src/sanitization/sanitizer.ts
+++ b/packages/core/src/sanitization/sanitizer.ts
@@ -15,7 +15,15 @@ import {SecurityContext} from './security';
* @publicApi
*/
export abstract class Sanitizer {
+ abstract sanitize(context: SecurityContext.HTML, value: {}|string|null): string|TrustedHTML|null;
+ abstract sanitize(context: SecurityContext.SCRIPT, value: {}|string|null): string|TrustedScript
+ |null;
+ abstract sanitize(context: SecurityContext.RESOURCE_URL, value: {}|string|null): string
+ |TrustedScriptURL|null;
abstract sanitize(context: SecurityContext, value: {}|string|null): string|null;
+ abstract sanitize(context: SecurityContext, value: {}|string|null): string|TrustedHTML
+ |TrustedScript|TrustedScriptURL|null;
+
/** @nocollapse */
static ɵprov = ɵɵdefineInjectable({
token: Sanitizer,
diff --git a/packages/core/src/sanitization/trusted_types.ts b/packages/core/src/sanitization/trusted_types.ts
new file mode 100644
index 000000000000..c571ec3077b1
--- /dev/null
+++ b/packages/core/src/sanitization/trusted_types.ts
@@ -0,0 +1,52 @@
+
+/**
+ * The Trusted Types policy used by Angular, or null if Trusted Types are not
+ * enabled/supported, or undefined if the policy has not been created yet.
+ */
+let trustedTypesPolicy: TrustedTypePolicy|null|undefined;
+
+/**
+ * Returns the Trusted Types policy used by Angular, or null if Trusted Types
+ * are not enabled/supported. The first call to this function will create the
+ * policy.
+ */
+export function getTrustedTypesPolicy(): TrustedTypePolicy|null {
+ if (trustedTypesPolicy === undefined) {
+ trustedTypesPolicy = null;
+ if (typeof window !== 'undefined') {
+ try {
+ trustedTypesPolicy = window.trustedTypes?.createPolicy('angular', {
+ createHTML: (s: string) => s,
+ createScript: (s: string) => s,
+ createScriptURL: (s: string) => s
+ }) ??
+ null;
+ } catch (e) {
+ // trustedTypes.createPolicy throws if called with a name that is already
+ // registered, even in report-only mode. Until the API changes, catch the
+ // error not to break the applications functionally. In such case, the
+ // code will fall back to using strings.
+ console.log(e);
+ }
+ }
+ }
+ return trustedTypesPolicy;
+}
+
+export function trustedConstSanitizer(value: any, tagName?: string, propName?: string): string|
+ TrustedHTML|TrustedScript|TrustedScriptURL {
+ if (tagName && propName && typeof window !== 'undefined' && window.trustedTypes) {
+ const type = window.trustedTypes.getAttributeType(tagName, propName) ||
+ window.trustedTypes.getPropertyType(tagName, propName);
+ if (type === 'TrustedHTML') {
+ return getTrustedTypesPolicy()?.createHTML(value) ?? value;
+ }
+ if (type === 'TrustedScript') {
+ return getTrustedTypesPolicy()?.createScript(value) ?? value;
+ }
+ if (type === 'TrustedScriptURL') {
+ return getTrustedTypesPolicy()?.createScriptURL(value) ?? value;
+ }
+ }
+ return value;
+}
diff --git a/packages/core/src/util/dev_trusted_types.ts b/packages/core/src/util/dev_trusted_types.ts
new file mode 100644
index 000000000000..d511c5223022
--- /dev/null
+++ b/packages/core/src/util/dev_trusted_types.ts
@@ -0,0 +1,81 @@
+
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import './ng_dev_mode';
+
+/**
+ * THIS FILE CONTAINS CODE WHICH SHOULD BE TREE SHAKEN AND NEVER CALLED FROM PRODUCTION CODE!!!
+ */
+
+/**
+ * The Trusted Types policy used by Angular, or null if Trusted Types are not
+ * enabled/supported, or undefined if the policy has not been created yet.
+ */
+let trustedTypesPolicyForDev: TrustedTypePolicy|null|undefined;
+
+/**
+ * Returns the Trusted Types policy used by Angular, or null if Trusted Types
+ * are not enabled/supported. The first call to this function will create the
+ * policy.
+ */
+function getTrustedTypesPolicyForDev(): TrustedTypePolicy|null {
+ if (!ngDevMode) {
+ throw new Error(
+ 'Looks like we are in \'prod mode\', but we are creating a Trusted Types policy for development, which is wrong! Check your code');
+ }
+ if (trustedTypesPolicyForDev === undefined) {
+ trustedTypesPolicyForDev = null;
+ if (typeof window !== 'undefined') {
+ try {
+ trustedTypesPolicyForDev =
+ window?.trustedTypes?.createPolicy('angular#only-for-development', {
+ createHTML: (s: string) => s,
+ createScript: (s: string) => s,
+ createScriptURL: (s: string) => s
+ }) ??
+ null;
+ } catch (e) {
+ // trustedTypes.createPolicy throws if called with a name that is already
+ // registered, even in report-only mode. Until the API changes, catch the
+ // error not to break the applications functionally. In such case, the
+ // code will fall back to using strings.
+ console.log(e);
+ }
+ }
+ }
+ return trustedTypesPolicyForDev;
+}
+
+export function trustedHTMLForDev(html: string): string|TrustedHTML {
+ return getTrustedTypesPolicyForDev()?.createHTML(html) ?? html;
+}
+
+export function trustedScriptForDev(script: string): string|TrustedScript {
+ return getTrustedTypesPolicyForDev()?.createScript(script) ?? script;
+}
+
+export function trustedScriptURLForDev(url: string): string|TrustedScriptURL {
+ return getTrustedTypesPolicyForDev()?.createScriptURL(url) ?? url;
+}
+
+export function trustedFunctionForDev(...args: string[]): Function {
+ // return new Function(...args.map((a) => {
+ // return trustedScriptForTest(a) as string;
+ // }));
+
+ // Workaround for a Trusted Type bug in Chrome 83.
+ const fnArgs = args.slice(0, -1).join(',');
+ const fnBody = args.pop()!.toString();
+ const body = `(function anonymous(${fnArgs}
+) { ${fnBody}
+}).bind(globalThis)`;
+ const res = eval(trustedScriptForDev(body) as string) as Function;
+ res.toString = () => body; // To fix sourcemaps
+ return res;
+}
diff --git a/packages/core/src/util/named_array_type.ts b/packages/core/src/util/named_array_type.ts
index b4a90c7d1598..6810a4b2117d 100644
--- a/packages/core/src/util/named_array_type.ts
+++ b/packages/core/src/util/named_array_type.ts
@@ -8,6 +8,7 @@
*/
import './ng_dev_mode';
+import {trustedFunctionForDev} from './dev_trusted_types';
/**
* THIS FILE CONTAINS CODE WHICH SHOULD BE TREE SHAKEN AND NEVER CALLED FROM PRODUCTION CODE!!!
@@ -26,10 +27,12 @@ import './ng_dev_mode';
export function createNamedArrayType(name: string): typeof Array {
// This should never be called in prod mode, so let's verify that is the case.
if (ngDevMode) {
+ // const body = trustedScriptForDev(`return class ${name} extends Array{}`);
try {
// We need to do it this way so that TypeScript does not down-level the below code.
- const FunctionConstructor: any = createNamedArrayType.constructor;
- return (new FunctionConstructor('Array', `return class ${name} extends Array{}`))(Array);
+ // const FunctionConstructor: any = createNamedArrayType.constructor;
+ // return (new FunctionConstructor('Array', body))(Array);
+ return trustedFunctionForDev('Array', `return class ${name} extends Array{}`)(Array);
} catch (e) {
// If it does not work just give up and fall back to regular Array.
return Array;
diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts
index 192158364f54..d1d519319b3e 100644
--- a/packages/core/test/render3/instructions_spec.ts
+++ b/packages/core/test/render3/instructions_spec.ts
@@ -8,11 +8,12 @@
import {NgForOfContext} from '@angular/common';
import {getSortedClassName} from '@angular/core/testing/src/styling';
+import {trustedHTMLForTest, trustedScriptForTest, trustedScriptURLForTest} from '@angular/core/testing';
import {ɵɵdefineComponent} from '../../src/render3/definition';
import {RenderFlags, ɵɵadvance, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵstyleMap, ɵɵstyleProp, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
import {AttributeMarker} from '../../src/render3/interfaces/node';
-import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, SafeValue, unwrapSafeValue} from '../../src/sanitization/bypass';
+import {allowSanitizationBypassAndThrow, BypassType, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, SafeValue, unwrapSafeValue} from '../../src/sanitization/bypass';
import {ɵɵsanitizeHtml, ɵɵsanitizeResourceUrl, ɵɵsanitizeScript, ɵɵsanitizeStyle, ɵɵsanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer} from '../../src/sanitization/sanitizer';
import {SecurityContext} from '../../src/sanitization/security';
@@ -361,7 +362,7 @@ describe('instructions', () => {
});
it('should work for resourceUrl sanitization', () => {
- const s = new LocalMockSanitizer(value => `${value}-sanitized`);
+ const s = new LocalMockSanitizer(value => trustedScriptURLForTest(`${value}-sanitized`));
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s);
const inputValue = 'http://resource';
const outputValue = 'http://resource-sanitized';
@@ -376,7 +377,7 @@ describe('instructions', () => {
it('should bypass resourceUrl sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s);
- const inputValue = s.bypassSecurityTrustResourceUrl('file://all-my-secrets.pdf');
+ const inputValue = s.bypassSecurityTrustResourceUrl(trustedScriptURLForTest('file://all-my-secrets.pdf'));
const outputValue = 'file://all-my-secrets.pdf';
t.update(() => {
@@ -389,7 +390,7 @@ describe('instructions', () => {
it('should bypass ivy-level resourceUrl sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s);
- const inputValue = bypassSanitizationTrustResourceUrl('file://all-my-secrets.pdf');
+ const inputValue = bypassSanitizationTrustResourceUrl(trustedScriptURLForTest('file://all-my-secrets.pdf'));
const outputValue = 'file://all-my-secrets.pdf-ivy';
t.update(() => {
@@ -400,13 +401,13 @@ describe('instructions', () => {
});
it('should work for script sanitization', () => {
- const s = new LocalMockSanitizer(value => `${value} //sanitized`);
+ const s = new LocalMockSanitizer(value => trustedScriptForTest(`${value} //sanitized`));
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s);
const inputValue = 'fn();';
const outputValue = 'fn(); //sanitized';
t.update(() => {
- ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript);
+ ɵɵproperty('textContent', inputValue, ɵɵsanitizeScript);
});
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toEqual(outputValue);
@@ -415,11 +416,11 @@ describe('instructions', () => {
it('should bypass script sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s);
- const inputValue = s.bypassSecurityTrustScript('alert("bar")');
+ const inputValue = s.bypassSecurityTrustScript(trustedScriptForTest('alert("bar")'));
const outputValue = 'alert("bar")';
t.update(() => {
- ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript);
+ ɵɵproperty('textContent', inputValue, ɵɵsanitizeScript);
});
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toBeFalsy();
@@ -428,18 +429,18 @@ describe('instructions', () => {
it('should bypass ivy-level script sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, 1, 1, null, null, s);
- const inputValue = bypassSanitizationTrustScript('alert("bar")');
+ const inputValue = bypassSanitizationTrustScript(trustedScriptForTest('alert("bar")'));
const outputValue = 'alert("bar")-ivy';
t.update(() => {
- ɵɵproperty('innerHTML', inputValue, ɵɵsanitizeScript);
+ ɵɵproperty('textContent', inputValue, ɵɵsanitizeScript);
});
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should work for html sanitization', () => {
- const s = new LocalMockSanitizer(value => `${value} `);
+ const s = new LocalMockSanitizer(value => trustedHTMLForTest(`${value} `));
const t = new TemplateFixture(createDiv, undefined, 1, 1, null, null, s);
const inputValue = '';
const outputValue = ' ';
@@ -454,7 +455,7 @@ describe('instructions', () => {
it('should bypass html sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createDiv, undefined, 1, 1, null, null, s);
- const inputValue = s.bypassSecurityTrustHtml('');
+ const inputValue = s.bypassSecurityTrustHtml(trustedHTMLForTest(''));
const outputValue = '';
t.update(() => {
@@ -467,7 +468,7 @@ describe('instructions', () => {
it('should bypass ivy-level script sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createDiv, undefined, 1, 1, null, null, s);
- const inputValue = bypassSanitizationTrustHtml('');
+ const inputValue = bypassSanitizationTrustHtml(trustedHTMLForTest(''));
const outputValue = '-ivy';
t.update(() => {
@@ -480,7 +481,7 @@ describe('instructions', () => {
});
class LocalSanitizedValue {
- constructor(public value: any) {}
+ constructor(public value: string|TrustedHTML|TrustedScript|TrustedScriptURL) {}
toString() {
return this.value;
@@ -491,21 +492,48 @@ class LocalMockSanitizer implements Sanitizer {
// TODO(issue/24571): remove '!'.
public lastSanitizedValue!: string|null;
- constructor(private _interceptor: (value: string|null|any) => string) {}
+ constructor(private _interceptor: (value: string|null|any) => string|TrustedHTML|TrustedScript|TrustedScriptURL) {}
- sanitize(context: SecurityContext, value: LocalSanitizedValue|string|null|any): string|null {
- if (getSanitizationBypassType(value) != null) {
- return unwrapSafeValue(value) + '-ivy';
+ sanitize(context: SecurityContext.HTML, value: {}|string|null): string|TrustedHTML|null;
+ sanitize(context: SecurityContext.SCRIPT, value: {}|string|null): string|TrustedScript|null;
+ sanitize(context: SecurityContext.RESOURCE_URL, value: {}|string|null): string
+ |TrustedScriptURL|null;
+ sanitize(context: SecurityContext, value: {}|string|null): string|null;
+ sanitize(context: SecurityContext, value: LocalSanitizedValue|string|null|any): string|TrustedHTML
+ |TrustedScript|TrustedScriptURL|null {
+
+ // TODO: Make sure unwrapped values are wrapped in the same trusted type (or not at all)
+ if (context === SecurityContext.HTML &&
+ allowSanitizationBypassAndThrow(value, BypassType.Html)) {
+ return trustedHTMLForTest(unwrapSafeValue(value).toString() + '-ivy');
+ }
+ if (context === SecurityContext.SCRIPT &&
+ allowSanitizationBypassAndThrow(value, BypassType.Script)) {
+ return trustedScriptForTest(unwrapSafeValue(value).toString() + '-ivy');
+ }
+ if (context === SecurityContext.STYLE &&
+ allowSanitizationBypassAndThrow(value, BypassType.Style)) {
+ return unwrapSafeValue(value).toString() + '-ivy';
+ }
+ if (context === SecurityContext.URL &&
+ allowSanitizationBypassAndThrow(value, BypassType.Url)) {
+ return unwrapSafeValue(value).toString() + '-ivy';
+ }
+ if (context === SecurityContext.RESOURCE_URL &&
+ allowSanitizationBypassAndThrow(value, BypassType.ResourceUrl)) {
+ return trustedScriptURLForTest(unwrapSafeValue(value).toString() + '-ivy');
}
if (value instanceof LocalSanitizedValue) {
- return value.toString();
+ return value.value;
}
- return this.lastSanitizedValue = this._interceptor(value);
+ const sanitizedValue = this._interceptor(value);
+ this.lastSanitizedValue = sanitizedValue.toString();
+ return sanitizedValue;
}
- bypassSecurityTrustHtml(value: string) {
+ bypassSecurityTrustHtml(value: string|TrustedHTML) {
return new LocalSanitizedValue(value);
}
@@ -513,7 +541,7 @@ class LocalMockSanitizer implements Sanitizer {
return new LocalSanitizedValue(value);
}
- bypassSecurityTrustScript(value: string) {
+ bypassSecurityTrustScript(value: string|TrustedScript) {
return new LocalSanitizedValue(value);
}
@@ -521,7 +549,7 @@ class LocalMockSanitizer implements Sanitizer {
return new LocalSanitizedValue(value);
}
- bypassSecurityTrustResourceUrl(value: string) {
+ bypassSecurityTrustResourceUrl(value: string|TrustedScriptURL) {
return new LocalSanitizedValue(value);
}
}
diff --git a/packages/core/test/sanitization/html_sanitizer_spec.ts b/packages/core/test/sanitization/html_sanitizer_spec.ts
index d577ce2c4dc7..4e97112182e5 100644
--- a/packages/core/test/sanitization/html_sanitizer_spec.ts
+++ b/packages/core/test/sanitization/html_sanitizer_spec.ts
@@ -29,73 +29,79 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
});
it('serializes nested structures', () => {
- expect(_sanitizeHtml(defaultDoc, ''))
+ expect(_sanitizeHtml(defaultDoc, '')
+ .toString())
.toEqual('');
expect(logMsgs).toEqual([]);
});
it('serializes self closing elements', () => {
- expect(_sanitizeHtml(defaultDoc, 'Hello
World
'))
+ expect(_sanitizeHtml(defaultDoc, 'Hello
World
').toString())
.toEqual('Hello
World
');
});
it('supports namespaced elements', () => {
- expect(_sanitizeHtml(defaultDoc, 'abc')).toEqual('abc');
+ expect(_sanitizeHtml(defaultDoc, 'abc').toString()).toEqual('abc');
});
it('supports namespaced attributes', () => {
- expect(_sanitizeHtml(defaultDoc, 't'))
+ expect(_sanitizeHtml(defaultDoc, 't').toString())
.toEqual('t');
- expect(_sanitizeHtml(defaultDoc, 't')).toEqual('t');
- expect(_sanitizeHtml(defaultDoc, 't'))
+ expect(_sanitizeHtml(defaultDoc, 't').toString())
+ .toEqual('t');
+ expect(_sanitizeHtml(defaultDoc, 't').toString())
.toEqual('t');
});
it('supports HTML5 elements', () => {
- expect(_sanitizeHtml(defaultDoc, 'Works'))
+ expect(_sanitizeHtml(defaultDoc, 'Works').toString())
.toEqual('Works');
});
it('supports ARIA attributes', () => {
- expect(_sanitizeHtml(defaultDoc, 'Test
'))
+ expect(_sanitizeHtml(defaultDoc, 'Test
')
+ .toString())
.toEqual('Test
');
- expect(_sanitizeHtml(defaultDoc, 'Info'))
+ expect(_sanitizeHtml(defaultDoc, 'Info').toString())
.toEqual('Info');
- expect(_sanitizeHtml(defaultDoc, '
'))
+ expect(
+ _sanitizeHtml(defaultDoc, '
').toString())
.toEqual('
');
});
it('sanitizes srcset attributes', () => {
- expect(_sanitizeHtml(defaultDoc, '
'))
+ expect(_sanitizeHtml(defaultDoc, '
')
+ .toString())
.toEqual('
');
});
it('supports sanitizing plain text', () => {
- expect(_sanitizeHtml(defaultDoc, 'Hello, World')).toEqual('Hello, World');
+ expect(_sanitizeHtml(defaultDoc, 'Hello, World').toString()).toEqual('Hello, World');
});
it('ignores non-element, non-attribute nodes', () => {
- expect(_sanitizeHtml(defaultDoc, 'no.')).toEqual('no.');
- expect(_sanitizeHtml(defaultDoc, 'no.')).toEqual('no.');
+ expect(_sanitizeHtml(defaultDoc, 'no.').toString()).toEqual('no.');
+ expect(_sanitizeHtml(defaultDoc, 'no.').toString()).toEqual('no.');
expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/);
});
it('supports sanitizing escaped entities', () => {
- expect(_sanitizeHtml(defaultDoc, '🚀')).toEqual('🚀');
+ expect(_sanitizeHtml(defaultDoc, '🚀').toString()).toEqual('🚀');
expect(logMsgs).toEqual([]);
});
it('does not warn when just re-encoding text', () => {
- expect(_sanitizeHtml(defaultDoc, 'Hellö Wörld
'))
+ expect(_sanitizeHtml(defaultDoc, 'Hellö Wörld
').toString())
.toEqual('Hellö Wörld
');
expect(logMsgs).toEqual([]);
});
it('escapes entities', () => {
- expect(_sanitizeHtml(defaultDoc, 'Hello < World
'))
+ expect(_sanitizeHtml(defaultDoc, 'Hello < World
').toString())
.toEqual('Hello < World
');
- expect(_sanitizeHtml(defaultDoc, 'Hello < World
')).toEqual('Hello < World
');
- expect(_sanitizeHtml(defaultDoc, 'Hello
'))
+ expect(_sanitizeHtml(defaultDoc, 'Hello < World
').toString())
+ .toEqual('Hello < World
');
+ expect(_sanitizeHtml(defaultDoc, 'Hello
').toString())
.toEqual('Hello
'); // NB: quote encoded as ASCII ".
});
@@ -110,7 +116,7 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
];
for (const tag of dangerousTags) {
it(tag, () => {
- expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!${tag}>`)).toEqual('evil!');
+ expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!${tag}>`).toString()).toEqual('evil!');
});
}
@@ -125,7 +131,8 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
];
for (const tag of dangerousSelfClosingTags) {
it(tag, () => {
- expect(_sanitizeHtml(defaultDoc, `before<${tag}>After`)).toEqual('beforeAfter');
+ expect(_sanitizeHtml(defaultDoc, `before<${tag}>After`).toString())
+ .toEqual('beforeAfter');
});
}
@@ -136,7 +143,7 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
];
for (const tag of dangerousSkipContentTags) {
it(tag, () => {
- expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!${tag}>`)).toEqual('');
+ expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!${tag}>`).toString()).toEqual('');
});
}
@@ -144,7 +151,8 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
// `` is special, because different browsers treat it differently (e.g. remove it
// altogether). // We just verify that (one way or another), there is no `` element
// after sanitization.
- expect(_sanitizeHtml(defaultDoc, `evil!`)).not.toContain('');
+ expect(_sanitizeHtml(defaultDoc, `evil!`).toString())
+ .not.toContain('');
});
});
@@ -153,45 +161,50 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
for (const attr of dangerousAttrs) {
it(`${attr}`, () => {
- expect(_sanitizeHtml(defaultDoc, `evil!`)).toEqual('evil!');
+ expect(_sanitizeHtml(defaultDoc, `evil!`).toString())
+ .toEqual('evil!');
});
}
});
it('ignores content of script elements', () => {
- expect(_sanitizeHtml(defaultDoc, '')).toEqual('');
- expect(_sanitizeHtml(defaultDoc, 'hi
'))
+ expect(_sanitizeHtml(defaultDoc, '').toString())
+ .toEqual('');
+ expect(_sanitizeHtml(defaultDoc, 'hi
')
+ .toString())
.toEqual('hi
');
- expect(_sanitizeHtml(defaultDoc, '')).toEqual('');
+ expect(_sanitizeHtml(defaultDoc, '').toString())
+ .toEqual('');
});
it('ignores content of style elements', () => {
- expect(_sanitizeHtml(defaultDoc, 'hi
'))
+ expect(_sanitizeHtml(defaultDoc, 'hi
').toString())
.toEqual('hi
');
- expect(_sanitizeHtml(defaultDoc, '')).toEqual('');
- expect(_sanitizeHtml(defaultDoc, '')).toEqual('');
+ expect(_sanitizeHtml(defaultDoc, '').toString()).toEqual('');
+ expect(_sanitizeHtml(defaultDoc, '').toString())
+ .toEqual('');
expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/);
});
it('should strip unclosed iframe tag', () => {
- expect(_sanitizeHtml(defaultDoc, 'bar')).toEqual('foobar');
- expect(_sanitizeHtml(defaultDoc, 'foobar')).toEqual('foobar');
+ expect(_sanitizeHtml(defaultDoc, 'bar').toString()).toEqual('foobar');
+ expect(_sanitizeHtml(defaultDoc, 'foobar').toString()).toEqual('foobar');
});
it('should not enter an infinite loop on clobbered elements', () => {
@@ -220,7 +233,8 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
// See
// https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
it('should not allow JavaScript execution when creating inert document', () => {
- const output = _sanitizeHtml(defaultDoc, '');
+ const output =
+ _sanitizeHtml(defaultDoc, '').toString();
const window = defaultDoc.defaultView;
if (window) {
expect(window.xxx).toBe(undefined);
@@ -233,7 +247,8 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
it('should not allow JavaScript hidden in badly formed HTML to get through sanitization (Firefox bug)',
() => {
expect(_sanitizeHtml(
- defaultDoc, '