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 `` tag. html = '' + 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