fix(ivy): throw on bindings to unknown properties (#28537)

This commit adds a devMode-only check which will throw if a user
attempts to bind a property that does not match a directive
input or a known HTML property.

Example:
```
<div [unknownProp]="someValue"></div>
```

The above will throw because "unknownProp" is not a known
property of HTMLDivElement.

This check is similar to the check executed in View Engine during
template parsing, but occurs at runtime instead of compile-time.

Note: This change uncovered an existing bug with host binding
inheritance, so some Material tests had to be turned off. They
will be fixed in an upcoming PR.

PR Close #28537
This commit is contained in:
Kara Erickson
2019-02-04 21:42:55 -08:00
committed by Miško Hevery
parent 7660d0d74a
commit 1950e2d9ba
13 changed files with 262 additions and 160 deletions

View File

@ -10,7 +10,7 @@ import {InjectFlags, InjectionToken, Injector} from '../di';
import {resolveForwardRef} from '../di/forward_ref';
import {ErrorHandler} from '../error_handler';
import {Type} from '../interface/type';
import {validateAttribute, validateProperty} from '../sanitization/sanitization';
import {validateAgainstEventAttributes, validateAgainstEventProperties} from '../sanitization/sanitization';
import {Sanitizer} from '../sanitization/security';
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {assertDataInRange, assertDefined, assertEqual, assertLessThan, assertNotEqual} from '../util/assert';
@ -39,7 +39,7 @@ import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode} from './state';
import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
import {BoundPlayerFactory} from './styling/player_factory';
import {createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util';
import {ANIMATION_PROP_PREFIX, createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util';
import {NO_CHANGE} from './tokens';
import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util';
@ -1099,7 +1099,7 @@ export function elementAttribute(
index: number, name: string, value: any, sanitizer?: SanitizerFn | null,
namespace?: string): void {
if (value !== NO_CHANGE) {
ngDevMode && validateAttribute(name);
ngDevMode && validateAgainstEventAttributes(name);
const lView = getLView();
const renderer = lView[RENDERER];
const element = getNativeByIndex(index, lView);
@ -1193,7 +1193,8 @@ function elementPropertyInternal<T>(
}
} else if (tNode.type === TNodeType.Element) {
if (ngDevMode) {
validateProperty(propName);
validateAgainstEventProperties(propName);
validateAgainstUnknownProperties(element, propName, tNode);
ngDevMode.rendererSetProperty++;
}
@ -1212,6 +1213,18 @@ function elementPropertyInternal<T>(
}
}
function validateAgainstUnknownProperties(
element: RElement | RComment, propName: string, tNode: TNode) {
// If prop is not a known property of the HTML element...
if (!(propName in element) &&
// and isn't a synthetic animation property...
propName[0] !== ANIMATION_PROP_PREFIX) {
// ... it is probably a user error and we should throw.
throw new Error(
`Template error: Can't bind to '${propName}' since it isn't a known property of '${tNode.tagName}'.`);
}
}
/**
* Stores debugging data for this property binding on first template pass.
* This enables features like DebugElement.properties.

View File

@ -20,7 +20,7 @@ import {getTNode} from '../util';
import {CorePlayerHandler} from './core_player_handler';
const ANIMATION_PROP_PREFIX = '@';
export const ANIMATION_PROP_PREFIX = '@';
export function createEmptyStylingContext(
element?: RElement | null, sanitizer?: StyleSanitizeFn | null,

View File

@ -178,7 +178,7 @@ export const defaultStyleSanitizer = (function(prop: string, value?: string): st
return sanitizeStyle(value);
} as StyleSanitizeFn);
export function validateProperty(name: string) {
export function validateAgainstEventProperties(name: string) {
if (name.toLowerCase().startsWith('on')) {
const msg = `Binding to event property '${name}' is disallowed for security reasons, ` +
`please use (${name.slice(2)})=...` +
@ -188,7 +188,7 @@ export function validateProperty(name: string) {
}
}
export function validateAttribute(name: string) {
export function validateAgainstEventAttributes(name: string) {
if (name.toLowerCase().startsWith('on')) {
const msg = `Binding to event attribute '${name}' is disallowed for security reasons, ` +
`please use (${name.slice(2)})=...`;