refactor(compiler): move host properties into DirectiveWrapper

Part of #11683
This commit is contained in:
Tobias Bosch 2016-10-24 09:58:52 -07:00 committed by vsavkin
parent 5a7a58b1e0
commit 178fb79b5c
19 changed files with 409 additions and 175 deletions

View File

@ -133,7 +133,9 @@ export class CodeGenerator {
// TODO(vicb): do not pass cliOptions.i18nFormat here // TODO(vicb): do not pass cliOptions.i18nFormat here
const offlineCompiler = new compiler.OfflineCompiler( const offlineCompiler = new compiler.OfflineCompiler(
resolver, normalizer, tmplParser, new compiler.StyleCompiler(urlResolver), resolver, normalizer, tmplParser, new compiler.StyleCompiler(urlResolver),
new compiler.ViewCompiler(config), new compiler.DirectiveWrapperCompiler(config), new compiler.ViewCompiler(config, elementSchemaRegistry),
new compiler.DirectiveWrapperCompiler(
config, expressionParser, elementSchemaRegistry, console),
new compiler.NgModuleCompiler(), new compiler.TypeScriptEmitter(reflectorHost), new compiler.NgModuleCompiler(), new compiler.TypeScriptEmitter(reflectorHost),
cliOptions.locale, cliOptions.i18nFormat); cliOptions.locale, cliOptions.i18nFormat);

View File

@ -34,7 +34,7 @@ export {DirectiveResolver} from './src/directive_resolver';
export {PipeResolver} from './src/pipe_resolver'; export {PipeResolver} from './src/pipe_resolver';
export {NgModuleResolver} from './src/ng_module_resolver'; export {NgModuleResolver} from './src/ng_module_resolver';
export {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './src/ml_parser/interpolation_config'; export {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './src/ml_parser/interpolation_config';
export {ElementSchemaRegistry} from './src/schema/element_schema_registry'; export * from './src/schema/element_schema_registry';
export * from './src/i18n/index'; export * from './src/i18n/index';
export * from './src/template_parser/template_ast'; export * from './src/template_parser/template_ast';
export * from './src/directive_normalizer'; export * from './src/directive_normalizer';

View File

@ -8,7 +8,7 @@
import {CompileTokenMetadata} from '../compile_metadata'; import {CompileTokenMetadata} from '../compile_metadata';
import {isPresent} from '../facade/lang'; import {isPresent} from '../facade/lang';
import {Identifiers, resolveIdentifier} from '../identifiers'; import {IdentifierSpec, Identifiers, resolveEnumIdentifier, resolveIdentifier} from '../identifiers';
import * as o from '../output/output_ast'; import * as o from '../output/output_ast';
export function createDiTokenExpression(token: CompileTokenMetadata): o.Expression { export function createDiTokenExpression(token: CompileTokenMetadata): o.Expression {
@ -49,3 +49,12 @@ export function createPureProxy(
.set(o.importExpr(resolveIdentifier(pureProxyId)).callFn([fn])) .set(o.importExpr(resolveIdentifier(pureProxyId)).callFn([fn]))
.toStmt()); .toStmt());
} }
export function createEnumExpression(enumType: IdentifierSpec, enumValue: any): o.Expression {
const enumName =
Object.keys(enumType.runtime).find((propName) => enumType.runtime[propName] === enumValue);
if (!enumName) {
throw new Error(`Unknown enum value ${enumValue} in ${enumType.name}`);
}
return o.importExpr(resolveEnumIdentifier(resolveIdentifier(enumType), enumName));
}

View File

@ -12,23 +12,27 @@ import {Identifiers, resolveIdentifier} from '../identifiers';
import * as o from '../output/output_ast'; import * as o from '../output/output_ast';
import {BoundElementPropertyAst, PropertyBindingType} from '../template_parser/template_ast'; import {BoundElementPropertyAst, PropertyBindingType} from '../template_parser/template_ast';
import {createEnumExpression} from './identifier_util';
export function writeToRenderer( export function writeToRenderer(
view: o.Expression, boundProp: BoundElementPropertyAst, renderNode: o.Expression, view: o.Expression, boundProp: BoundElementPropertyAst, renderElement: o.Expression,
renderValue: o.Expression, logBindingUpdate: boolean): o.Statement[] { renderValue: o.Expression, logBindingUpdate: boolean,
securityContextExpression?: o.Expression): o.Statement[] {
const updateStmts: o.Statement[] = []; const updateStmts: o.Statement[] = [];
const renderer = view.prop('renderer'); const renderer = view.prop('renderer');
renderValue = sanitizedValue(view, boundProp, renderValue); renderValue = sanitizedValue(view, boundProp, renderValue, securityContextExpression);
switch (boundProp.type) { switch (boundProp.type) {
case PropertyBindingType.Property: case PropertyBindingType.Property:
if (logBindingUpdate) { if (logBindingUpdate) {
updateStmts.push(o.importExpr(resolveIdentifier(Identifiers.setBindingDebugInfo)) updateStmts.push(
.callFn([renderer, renderNode, o.literal(boundProp.name), renderValue]) o.importExpr(resolveIdentifier(Identifiers.setBindingDebugInfo))
.toStmt()); .callFn([renderer, renderElement, o.literal(boundProp.name), renderValue])
.toStmt());
} }
updateStmts.push( updateStmts.push(
renderer renderer
.callMethod( .callMethod(
'setElementProperty', [renderNode, o.literal(boundProp.name), renderValue]) 'setElementProperty', [renderElement, o.literal(boundProp.name), renderValue])
.toStmt()); .toStmt());
break; break;
case PropertyBindingType.Attribute: case PropertyBindingType.Attribute:
@ -37,13 +41,14 @@ export function writeToRenderer(
updateStmts.push( updateStmts.push(
renderer renderer
.callMethod( .callMethod(
'setElementAttribute', [renderNode, o.literal(boundProp.name), renderValue]) 'setElementAttribute', [renderElement, o.literal(boundProp.name), renderValue])
.toStmt()); .toStmt());
break; break;
case PropertyBindingType.Class: case PropertyBindingType.Class:
updateStmts.push( updateStmts.push(
renderer renderer
.callMethod('setElementClass', [renderNode, o.literal(boundProp.name), renderValue]) .callMethod(
'setElementClass', [renderElement, o.literal(boundProp.name), renderValue])
.toStmt()); .toStmt());
break; break;
case PropertyBindingType.Style: case PropertyBindingType.Style:
@ -55,7 +60,8 @@ export function writeToRenderer(
renderValue = renderValue.isBlank().conditional(o.NULL_EXPR, strValue); renderValue = renderValue.isBlank().conditional(o.NULL_EXPR, strValue);
updateStmts.push( updateStmts.push(
renderer renderer
.callMethod('setElementStyle', [renderNode, o.literal(boundProp.name), renderValue]) .callMethod(
'setElementStyle', [renderElement, o.literal(boundProp.name), renderValue])
.toStmt()); .toStmt());
break; break;
case PropertyBindingType.Animation: case PropertyBindingType.Animation:
@ -65,32 +71,19 @@ export function writeToRenderer(
} }
function sanitizedValue( function sanitizedValue(
view: o.Expression, boundProp: BoundElementPropertyAst, view: o.Expression, boundProp: BoundElementPropertyAst, renderValue: o.Expression,
renderValue: o.Expression): o.Expression { securityContextExpression?: o.Expression): o.Expression {
let enumValue: string; if (boundProp.securityContext === SecurityContext.NONE) {
switch (boundProp.securityContext) { return renderValue; // No sanitization needed.
case SecurityContext.NONE: }
return renderValue; // No sanitization needed. if (!boundProp.needsRuntimeSecurityContext) {
case SecurityContext.HTML: securityContextExpression =
enumValue = 'HTML'; createEnumExpression(Identifiers.SecurityContext, boundProp.securityContext);
break; }
case SecurityContext.STYLE: if (!securityContextExpression) {
enumValue = 'STYLE'; throw new Error(`internal error, no SecurityContext given ${boundProp.name}`);
break;
case SecurityContext.SCRIPT:
enumValue = 'SCRIPT';
break;
case SecurityContext.URL:
enumValue = 'URL';
break;
case SecurityContext.RESOURCE_URL:
enumValue = 'RESOURCE_URL';
break;
default:
throw new Error(`internal error, unexpected SecurityContext ${boundProp.securityContext}.`);
} }
let ctx = view.prop('viewUtils').prop('sanitizer'); let ctx = view.prop('viewUtils').prop('sanitizer');
let args = let args = [securityContextExpression, renderValue];
[o.importExpr(resolveIdentifier(Identifiers.SecurityContext)).prop(enumValue), renderValue];
return ctx.callMethod('sanitize', args); return ctx.callMethod('sanitize', args);
} }

View File

@ -10,11 +10,19 @@ import {Injectable} from '@angular/core';
import {CompileDirectiveMetadata, CompileIdentifierMetadata} from './compile_metadata'; import {CompileDirectiveMetadata, CompileIdentifierMetadata} from './compile_metadata';
import {createCheckBindingField, createCheckBindingStmt} from './compiler_util/binding_util'; import {createCheckBindingField, createCheckBindingStmt} from './compiler_util/binding_util';
import {convertPropertyBinding} from './compiler_util/expression_converter';
import {writeToRenderer} from './compiler_util/render_util';
import {CompilerConfig} from './config'; import {CompilerConfig} from './config';
import {Parser} from './expression_parser/parser';
import {Identifiers, resolveIdentifier} from './identifiers'; import {Identifiers, resolveIdentifier} from './identifiers';
import {DEFAULT_INTERPOLATION_CONFIG} from './ml_parser/interpolation_config';
import {ClassBuilder, createClassStmt} from './output/class_builder'; import {ClassBuilder, createClassStmt} from './output/class_builder';
import * as o from './output/output_ast'; import * as o from './output/output_ast';
import {LifecycleHooks, isDefaultChangeDetectionStrategy} from './private_import_core'; import {ParseError, ParseErrorLevel, ParseLocation, ParseSourceFile, ParseSourceSpan} from './parse_util';
import {Console, LifecycleHooks, isDefaultChangeDetectionStrategy} from './private_import_core';
import {ElementSchemaRegistry} from './schema/element_schema_registry';
import {BindingParser} from './template_parser/binding_parser';
import {BoundElementPropertyAst, BoundEventAst} from './template_parser/template_ast';
export class DirectiveWrapperCompileResult { export class DirectiveWrapperCompileResult {
constructor(public statements: o.Statement[], public dirWrapperClassVar: string) {} constructor(public statements: o.Statement[], public dirWrapperClassVar: string) {}
@ -44,14 +52,28 @@ const RESET_CHANGES_STMT = o.THIS_EXPR.prop(CHANGES_FIELD_NAME).set(o.literalMap
export class DirectiveWrapperCompiler { export class DirectiveWrapperCompiler {
static dirWrapperClassName(id: CompileIdentifierMetadata) { return `Wrapper_${id.name}`; } static dirWrapperClassName(id: CompileIdentifierMetadata) { return `Wrapper_${id.name}`; }
constructor(private compilerConfig: CompilerConfig) {} constructor(
private compilerConfig: CompilerConfig, private _exprParser: Parser,
private _schemaRegistry: ElementSchemaRegistry, private _console: Console) {}
compile(dirMeta: CompileDirectiveMetadata): DirectiveWrapperCompileResult { compile(dirMeta: CompileDirectiveMetadata): DirectiveWrapperCompileResult {
const builder = new DirectiveWrapperBuilder(this.compilerConfig, dirMeta); const builder = new DirectiveWrapperBuilder(this.compilerConfig, dirMeta);
Object.keys(dirMeta.inputs).forEach((inputFieldName) => { Object.keys(dirMeta.inputs).forEach((inputFieldName) => {
addCheckInputMethod(inputFieldName, builder); addCheckInputMethod(inputFieldName, builder);
}); });
addDetectChangesInternalMethod(builder); addDetectChangesInInputPropsMethod(builder);
const hostParseResult = parseHostBindings(dirMeta, this._exprParser, this._schemaRegistry);
reportParseErrors(hostParseResult.errors, this._console);
// host properties are change detected by the DirectiveWrappers,
// except for the animation properties as they need close integration with animation events
// and DirectiveWrappers don't support
// event listeners right now.
addDetectChangesInHostPropsMethod(
hostParseResult.hostProps.filter(hostProp => !hostProp.isAnimation), builder);
// TODO(tbosch): implement hostListeners via DirectiveWrapper as well!
const classStmt = builder.build(); const classStmt = builder.build();
return new DirectiveWrapperCompileResult([classStmt], classStmt.name); return new DirectiveWrapperCompileResult([classStmt], classStmt.name);
} }
@ -108,7 +130,7 @@ class DirectiveWrapperBuilder implements ClassBuilder {
} }
} }
function addDetectChangesInternalMethod(builder: DirectiveWrapperBuilder) { function addDetectChangesInInputPropsMethod(builder: DirectiveWrapperBuilder) {
const changedVar = o.variable('changed'); const changedVar = o.variable('changed');
const stmts: o.Statement[] = [ const stmts: o.Statement[] = [
changedVar.set(o.THIS_EXPR.prop(CHANGED_FIELD_NAME)).toDeclStmt(), changedVar.set(o.THIS_EXPR.prop(CHANGED_FIELD_NAME)).toDeclStmt(),
@ -148,7 +170,7 @@ function addDetectChangesInternalMethod(builder: DirectiveWrapperBuilder) {
stmts.push(new o.ReturnStatement(changedVar)); stmts.push(new o.ReturnStatement(changedVar));
builder.methods.push(new o.ClassMethod( builder.methods.push(new o.ClassMethod(
'detectChangesInternal', 'detectChangesInInputProps',
[ [
new o.FnParam( new o.FnParam(
VIEW_VAR.name, o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])), VIEW_VAR.name, o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])),
@ -184,3 +206,72 @@ function addCheckInputMethod(input: string, builder: DirectiveWrapperBuilder) {
], ],
methodBody)); methodBody));
} }
function addDetectChangesInHostPropsMethod(
hostProps: BoundElementPropertyAst[], builder: DirectiveWrapperBuilder) {
const stmts: o.Statement[] = [];
const methodParams: o.FnParam[] = [
new o.FnParam(
VIEW_VAR.name, o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])),
new o.FnParam(RENDER_EL_VAR.name, o.DYNAMIC_TYPE),
new o.FnParam(THROW_ON_CHANGE_VAR.name, o.BOOL_TYPE),
];
hostProps.forEach((hostProp) => {
const field = createCheckBindingField(builder);
const evalResult = convertPropertyBinding(
builder, null, o.THIS_EXPR.prop(CONTEXT_FIELD_NAME), hostProp.value, field.bindingId);
if (!evalResult) {
return;
}
let securityContextExpr: o.ReadVarExpr;
if (hostProp.needsRuntimeSecurityContext) {
securityContextExpr = o.variable(`secCtx_${methodParams.length}`);
methodParams.push(new o.FnParam(
securityContextExpr.name, o.importType(resolveIdentifier(Identifiers.SecurityContext))));
}
stmts.push(...createCheckBindingStmt(
evalResult, field.expression, THROW_ON_CHANGE_VAR,
writeToRenderer(
VIEW_VAR, hostProp, RENDER_EL_VAR, evalResult.currValExpr,
builder.compilerConfig.logBindingUpdate, securityContextExpr)));
});
builder.methods.push(new o.ClassMethod('detectChangesInHostProps', methodParams, stmts));
}
class ParseResult {
constructor(
public hostProps: BoundElementPropertyAst[], public hostListeners: BoundEventAst[],
public errors: ParseError[]) {}
}
function parseHostBindings(
dirMeta: CompileDirectiveMetadata, exprParser: Parser,
schemaRegistry: ElementSchemaRegistry): ParseResult {
const errors: ParseError[] = [];
const parser =
new BindingParser(exprParser, DEFAULT_INTERPOLATION_CONFIG, schemaRegistry, [], errors);
const sourceFileName = dirMeta.type.moduleUrl ?
`in Directive ${dirMeta.type.name} in ${dirMeta.type.moduleUrl}` :
`in Directive ${dirMeta.type.name}`;
const sourceFile = new ParseSourceFile('', sourceFileName);
const sourceSpan = new ParseSourceSpan(
new ParseLocation(sourceFile, null, null, null),
new ParseLocation(sourceFile, null, null, null));
const parsedHostProps = parser.createDirectiveHostPropertyAsts(dirMeta, sourceSpan);
const parsedHostListeners = parser.createDirectiveHostEventAsts(dirMeta, sourceSpan);
return new ParseResult(parsedHostProps, parsedHostListeners, errors);
}
function reportParseErrors(parseErrors: ParseError[], console: Console) {
const warnings = parseErrors.filter(error => error.level === ParseErrorLevel.WARNING);
const errors = parseErrors.filter(error => error.level === ParseErrorLevel.FATAL);
if (warnings.length > 0) {
this._console.warn(`Directive parse warnings:\n${warnings.join('\n')}`);
}
if (errors.length > 0) {
throw new Error(`Directive parse errors:\n${errors.join('\n')}`);
}
}

View File

@ -328,7 +328,12 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
* 'NONE' security context, i.e. that they are safe inert string values. Only specific well known * 'NONE' security context, i.e. that they are safe inert string values. Only specific well known
* attack vectors are assigned their appropriate context. * attack vectors are assigned their appropriate context.
*/ */
securityContext(tagName: string, propName: string): SecurityContext { securityContext(tagName: string, propName: string, isAttribute: boolean): SecurityContext {
if (isAttribute) {
// NB: For security purposes, use the mapped property name, not the attribute name.
propName = this.getMappedPropName(propName);
}
// Make sure comparisons are case insensitive, so that case differences between attribute and // Make sure comparisons are case insensitive, so that case differences between attribute and
// property names do not have a security impact. // property names do not have a security impact.
tagName = tagName.toLowerCase(); tagName = tagName.toLowerCase();
@ -366,4 +371,6 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
return {error: false}; return {error: false};
} }
} }
allKnownElementNames(): string[] { return Object.keys(this._schema); }
} }

View File

@ -6,12 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {SchemaMetadata} from '@angular/core'; import {SchemaMetadata, SecurityContext} from '@angular/core';
export abstract class ElementSchemaRegistry { export abstract class ElementSchemaRegistry {
abstract hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean; abstract hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean;
abstract hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean; abstract hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean;
abstract securityContext(tagName: string, propName: string): any; abstract securityContext(elementName: string, propName: string, isAttribute: boolean):
SecurityContext;
abstract allKnownElementNames(): string[];
abstract getMappedPropName(propName: string): string; abstract getMappedPropName(propName: string): string;
abstract getDefaultComponentElementName(): string; abstract getDefaultComponentElementName(): string;
abstract validateProperty(name: string): {error: boolean, msg?: string}; abstract validateProperty(name: string): {error: boolean, msg?: string};

View File

@ -6,17 +6,18 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {SchemaMetadata, SecurityContext} from '@angular/core'; import {SecurityContext} from '@angular/core';
import {CompilePipeMetadata} from '../compile_metadata'; import {CompileDirectiveMetadata, CompilePipeMetadata} from '../compile_metadata';
import {AST, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, LiteralPrimitive, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast'; import {AST, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, LiteralPrimitive, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser'; import {Parser} from '../expression_parser/parser';
import {isPresent} from '../facade/lang'; import {isPresent} from '../facade/lang';
import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
import {mergeNsAndName} from '../ml_parser/tags'; import {mergeNsAndName} from '../ml_parser/tags';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util'; import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {view_utils} from '../private_import_core'; import {view_utils} from '../private_import_core';
import {ElementSchemaRegistry} from '../schema/element_schema_registry'; import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector} from '../selector';
import {splitAtColon, splitAtPeriod} from '../util'; import {splitAtColon, splitAtPeriod} from '../util';
import {BoundElementPropertyAst, BoundEventAst, PropertyBindingType, VariableAst} from './template_ast'; import {BoundElementPropertyAst, BoundEventAst, PropertyBindingType, VariableAst} from './template_ast';
@ -55,18 +56,17 @@ export class BindingParser {
constructor( constructor(
private _exprParser: Parser, private _interpolationConfig: InterpolationConfig, private _exprParser: Parser, private _interpolationConfig: InterpolationConfig,
private _schemaRegistry: ElementSchemaRegistry, private _schemas: SchemaMetadata[], private _schemaRegistry: ElementSchemaRegistry, pipes: CompilePipeMetadata[],
pipes: CompilePipeMetadata[], private _targetErrors: ParseError[]) { private _targetErrors: ParseError[]) {
pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe)); pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe));
} }
createDirectiveHostPropertyAsts( createDirectiveHostPropertyAsts(dirMeta: CompileDirectiveMetadata, sourceSpan: ParseSourceSpan):
elementName: string, hostProps: {[key: string]: string}, BoundElementPropertyAst[] {
sourceSpan: ParseSourceSpan): BoundElementPropertyAst[] { if (dirMeta.hostProperties) {
if (hostProps) {
const boundProps: BoundProperty[] = []; const boundProps: BoundProperty[] = [];
Object.keys(hostProps).forEach(propName => { Object.keys(dirMeta.hostProperties).forEach(propName => {
const expression = hostProps[propName]; const expression = dirMeta.hostProperties[propName];
if (typeof expression === 'string') { if (typeof expression === 'string') {
this.parsePropertyBinding(propName, expression, true, sourceSpan, [], boundProps); this.parsePropertyBinding(propName, expression, true, sourceSpan, [], boundProps);
} else { } else {
@ -75,16 +75,16 @@ export class BindingParser {
sourceSpan); sourceSpan);
} }
}); });
return boundProps.map((prop) => this.createElementPropertyAst(elementName, prop)); return boundProps.map((prop) => this.createElementPropertyAst(dirMeta.selector, prop));
} }
} }
createDirectiveHostEventAsts(hostListeners: {[key: string]: string}, sourceSpan: ParseSourceSpan): createDirectiveHostEventAsts(dirMeta: CompileDirectiveMetadata, sourceSpan: ParseSourceSpan):
BoundEventAst[] { BoundEventAst[] {
if (hostListeners) { if (dirMeta.hostListeners) {
const targetEventAsts: BoundEventAst[] = []; const targetEventAsts: BoundEventAst[] = [];
Object.keys(hostListeners).forEach(propName => { Object.keys(dirMeta.hostListeners).forEach(propName => {
const expression = hostListeners[propName]; const expression = dirMeta.hostListeners[propName];
if (typeof expression === 'string') { if (typeof expression === 'string') {
this.parseEvent(propName, expression, sourceSpan, [], targetEventAsts); this.parseEvent(propName, expression, sourceSpan, [], targetEventAsts);
} else { } else {
@ -240,42 +240,33 @@ export class BindingParser {
} }
} }
createElementPropertyAst(elementName: string, boundProp: BoundProperty): BoundElementPropertyAst { createElementPropertyAst(elementSelector: string, boundProp: BoundProperty):
BoundElementPropertyAst {
if (boundProp.isAnimation) { if (boundProp.isAnimation) {
return new BoundElementPropertyAst( return new BoundElementPropertyAst(
boundProp.name, PropertyBindingType.Animation, SecurityContext.NONE, boundProp.expression, boundProp.name, PropertyBindingType.Animation, SecurityContext.NONE, false,
null, boundProp.sourceSpan); boundProp.expression, null, boundProp.sourceSpan);
} }
let unit: string = null; let unit: string = null;
let bindingType: PropertyBindingType; let bindingType: PropertyBindingType;
let boundPropertyName: string; let boundPropertyName: string;
const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR); const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR);
let securityContext: SecurityContext; let securityContexts: SecurityContext[];
if (parts.length === 1) { if (parts.length === 1) {
var partValue = parts[0]; var partValue = parts[0];
boundPropertyName = this._schemaRegistry.getMappedPropName(partValue); boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName); securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry, elementSelector, boundPropertyName, false);
bindingType = PropertyBindingType.Property; bindingType = PropertyBindingType.Property;
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false); this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false);
if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName, this._schemas)) {
let errorMsg =
`Can't bind to '${boundPropertyName}' since it isn't a known property of '${elementName}'.`;
if (elementName.indexOf('-') > -1) {
errorMsg +=
`\n1. If '${elementName}' is an Angular component and it has '${boundPropertyName}' input, then verify that it is part of this module.` +
`\n2. If '${elementName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.\n`;
}
this._reportError(errorMsg, boundProp.sourceSpan);
}
} else { } else {
if (parts[0] == ATTRIBUTE_PREFIX) { if (parts[0] == ATTRIBUTE_PREFIX) {
boundPropertyName = parts[1]; boundPropertyName = parts[1];
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true); this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true);
// NB: For security purposes, use the mapped property name, not the attribute name. securityContexts = calcPossibleSecurityContexts(
const mapPropName = this._schemaRegistry.getMappedPropName(boundPropertyName); this._schemaRegistry, elementSelector, boundPropertyName, true);
securityContext = this._schemaRegistry.securityContext(elementName, mapPropName);
const nsSeparatorIdx = boundPropertyName.indexOf(':'); const nsSeparatorIdx = boundPropertyName.indexOf(':');
if (nsSeparatorIdx > -1) { if (nsSeparatorIdx > -1) {
@ -288,22 +279,21 @@ export class BindingParser {
} else if (parts[0] == CLASS_PREFIX) { } else if (parts[0] == CLASS_PREFIX) {
boundPropertyName = parts[1]; boundPropertyName = parts[1];
bindingType = PropertyBindingType.Class; bindingType = PropertyBindingType.Class;
securityContext = SecurityContext.NONE; securityContexts = [SecurityContext.NONE];
} else if (parts[0] == STYLE_PREFIX) { } else if (parts[0] == STYLE_PREFIX) {
unit = parts.length > 2 ? parts[2] : null; unit = parts.length > 2 ? parts[2] : null;
boundPropertyName = parts[1]; boundPropertyName = parts[1];
bindingType = PropertyBindingType.Style; bindingType = PropertyBindingType.Style;
securityContext = SecurityContext.STYLE; securityContexts = [SecurityContext.STYLE];
} else { } else {
this._reportError(`Invalid property name '${boundProp.name}'`, boundProp.sourceSpan); this._reportError(`Invalid property name '${boundProp.name}'`, boundProp.sourceSpan);
bindingType = null; bindingType = null;
securityContext = null; securityContexts = [];
} }
} }
return new BoundElementPropertyAst( return new BoundElementPropertyAst(
boundPropertyName, bindingType, securityContext, boundProp.expression, unit, boundPropertyName, bindingType, securityContexts.length === 1 ? securityContexts[0] : null,
boundProp.sourceSpan); securityContexts.length > 1, boundProp.expression, unit, boundProp.sourceSpan);
} }
parseEvent( parseEvent(
@ -429,3 +419,21 @@ export class PipeCollector extends RecursiveAstVisitor {
function _isAnimationLabel(name: string): boolean { function _isAnimationLabel(name: string): boolean {
return name[0] == '@'; return name[0] == '@';
} }
export function calcPossibleSecurityContexts(
registry: ElementSchemaRegistry, selector: string, propName: string,
isAttribute: boolean): SecurityContext[] {
const ctxs: SecurityContext[] = [];
CssSelector.parse(selector).forEach((selector) => {
const elementNames = selector.element ? [selector.element] : registry.allKnownElementNames();
const notElementNames =
new Set(selector.notSelectors.filter(selector => selector.isElementSelector())
.map((selector) => selector.element));
const possibleElementNames =
elementNames.filter(elementName => !notElementNames.has(elementName));
ctxs.push(...possibleElementNames.map(
elementName => registry.securityContext(elementName, propName, isAttribute)));
});
return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort();
}

View File

@ -63,8 +63,8 @@ export class AttrAst implements TemplateAst {
export class BoundElementPropertyAst implements TemplateAst { export class BoundElementPropertyAst implements TemplateAst {
constructor( constructor(
public name: string, public type: PropertyBindingType, public name: string, public type: PropertyBindingType,
public securityContext: SecurityContext, public value: AST, public unit: string, public securityContext: SecurityContext, public needsRuntimeSecurityContext: boolean,
public sourceSpan: ParseSourceSpan) {} public value: AST, public unit: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any { visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElementProperty(this, context); return visitor.visitElementProperty(this, context);
} }

View File

@ -137,7 +137,7 @@ export class TemplateParser {
}; };
} }
const bindingParser = new BindingParser( const bindingParser = new BindingParser(
this._exprParser, interpolationConfig, this._schemaRegistry, schemas, uniqPipes, errors); this._exprParser, interpolationConfig, this._schemaRegistry, uniqPipes, errors);
const parseVisitor = new TemplateParseVisitor( const parseVisitor = new TemplateParseVisitor(
providerViewContext, uniqDirectives, bindingParser, this._schemaRegistry, schemas, providerViewContext, uniqDirectives, bindingParser, this._schemaRegistry, schemas,
errors); errors);
@ -549,10 +549,12 @@ class TemplateParseVisitor implements html.Visitor {
component = directive; component = directive;
} }
const directiveProperties: BoundDirectivePropertyAst[] = []; const directiveProperties: BoundDirectivePropertyAst[] = [];
const hostProperties = this._bindingParser.createDirectiveHostPropertyAsts( const hostProperties =
elementName, directive.hostProperties, sourceSpan); this._bindingParser.createDirectiveHostPropertyAsts(directive, sourceSpan);
const hostEvents = // Note: We need to check the host properties here as well,
this._bindingParser.createDirectiveHostEventAsts(directive.hostListeners, sourceSpan); // as we don't know the element name in the DirectiveWrapperCompiler yet.
this._checkPropertiesInSchema(elementName, hostProperties);
const hostEvents = this._bindingParser.createDirectiveHostEventAsts(directive, sourceSpan);
this._createDirectivePropertyAsts(directive.inputs, props, directiveProperties); this._createDirectivePropertyAsts(directive.inputs, props, directiveProperties);
elementOrDirectiveRefs.forEach((elOrDirRef) => { elementOrDirectiveRefs.forEach((elOrDirRef) => {
if ((elOrDirRef.value.length === 0 && directive.isComponent) || if ((elOrDirRef.value.length === 0 && directive.isComponent) ||
@ -626,6 +628,7 @@ class TemplateParseVisitor implements html.Visitor {
boundElementProps.push(this._bindingParser.createElementPropertyAst(elementName, prop)); boundElementProps.push(this._bindingParser.createElementPropertyAst(elementName, prop));
} }
}); });
this._checkPropertiesInSchema(elementName, boundElementProps);
return boundElementProps; return boundElementProps;
} }
@ -700,6 +703,22 @@ class TemplateParseVisitor implements html.Visitor {
}); });
} }
private _checkPropertiesInSchema(elementName: string, boundProps: BoundElementPropertyAst[]) {
boundProps.forEach((boundProp) => {
if (boundProp.type === PropertyBindingType.Property &&
!this._schemaRegistry.hasProperty(elementName, boundProp.name, this._schemas)) {
let errorMsg =
`Can't bind to '${boundProp.name}' since it isn't a known property of '${elementName}'.`;
if (elementName.indexOf('-') > -1) {
errorMsg +=
`\n1. If '${elementName}' is an Angular component and it has '${boundProp.name}' input, then verify that it is part of this module.` +
`\n2. If '${elementName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.\n`;
}
this._reportError(errorMsg, boundProp.sourceSpan);
}
});
}
private _reportError( private _reportError(
message: string, sourceSpan: ParseSourceSpan, message: string, sourceSpan: ParseSourceSpan,
level: ParseErrorLevel = ParseErrorLevel.FATAL) { level: ParseErrorLevel = ParseErrorLevel.FATAL) {

View File

@ -9,9 +9,9 @@
import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core'; import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
import {CompileIdentifierMetadata} from '../compile_metadata'; import {CompileIdentifierMetadata} from '../compile_metadata';
import {Identifiers, resolveEnumIdentifier, resolveIdentifier} from '../identifiers'; import {createEnumExpression} from '../compiler_util/identifier_util';
import {Identifiers, resolveEnumIdentifier} from '../identifiers';
import * as o from '../output/output_ast'; import * as o from '../output/output_ast';
import {ChangeDetectorStatus, ViewType} from '../private_import_core'; import {ChangeDetectorStatus, ViewType} from '../private_import_core';
function _enumExpression(classIdentifier: CompileIdentifierMetadata, name: string): o.Expression { function _enumExpression(classIdentifier: CompileIdentifierMetadata, name: string): o.Expression {
@ -20,69 +20,25 @@ function _enumExpression(classIdentifier: CompileIdentifierMetadata, name: strin
export class ViewTypeEnum { export class ViewTypeEnum {
static fromValue(value: ViewType): o.Expression { static fromValue(value: ViewType): o.Expression {
const viewType = resolveIdentifier(Identifiers.ViewType); return createEnumExpression(Identifiers.ViewType, value);
switch (value) {
case ViewType.HOST:
return _enumExpression(viewType, 'HOST');
case ViewType.COMPONENT:
return _enumExpression(viewType, 'COMPONENT');
case ViewType.EMBEDDED:
return _enumExpression(viewType, 'EMBEDDED');
default:
throw Error(`Inavlid ViewType value: ${value}`);
}
} }
} }
export class ViewEncapsulationEnum { export class ViewEncapsulationEnum {
static fromValue(value: ViewEncapsulation): o.Expression { static fromValue(value: ViewEncapsulation): o.Expression {
const viewEncapsulation = resolveIdentifier(Identifiers.ViewEncapsulation); return createEnumExpression(Identifiers.ViewEncapsulation, value);
switch (value) {
case ViewEncapsulation.Emulated:
return _enumExpression(viewEncapsulation, 'Emulated');
case ViewEncapsulation.Native:
return _enumExpression(viewEncapsulation, 'Native');
case ViewEncapsulation.None:
return _enumExpression(viewEncapsulation, 'None');
default:
throw Error(`Inavlid ViewEncapsulation value: ${value}`);
}
} }
} }
export class ChangeDetectionStrategyEnum { export class ChangeDetectionStrategyEnum {
static fromValue(value: ChangeDetectionStrategy): o.Expression { static fromValue(value: ChangeDetectionStrategy): o.Expression {
const changeDetectionStrategy = resolveIdentifier(Identifiers.ChangeDetectionStrategy); return createEnumExpression(Identifiers.ChangeDetectionStrategy, value);
switch (value) {
case ChangeDetectionStrategy.OnPush:
return _enumExpression(changeDetectionStrategy, 'OnPush');
case ChangeDetectionStrategy.Default:
return _enumExpression(changeDetectionStrategy, 'Default');
default:
throw Error(`Inavlid ChangeDetectionStrategy value: ${value}`);
}
} }
} }
export class ChangeDetectorStatusEnum { export class ChangeDetectorStatusEnum {
static fromValue(value: ChangeDetectorStatusEnum): o.Expression { static fromValue(value: ChangeDetectorStatusEnum): o.Expression {
const changeDetectorStatus = resolveIdentifier(Identifiers.ChangeDetectorStatus); return createEnumExpression(Identifiers.ChangeDetectorStatus, value);
switch (value) {
case ChangeDetectorStatus.CheckOnce:
return _enumExpression(changeDetectorStatus, 'CheckOnce');
case ChangeDetectorStatus.Checked:
return _enumExpression(changeDetectorStatus, 'Checked');
case ChangeDetectorStatus.CheckAlways:
return _enumExpression(changeDetectorStatus, 'CheckAlways');
case ChangeDetectorStatus.Detached:
return _enumExpression(changeDetectorStatus, 'Detached');
case ChangeDetectorStatus.Errored:
return _enumExpression(changeDetectorStatus, 'Errored');
case ChangeDetectorStatus.Destroyed:
return _enumExpression(changeDetectorStatus, 'Destroyed');
default:
throw Error(`Inavlid ChangeDetectorStatus value: ${value}`);
}
} }
} }

View File

@ -10,12 +10,14 @@ import {SecurityContext} from '@angular/core';
import {createCheckBindingField, createCheckBindingStmt} from '../compiler_util/binding_util'; import {createCheckBindingField, createCheckBindingStmt} from '../compiler_util/binding_util';
import {ConvertPropertyBindingResult, convertPropertyBinding} from '../compiler_util/expression_converter'; import {ConvertPropertyBindingResult, convertPropertyBinding} from '../compiler_util/expression_converter';
import {createEnumExpression} from '../compiler_util/identifier_util';
import {writeToRenderer} from '../compiler_util/render_util'; import {writeToRenderer} from '../compiler_util/render_util';
import * as cdAst from '../expression_parser/ast'; import * as cdAst from '../expression_parser/ast';
import {isPresent} from '../facade/lang'; import {isPresent} from '../facade/lang';
import {Identifiers, resolveIdentifier} from '../identifiers'; import {Identifiers, resolveIdentifier} from '../identifiers';
import * as o from '../output/output_ast'; import * as o from '../output/output_ast';
import {EMPTY_STATE as EMPTY_ANIMATION_STATE, LifecycleHooks, isDefaultChangeDetectionStrategy} from '../private_import_core'; import {EMPTY_STATE as EMPTY_ANIMATION_STATE, LifecycleHooks, isDefaultChangeDetectionStrategy} from '../private_import_core';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {BoundElementPropertyAst, BoundTextAst, DirectiveAst, PropertyBindingType} from '../template_parser/template_ast'; import {BoundElementPropertyAst, BoundTextAst, DirectiveAst, PropertyBindingType} from '../template_parser/template_ast';
import {camelCaseToDashCase} from '../util'; import {camelCaseToDashCase} from '../util';
@ -121,10 +123,39 @@ export function bindRenderInputs(
} }
export function bindDirectiveHostProps( export function bindDirectiveHostProps(
directiveAst: DirectiveAst, directiveInstance: o.Expression, compileElement: CompileElement, directiveAst: DirectiveAst, directiveWrapperInstance: o.Expression,
eventListeners: CompileEventListener[]): void { compileElement: CompileElement, eventListeners: CompileEventListener[], elementName: string,
schemaRegistry: ElementSchemaRegistry): void {
// host properties are change detected by the DirectiveWrappers,
// except for the animation properties as they need close integration with animation events
// and DirectiveWrappers don't support
// event listeners right now.
bindAndWriteToRenderer( bindAndWriteToRenderer(
directiveAst.hostProperties, directiveInstance, compileElement, true, eventListeners); directiveAst.hostProperties.filter(boundProp => boundProp.isAnimation),
directiveWrapperInstance.prop('context'), compileElement, true, eventListeners);
const methodArgs: o.Expression[] =
[o.THIS_EXPR, compileElement.renderNode, DetectChangesVars.throwOnChange];
// We need to provide the SecurityContext for properties that could need sanitization.
directiveAst.hostProperties.filter(boundProp => boundProp.needsRuntimeSecurityContext)
.forEach((boundProp) => {
let ctx: SecurityContext;
switch (boundProp.type) {
case PropertyBindingType.Property:
ctx = schemaRegistry.securityContext(elementName, boundProp.name, false);
break;
case PropertyBindingType.Attribute:
ctx = schemaRegistry.securityContext(elementName, boundProp.name, true);
break;
default:
throw new Error(
`Illegal state: Only property / attribute bindings can have an unknown security context! Binding ${boundProp.name}`);
}
methodArgs.push(createEnumExpression(Identifiers.SecurityContext, ctx));
});
compileElement.view.detectChangesRenderPropertiesMethod.addStmt(
directiveWrapperInstance.callMethod('detectChangesInHostProps', methodArgs).toStmt());
} }
export function bindDirectiveInputs( export function bindDirectiveInputs(
@ -157,7 +188,7 @@ export function bindDirectiveInputs(
var isOnPushComp = directiveAst.directive.isComponent && var isOnPushComp = directiveAst.directive.isComponent &&
!isDefaultChangeDetectionStrategy(directiveAst.directive.changeDetection); !isDefaultChangeDetectionStrategy(directiveAst.directive.changeDetection);
let directiveDetectChangesExpr = directiveWrapperInstance.callMethod( let directiveDetectChangesExpr = directiveWrapperInstance.callMethod(
'detectChangesInternal', 'detectChangesInInputProps',
[o.THIS_EXPR, compileElement.renderNode, DetectChangesVars.throwOnChange]); [o.THIS_EXPR, compileElement.renderNode, DetectChangesVars.throwOnChange]);
const directiveDetectChangesStmt = isOnPushComp ? const directiveDetectChangesStmt = isOnPushComp ?
new o.IfStmt(directiveDetectChangesExpr, [compileElement.appElement.prop('componentView') new o.IfStmt(directiveDetectChangesExpr, [compileElement.appElement.prop('componentView')

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast';
import {CompileElement} from './compile_element'; import {CompileElement} from './compile_element';
@ -14,8 +15,9 @@ import {CompileEventListener, bindDirectiveOutputs, bindRenderOutputs, collectEv
import {bindDirectiveAfterContentLifecycleCallbacks, bindDirectiveAfterViewLifecycleCallbacks, bindInjectableDestroyLifecycleCallbacks, bindPipeDestroyLifecycleCallbacks} from './lifecycle_binder'; import {bindDirectiveAfterContentLifecycleCallbacks, bindDirectiveAfterViewLifecycleCallbacks, bindInjectableDestroyLifecycleCallbacks, bindPipeDestroyLifecycleCallbacks} from './lifecycle_binder';
import {bindDirectiveHostProps, bindDirectiveInputs, bindRenderInputs, bindRenderText} from './property_binder'; import {bindDirectiveHostProps, bindDirectiveInputs, bindRenderInputs, bindRenderText} from './property_binder';
export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void { export function bindView(
var visitor = new ViewBinderVisitor(view); view: CompileView, parsedTemplate: TemplateAst[], schemaRegistry: ElementSchemaRegistry): void {
var visitor = new ViewBinderVisitor(view, schemaRegistry);
templateVisitAll(visitor, parsedTemplate); templateVisitAll(visitor, parsedTemplate);
view.pipes.forEach( view.pipes.forEach(
(pipe) => { bindPipeDestroyLifecycleCallbacks(pipe.meta, pipe.instance, pipe.view); }); (pipe) => { bindPipeDestroyLifecycleCallbacks(pipe.meta, pipe.instance, pipe.view); });
@ -24,7 +26,7 @@ export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void
class ViewBinderVisitor implements TemplateAstVisitor { class ViewBinderVisitor implements TemplateAstVisitor {
private _nodeIndex: number = 0; private _nodeIndex: number = 0;
constructor(public view: CompileView) {} constructor(public view: CompileView, private _schemaRegistry: ElementSchemaRegistry) {}
visitBoundText(ast: BoundTextAst, parent: CompileElement): any { visitBoundText(ast: BoundTextAst, parent: CompileElement): any {
var node = this.view.nodes[this._nodeIndex++]; var node = this.view.nodes[this._nodeIndex++];
@ -52,7 +54,9 @@ class ViewBinderVisitor implements TemplateAstVisitor {
compileElement.directiveWrapperInstance.get(directiveAst.directive.type.reference); compileElement.directiveWrapperInstance.get(directiveAst.directive.type.reference);
bindDirectiveInputs(directiveAst, directiveWrapperInstance, dirIndex, compileElement); bindDirectiveInputs(directiveAst, directiveWrapperInstance, dirIndex, compileElement);
bindDirectiveHostProps(directiveAst, directiveInstance, compileElement, eventListeners); bindDirectiveHostProps(
directiveAst, directiveWrapperInstance, compileElement, eventListeners, ast.name,
this._schemaRegistry);
bindDirectiveOutputs(directiveAst, directiveInstance, eventListeners); bindDirectiveOutputs(directiveAst, directiveInstance, eventListeners);
}); });
templateVisitAll(this, ast.children, compileElement); templateVisitAll(this, ast.children, compileElement);
@ -91,7 +95,7 @@ class ViewBinderVisitor implements TemplateAstVisitor {
var providerInstance = compileElement.instances.get(providerAst.token.reference); var providerInstance = compileElement.instances.get(providerAst.token.reference);
bindInjectableDestroyLifecycleCallbacks(providerAst, providerInstance, compileElement); bindInjectableDestroyLifecycleCallbacks(providerAst, providerInstance, compileElement);
}); });
bindView(compileElement.embeddedView, ast.children); bindView(compileElement.embeddedView, ast.children, this._schemaRegistry);
return null; return null;
} }

View File

@ -12,6 +12,7 @@ import {AnimationEntryCompileResult} from '../animation/animation_compiler';
import {CompileDirectiveMetadata, CompilePipeMetadata} from '../compile_metadata'; import {CompileDirectiveMetadata, CompilePipeMetadata} from '../compile_metadata';
import {CompilerConfig} from '../config'; import {CompilerConfig} from '../config';
import * as o from '../output/output_ast'; import * as o from '../output/output_ast';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {TemplateAst} from '../template_parser/template_ast'; import {TemplateAst} from '../template_parser/template_ast';
import {CompileElement} from './compile_element'; import {CompileElement} from './compile_element';
@ -31,7 +32,7 @@ export class ViewCompileResult {
@Injectable() @Injectable()
export class ViewCompiler { export class ViewCompiler {
constructor(private _genConfig: CompilerConfig) {} constructor(private _genConfig: CompilerConfig, private _schemaRegistry: ElementSchemaRegistry) {}
compileComponent( compileComponent(
component: CompileDirectiveMetadata, template: TemplateAst[], styles: o.Expression, component: CompileDirectiveMetadata, template: TemplateAst[], styles: o.Expression,
@ -47,7 +48,7 @@ export class ViewCompiler {
buildView(view, template, dependencies); buildView(view, template, dependencies);
// Need to separate binding from creation to be able to refer to // Need to separate binding from creation to be able to refer to
// variables that have been declared after usage. // variables that have been declared after usage.
bindView(view, template); bindView(view, template, this._schemaRegistry);
finishView(view, statements); finishView(view, statements);
return new ViewCompileResult(statements, view.viewFactory.name, dependencies); return new ViewCompileResult(statements, view.viewFactory.name, dependencies);

View File

@ -147,12 +147,12 @@ If 'onAnything' is a directive input, make sure the directive is imported by the
}); });
it('should return security contexts for elements', () => { it('should return security contexts for elements', () => {
expect(registry.securityContext('iframe', 'srcdoc')).toBe(SecurityContext.HTML); expect(registry.securityContext('iframe', 'srcdoc', false)).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'innerHTML')).toBe(SecurityContext.HTML); expect(registry.securityContext('p', 'innerHTML', false)).toBe(SecurityContext.HTML);
expect(registry.securityContext('a', 'href')).toBe(SecurityContext.URL); expect(registry.securityContext('a', 'href', false)).toBe(SecurityContext.URL);
expect(registry.securityContext('a', 'style')).toBe(SecurityContext.STYLE); expect(registry.securityContext('a', 'style', false)).toBe(SecurityContext.STYLE);
expect(registry.securityContext('ins', 'cite')).toBe(SecurityContext.URL); expect(registry.securityContext('ins', 'cite', false)).toBe(SecurityContext.URL);
expect(registry.securityContext('base', 'href')).toBe(SecurityContext.RESOURCE_URL); expect(registry.securityContext('base', 'href', false)).toBe(SecurityContext.RESOURCE_URL);
}); });
it('should detect properties on namespaced elements', () => { it('should detect properties on namespaced elements', () => {
@ -162,9 +162,14 @@ If 'onAnything' is a directive input, make sure the directive is imported by the
}); });
it('should check security contexts case insensitive', () => { it('should check security contexts case insensitive', () => {
expect(registry.securityContext('p', 'iNnErHtMl')).toBe(SecurityContext.HTML); expect(registry.securityContext('p', 'iNnErHtMl', false)).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'formaction')).toBe(SecurityContext.URL); expect(registry.securityContext('p', 'formaction', false)).toBe(SecurityContext.URL);
expect(registry.securityContext('p', 'formAction')).toBe(SecurityContext.URL); expect(registry.securityContext('p', 'formAction', false)).toBe(SecurityContext.URL);
});
it('should check security contexts for attributes', () => {
expect(registry.securityContext('p', 'innerHtml', true)).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'formaction', true)).toBe(SecurityContext.URL);
}); });
describe('Angular custom elements', () => { describe('Angular custom elements', () => {

View File

@ -0,0 +1,59 @@
/**
* @license
* Copyright Google Inc. 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 {SecurityContext} from '@angular/core';
import {inject} from '@angular/core/testing';
import {ElementSchemaRegistry} from '../../src/schema/element_schema_registry';
import {calcPossibleSecurityContexts} from '../../src/template_parser/binding_parser';
export function main() {
describe('BindingParser', () => {
let registry: ElementSchemaRegistry;
beforeEach(inject(
[ElementSchemaRegistry], (_registry: ElementSchemaRegistry) => { registry = _registry; }));
describe('possibleSecurityContexts', () => {
function hrefSecurityContexts(selector: string) {
return calcPossibleSecurityContexts(registry, selector, 'href', false);
}
it('should return a single security context if the selector as an element name',
() => { expect(hrefSecurityContexts('a')).toEqual([SecurityContext.URL]); });
it('should return the possible security contexts if the selector has no element name', () => {
expect(hrefSecurityContexts('[myDir]')).toEqual([
SecurityContext.NONE, SecurityContext.URL, SecurityContext.RESOURCE_URL
]);
});
it('should exclude possible elements via :not', () => {
expect(hrefSecurityContexts('[myDir]:not(link):not(base)')).toEqual([
SecurityContext.NONE, SecurityContext.URL
]);
});
it('should not exclude possible narrowed elements via :not', () => {
expect(hrefSecurityContexts('[myDir]:not(link.someClass):not(base.someClass)')).toEqual([
SecurityContext.NONE, SecurityContext.URL, SecurityContext.RESOURCE_URL
]);
});
it('should return SecurityContext.NONE if there are no possible elements',
() => { expect(hrefSecurityContexts('img:not(img)')).toEqual([SecurityContext.NONE]); });
it('should return the union of the possible security contexts if multiple selectors are specified',
() => {
expect(calcPossibleSecurityContexts(registry, 'a,link', 'href', false)).toEqual([
SecurityContext.URL, SecurityContext.RESOURCE_URL
]);
});
});
});
}

View File

@ -124,7 +124,7 @@ export function main() {
new class extends NullVisitor{ new class extends NullVisitor{
visitElementProperty(ast: BoundElementPropertyAst, context: any): any{return ast;} visitElementProperty(ast: BoundElementPropertyAst, context: any): any{return ast;}
}, },
new BoundElementPropertyAst('foo', null, null, null, 'bar', null)); new BoundElementPropertyAst('foo', null, null, false, null, 'bar', null));
}); });
it('should visit AttrAst', () => { it('should visit AttrAst', () => {
@ -171,7 +171,7 @@ export function main() {
new ElementAst('foo', [], [], [], [], [], [], false, [], 0, null, null), new ElementAst('foo', [], [], [], [], [], [], false, [], 0, null, null),
new ReferenceAst('foo', null, null), new VariableAst('foo', 'bar', null), new ReferenceAst('foo', null, null), new VariableAst('foo', 'bar', null),
new BoundEventAst('foo', 'bar', 'goo', null, null), new BoundEventAst('foo', 'bar', 'goo', null, null),
new BoundElementPropertyAst('foo', null, null, null, 'bar', null), new BoundElementPropertyAst('foo', null, null, false, null, 'bar', null),
new AttrAst('foo', 'bar', null), new BoundTextAst(null, 0, null), new AttrAst('foo', 'bar', null), new BoundTextAst(null, 0, null),
new TextAst('foo', 0, null), new DirectiveAst(null, [], [], [], null), new TextAst('foo', 0, null), new DirectiveAst(null, [], [], [], null),
new BoundDirectivePropertyAst('foo', 'bar', null, null) new BoundDirectivePropertyAst('foo', 'bar', null, null)

View File

@ -26,7 +26,9 @@ export class MockSchemaRegistry implements ElementSchemaRegistry {
return value === void 0 ? true : value; return value === void 0 ? true : value;
} }
securityContext(tagName: string, property: string): SecurityContext { allKnownElementNames(): string[] { return Object.keys(this.existingElements); }
securityContext(selector: string, property: string, isAttribute: boolean): SecurityContext {
return SecurityContext.NONE; return SecurityContext.NONE;
} }

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, Directive, Input, NO_ERRORS_SCHEMA} from '@angular/core'; import {Component, Directive, HostBinding, Input, NO_ERRORS_SCHEMA} from '@angular/core';
import {ComponentFixture, TestBed, getTestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed, getTestBed} from '@angular/core/testing';
import {afterEach, beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal'; import {afterEach, beforeEach, describe, expect, iit, it} from '@angular/core/testing/testing_internal';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {DomSanitizer} from '@angular/platform-browser/src/security/dom_sanitization_service'; import {DomSanitizer} from '@angular/platform-browser/src/security/dom_sanitization_service';
@ -133,22 +133,67 @@ function declareTests({useJit}: {useJit: boolean}) {
}); });
describe('sanitizing', () => { describe('sanitizing', () => {
it('should escape unsafe attributes', () => { function checkEscapeOfHrefProperty(fixture: ComponentFixture<any>, isAttribute: boolean) {
const template = `<a [href]="ctxProp">Link Title</a>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
let e = fixture.debugElement.children[0].nativeElement; let e = fixture.debugElement.children[0].nativeElement;
let ci = fixture.componentInstance; let ci = fixture.componentInstance;
ci.ctxProp = 'hello'; ci.ctxProp = 'hello';
fixture.detectChanges(); fixture.detectChanges();
// In the browser, reading href returns an absolute URL. On the server side, // In the browser, reading href returns an absolute URL. On the server side,
// it just echoes back the property. // it just echoes back the property.
expect(getDOM().getProperty(e, 'href')).toMatch(/.*\/?hello$/); let value =
isAttribute ? getDOM().getAttribute(e, 'href') : getDOM().getProperty(e, 'href');
expect(value).toMatch(/.*\/?hello$/);
ci.ctxProp = 'javascript:alert(1)'; ci.ctxProp = 'javascript:alert(1)';
fixture.detectChanges(); fixture.detectChanges();
expect(getDOM().getProperty(e, 'href')).toEqual('unsafe:javascript:alert(1)'); value = isAttribute ? getDOM().getAttribute(e, 'href') : getDOM().getProperty(e, 'href');
expect(value).toEqual('unsafe:javascript:alert(1)');
}
it('should escape unsafe properties', () => {
const template = `<a [href]="ctxProp">Link Title</a>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
checkEscapeOfHrefProperty(fixture, false);
});
it('should escape unsafe attributes', () => {
const template = `<a [attr.href]="ctxProp">Link Title</a>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
checkEscapeOfHrefProperty(fixture, true);
});
it('should escape unsafe properties if they are used in host bindings', () => {
@Directive({selector: '[dirHref]'})
class HrefDirective {
@HostBinding('href') @Input()
dirHref: string;
}
const template = `<a [dirHref]="ctxProp">Link Title</a>`;
TestBed.configureTestingModule({declarations: [HrefDirective]});
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
checkEscapeOfHrefProperty(fixture, false);
});
it('should escape unsafe attributes if they are used in host bindings', () => {
@Directive({selector: '[dirHref]'})
class HrefDirective {
@HostBinding('attr.href') @Input()
dirHref: string;
}
const template = `<a [dirHref]="ctxProp">Link Title</a>`;
TestBed.configureTestingModule({declarations: [HrefDirective]});
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
checkEscapeOfHrefProperty(fixture, true);
}); });
it('should escape unsafe style values', () => { it('should escape unsafe style values', () => {