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
const offlineCompiler = new compiler.OfflineCompiler(
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),
cliOptions.locale, cliOptions.i18nFormat);

View File

@ -34,7 +34,7 @@ export {DirectiveResolver} from './src/directive_resolver';
export {PipeResolver} from './src/pipe_resolver';
export {NgModuleResolver} from './src/ng_module_resolver';
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/template_parser/template_ast';
export * from './src/directive_normalizer';

View File

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

View File

@ -10,11 +10,19 @@ import {Injectable} from '@angular/core';
import {CompileDirectiveMetadata, CompileIdentifierMetadata} from './compile_metadata';
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 {Parser} from './expression_parser/parser';
import {Identifiers, resolveIdentifier} from './identifiers';
import {DEFAULT_INTERPOLATION_CONFIG} from './ml_parser/interpolation_config';
import {ClassBuilder, createClassStmt} from './output/class_builder';
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 {
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 {
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 {
const builder = new DirectiveWrapperBuilder(this.compilerConfig, dirMeta);
Object.keys(dirMeta.inputs).forEach((inputFieldName) => {
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();
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 stmts: o.Statement[] = [
changedVar.set(o.THIS_EXPR.prop(CHANGED_FIELD_NAME)).toDeclStmt(),
@ -148,7 +170,7 @@ function addDetectChangesInternalMethod(builder: DirectiveWrapperBuilder) {
stmts.push(new o.ReturnStatement(changedVar));
builder.methods.push(new o.ClassMethod(
'detectChangesInternal',
'detectChangesInInputProps',
[
new o.FnParam(
VIEW_VAR.name, o.importType(resolveIdentifier(Identifiers.AppView), [o.DYNAMIC_TYPE])),
@ -184,3 +206,72 @@ function addCheckInputMethod(input: string, builder: DirectiveWrapperBuilder) {
],
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
* 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
// property names do not have a security impact.
tagName = tagName.toLowerCase();
@ -366,4 +371,6 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
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
*/
import {SchemaMetadata} from '@angular/core';
import {SchemaMetadata, SecurityContext} from '@angular/core';
export abstract class ElementSchemaRegistry {
abstract hasProperty(tagName: string, propName: 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 getDefaultComponentElementName(): 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
*/
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 {Parser} from '../expression_parser/parser';
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 {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {view_utils} from '../private_import_core';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector} from '../selector';
import {splitAtColon, splitAtPeriod} from '../util';
import {BoundElementPropertyAst, BoundEventAst, PropertyBindingType, VariableAst} from './template_ast';
@ -55,18 +56,17 @@ export class BindingParser {
constructor(
private _exprParser: Parser, private _interpolationConfig: InterpolationConfig,
private _schemaRegistry: ElementSchemaRegistry, private _schemas: SchemaMetadata[],
pipes: CompilePipeMetadata[], private _targetErrors: ParseError[]) {
private _schemaRegistry: ElementSchemaRegistry, pipes: CompilePipeMetadata[],
private _targetErrors: ParseError[]) {
pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe));
}
createDirectiveHostPropertyAsts(
elementName: string, hostProps: {[key: string]: string},
sourceSpan: ParseSourceSpan): BoundElementPropertyAst[] {
if (hostProps) {
createDirectiveHostPropertyAsts(dirMeta: CompileDirectiveMetadata, sourceSpan: ParseSourceSpan):
BoundElementPropertyAst[] {
if (dirMeta.hostProperties) {
const boundProps: BoundProperty[] = [];
Object.keys(hostProps).forEach(propName => {
const expression = hostProps[propName];
Object.keys(dirMeta.hostProperties).forEach(propName => {
const expression = dirMeta.hostProperties[propName];
if (typeof expression === 'string') {
this.parsePropertyBinding(propName, expression, true, sourceSpan, [], boundProps);
} else {
@ -75,16 +75,16 @@ export class BindingParser {
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[] {
if (hostListeners) {
if (dirMeta.hostListeners) {
const targetEventAsts: BoundEventAst[] = [];
Object.keys(hostListeners).forEach(propName => {
const expression = hostListeners[propName];
Object.keys(dirMeta.hostListeners).forEach(propName => {
const expression = dirMeta.hostListeners[propName];
if (typeof expression === 'string') {
this.parseEvent(propName, expression, sourceSpan, [], targetEventAsts);
} else {
@ -240,42 +240,33 @@ export class BindingParser {
}
}
createElementPropertyAst(elementName: string, boundProp: BoundProperty): BoundElementPropertyAst {
createElementPropertyAst(elementSelector: string, boundProp: BoundProperty):
BoundElementPropertyAst {
if (boundProp.isAnimation) {
return new BoundElementPropertyAst(
boundProp.name, PropertyBindingType.Animation, SecurityContext.NONE, boundProp.expression,
null, boundProp.sourceSpan);
boundProp.name, PropertyBindingType.Animation, SecurityContext.NONE, false,
boundProp.expression, null, boundProp.sourceSpan);
}
let unit: string = null;
let bindingType: PropertyBindingType;
let boundPropertyName: string;
const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR);
let securityContext: SecurityContext;
let securityContexts: SecurityContext[];
if (parts.length === 1) {
var partValue = parts[0];
boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName);
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry, elementSelector, boundPropertyName, false);
bindingType = PropertyBindingType.Property;
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 {
if (parts[0] == ATTRIBUTE_PREFIX) {
boundPropertyName = parts[1];
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true);
// NB: For security purposes, use the mapped property name, not the attribute name.
const mapPropName = this._schemaRegistry.getMappedPropName(boundPropertyName);
securityContext = this._schemaRegistry.securityContext(elementName, mapPropName);
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry, elementSelector, boundPropertyName, true);
const nsSeparatorIdx = boundPropertyName.indexOf(':');
if (nsSeparatorIdx > -1) {
@ -288,22 +279,21 @@ export class BindingParser {
} else if (parts[0] == CLASS_PREFIX) {
boundPropertyName = parts[1];
bindingType = PropertyBindingType.Class;
securityContext = SecurityContext.NONE;
securityContexts = [SecurityContext.NONE];
} else if (parts[0] == STYLE_PREFIX) {
unit = parts.length > 2 ? parts[2] : null;
boundPropertyName = parts[1];
bindingType = PropertyBindingType.Style;
securityContext = SecurityContext.STYLE;
securityContexts = [SecurityContext.STYLE];
} else {
this._reportError(`Invalid property name '${boundProp.name}'`, boundProp.sourceSpan);
bindingType = null;
securityContext = null;
securityContexts = [];
}
}
return new BoundElementPropertyAst(
boundPropertyName, bindingType, securityContext, boundProp.expression, unit,
boundProp.sourceSpan);
boundPropertyName, bindingType, securityContexts.length === 1 ? securityContexts[0] : null,
securityContexts.length > 1, boundProp.expression, unit, boundProp.sourceSpan);
}
parseEvent(
@ -429,3 +419,21 @@ export class PipeCollector extends RecursiveAstVisitor {
function _isAnimationLabel(name: string): boolean {
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 {
constructor(
public name: string, public type: PropertyBindingType,
public securityContext: SecurityContext, public value: AST, public unit: string,
public sourceSpan: ParseSourceSpan) {}
public securityContext: SecurityContext, public needsRuntimeSecurityContext: boolean,
public value: AST, public unit: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElementProperty(this, context);
}

View File

@ -137,7 +137,7 @@ export class TemplateParser {
};
}
const bindingParser = new BindingParser(
this._exprParser, interpolationConfig, this._schemaRegistry, schemas, uniqPipes, errors);
this._exprParser, interpolationConfig, this._schemaRegistry, uniqPipes, errors);
const parseVisitor = new TemplateParseVisitor(
providerViewContext, uniqDirectives, bindingParser, this._schemaRegistry, schemas,
errors);
@ -549,10 +549,12 @@ class TemplateParseVisitor implements html.Visitor {
component = directive;
}
const directiveProperties: BoundDirectivePropertyAst[] = [];
const hostProperties = this._bindingParser.createDirectiveHostPropertyAsts(
elementName, directive.hostProperties, sourceSpan);
const hostEvents =
this._bindingParser.createDirectiveHostEventAsts(directive.hostListeners, sourceSpan);
const hostProperties =
this._bindingParser.createDirectiveHostPropertyAsts(directive, sourceSpan);
// Note: We need to check the host properties here as well,
// 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);
elementOrDirectiveRefs.forEach((elOrDirRef) => {
if ((elOrDirRef.value.length === 0 && directive.isComponent) ||
@ -626,6 +628,7 @@ class TemplateParseVisitor implements html.Visitor {
boundElementProps.push(this._bindingParser.createElementPropertyAst(elementName, prop));
}
});
this._checkPropertiesInSchema(elementName, 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(
message: string, sourceSpan: ParseSourceSpan,
level: ParseErrorLevel = ParseErrorLevel.FATAL) {

View File

@ -9,9 +9,9 @@
import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
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 {ChangeDetectorStatus, ViewType} from '../private_import_core';
function _enumExpression(classIdentifier: CompileIdentifierMetadata, name: string): o.Expression {
@ -20,69 +20,25 @@ function _enumExpression(classIdentifier: CompileIdentifierMetadata, name: strin
export class ViewTypeEnum {
static fromValue(value: ViewType): o.Expression {
const viewType = resolveIdentifier(Identifiers.ViewType);
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}`);
}
return createEnumExpression(Identifiers.ViewType, value);
}
}
export class ViewEncapsulationEnum {
static fromValue(value: ViewEncapsulation): o.Expression {
const viewEncapsulation = resolveIdentifier(Identifiers.ViewEncapsulation);
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}`);
}
return createEnumExpression(Identifiers.ViewEncapsulation, value);
}
}
export class ChangeDetectionStrategyEnum {
static fromValue(value: ChangeDetectionStrategy): o.Expression {
const changeDetectionStrategy = resolveIdentifier(Identifiers.ChangeDetectionStrategy);
switch (value) {
case ChangeDetectionStrategy.OnPush:
return _enumExpression(changeDetectionStrategy, 'OnPush');
case ChangeDetectionStrategy.Default:
return _enumExpression(changeDetectionStrategy, 'Default');
default:
throw Error(`Inavlid ChangeDetectionStrategy value: ${value}`);
}
return createEnumExpression(Identifiers.ChangeDetectionStrategy, value);
}
}
export class ChangeDetectorStatusEnum {
static fromValue(value: ChangeDetectorStatusEnum): o.Expression {
const changeDetectorStatus = resolveIdentifier(Identifiers.ChangeDetectorStatus);
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}`);
}
return createEnumExpression(Identifiers.ChangeDetectorStatus, value);
}
}

View File

@ -10,12 +10,14 @@ import {SecurityContext} from '@angular/core';
import {createCheckBindingField, createCheckBindingStmt} from '../compiler_util/binding_util';
import {ConvertPropertyBindingResult, convertPropertyBinding} from '../compiler_util/expression_converter';
import {createEnumExpression} from '../compiler_util/identifier_util';
import {writeToRenderer} from '../compiler_util/render_util';
import * as cdAst from '../expression_parser/ast';
import {isPresent} from '../facade/lang';
import {Identifiers, resolveIdentifier} from '../identifiers';
import * as o from '../output/output_ast';
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 {camelCaseToDashCase} from '../util';
@ -121,10 +123,39 @@ export function bindRenderInputs(
}
export function bindDirectiveHostProps(
directiveAst: DirectiveAst, directiveInstance: o.Expression, compileElement: CompileElement,
eventListeners: CompileEventListener[]): void {
directiveAst: DirectiveAst, directiveWrapperInstance: o.Expression,
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(
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(
@ -157,7 +188,7 @@ export function bindDirectiveInputs(
var isOnPushComp = directiveAst.directive.isComponent &&
!isDefaultChangeDetectionStrategy(directiveAst.directive.changeDetection);
let directiveDetectChangesExpr = directiveWrapperInstance.callMethod(
'detectChangesInternal',
'detectChangesInInputProps',
[o.THIS_EXPR, compileElement.renderNode, DetectChangesVars.throwOnChange]);
const directiveDetectChangesStmt = isOnPushComp ?
new o.IfStmt(directiveDetectChangesExpr, [compileElement.appElement.prop('componentView')

View File

@ -6,6 +6,7 @@
* 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 {CompileElement} from './compile_element';
@ -14,8 +15,9 @@ import {CompileEventListener, bindDirectiveOutputs, bindRenderOutputs, collectEv
import {bindDirectiveAfterContentLifecycleCallbacks, bindDirectiveAfterViewLifecycleCallbacks, bindInjectableDestroyLifecycleCallbacks, bindPipeDestroyLifecycleCallbacks} from './lifecycle_binder';
import {bindDirectiveHostProps, bindDirectiveInputs, bindRenderInputs, bindRenderText} from './property_binder';
export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void {
var visitor = new ViewBinderVisitor(view);
export function bindView(
view: CompileView, parsedTemplate: TemplateAst[], schemaRegistry: ElementSchemaRegistry): void {
var visitor = new ViewBinderVisitor(view, schemaRegistry);
templateVisitAll(visitor, parsedTemplate);
view.pipes.forEach(
(pipe) => { bindPipeDestroyLifecycleCallbacks(pipe.meta, pipe.instance, pipe.view); });
@ -24,7 +26,7 @@ export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void
class ViewBinderVisitor implements TemplateAstVisitor {
private _nodeIndex: number = 0;
constructor(public view: CompileView) {}
constructor(public view: CompileView, private _schemaRegistry: ElementSchemaRegistry) {}
visitBoundText(ast: BoundTextAst, parent: CompileElement): any {
var node = this.view.nodes[this._nodeIndex++];
@ -52,7 +54,9 @@ class ViewBinderVisitor implements TemplateAstVisitor {
compileElement.directiveWrapperInstance.get(directiveAst.directive.type.reference);
bindDirectiveInputs(directiveAst, directiveWrapperInstance, dirIndex, compileElement);
bindDirectiveHostProps(directiveAst, directiveInstance, compileElement, eventListeners);
bindDirectiveHostProps(
directiveAst, directiveWrapperInstance, compileElement, eventListeners, ast.name,
this._schemaRegistry);
bindDirectiveOutputs(directiveAst, directiveInstance, eventListeners);
});
templateVisitAll(this, ast.children, compileElement);
@ -91,7 +95,7 @@ class ViewBinderVisitor implements TemplateAstVisitor {
var providerInstance = compileElement.instances.get(providerAst.token.reference);
bindInjectableDestroyLifecycleCallbacks(providerAst, providerInstance, compileElement);
});
bindView(compileElement.embeddedView, ast.children);
bindView(compileElement.embeddedView, ast.children, this._schemaRegistry);
return null;
}

View File

@ -12,6 +12,7 @@ import {AnimationEntryCompileResult} from '../animation/animation_compiler';
import {CompileDirectiveMetadata, CompilePipeMetadata} from '../compile_metadata';
import {CompilerConfig} from '../config';
import * as o from '../output/output_ast';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {TemplateAst} from '../template_parser/template_ast';
import {CompileElement} from './compile_element';
@ -31,7 +32,7 @@ export class ViewCompileResult {
@Injectable()
export class ViewCompiler {
constructor(private _genConfig: CompilerConfig) {}
constructor(private _genConfig: CompilerConfig, private _schemaRegistry: ElementSchemaRegistry) {}
compileComponent(
component: CompileDirectiveMetadata, template: TemplateAst[], styles: o.Expression,
@ -47,7 +48,7 @@ export class ViewCompiler {
buildView(view, template, dependencies);
// Need to separate binding from creation to be able to refer to
// variables that have been declared after usage.
bindView(view, template);
bindView(view, template, this._schemaRegistry);
finishView(view, statements);
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', () => {
expect(registry.securityContext('iframe', 'srcdoc')).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'innerHTML')).toBe(SecurityContext.HTML);
expect(registry.securityContext('a', 'href')).toBe(SecurityContext.URL);
expect(registry.securityContext('a', 'style')).toBe(SecurityContext.STYLE);
expect(registry.securityContext('ins', 'cite')).toBe(SecurityContext.URL);
expect(registry.securityContext('base', 'href')).toBe(SecurityContext.RESOURCE_URL);
expect(registry.securityContext('iframe', 'srcdoc', false)).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'innerHTML', false)).toBe(SecurityContext.HTML);
expect(registry.securityContext('a', 'href', false)).toBe(SecurityContext.URL);
expect(registry.securityContext('a', 'style', false)).toBe(SecurityContext.STYLE);
expect(registry.securityContext('ins', 'cite', false)).toBe(SecurityContext.URL);
expect(registry.securityContext('base', 'href', false)).toBe(SecurityContext.RESOURCE_URL);
});
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', () => {
expect(registry.securityContext('p', 'iNnErHtMl')).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'formaction')).toBe(SecurityContext.URL);
expect(registry.securityContext('p', 'formAction')).toBe(SecurityContext.URL);
expect(registry.securityContext('p', 'iNnErHtMl', false)).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'formaction', false)).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', () => {

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{
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', () => {
@ -171,7 +171,7 @@ export function main() {
new ElementAst('foo', [], [], [], [], [], [], false, [], 0, null, null),
new ReferenceAst('foo', null, null), new VariableAst('foo', 'bar', 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 TextAst('foo', 0, null), new DirectiveAst(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;
}
securityContext(tagName: string, property: string): SecurityContext {
allKnownElementNames(): string[] { return Object.keys(this.existingElements); }
securityContext(selector: string, property: string, isAttribute: boolean): SecurityContext {
return SecurityContext.NONE;
}

View File

@ -6,9 +6,9 @@
* 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 {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 {DomSanitizer} from '@angular/platform-browser/src/security/dom_sanitization_service';
@ -133,22 +133,67 @@ function declareTests({useJit}: {useJit: boolean}) {
});
describe('sanitizing', () => {
it('should escape unsafe attributes', () => {
const template = `<a [href]="ctxProp">Link Title</a>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
const fixture = TestBed.createComponent(SecuredComponent);
function checkEscapeOfHrefProperty(fixture: ComponentFixture<any>, isAttribute: boolean) {
let e = fixture.debugElement.children[0].nativeElement;
let ci = fixture.componentInstance;
ci.ctxProp = 'hello';
fixture.detectChanges();
// In the browser, reading href returns an absolute URL. On the server side,
// 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)';
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', () => {