refactor(compiler): move host properties into DirectiveWrapper
Part of #11683
This commit is contained in:
parent
5a7a58b1e0
commit
178fb79b5c
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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); }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in New Issue