fix(DomSchemaRegistry): detect invalid elements

This commit is contained in:
Victor Berchet
2016-08-23 10:52:40 -07:00
parent 2b20db6c5a
commit 1df69cb4d2
22 changed files with 638 additions and 395 deletions

View File

@ -21,6 +21,16 @@ export function main() {
let registry: DomElementSchemaRegistry;
beforeEach(() => { registry = new DomElementSchemaRegistry(); });
it('should detect elements', () => {
expect(registry.hasElement('div', [])).toBeTruthy();
expect(registry.hasElement('b', [])).toBeTruthy();
expect(registry.hasElement('ng-container', [])).toBeTruthy();
expect(registry.hasElement('ng-content', [])).toBeTruthy();
expect(registry.hasElement('my-cmp', [])).toBeFalsy();
expect(registry.hasElement('abc', [])).toBeFalsy();
});
it('should detect properties on regular elements', () => {
expect(registry.hasProperty('div', 'id', [])).toBeTruthy();
expect(registry.hasProperty('div', 'title', [])).toBeTruthy();
@ -37,7 +47,7 @@ export function main() {
});
it('should detect different kinds of types', () => {
// inheritance: video => media => *
// inheritance: video => media => HTMLElement
expect(registry.hasProperty('video', 'className', [])).toBeTruthy(); // from *
expect(registry.hasProperty('video', 'id', [])).toBeTruthy(); // string
expect(registry.hasProperty('video', 'scrollLeft', [])).toBeTruthy(); // number
@ -57,11 +67,16 @@ export function main() {
it('should return true for custom-like elements if the CUSTOM_ELEMENTS_SCHEMA was used', () => {
expect(registry.hasProperty('custom-like', 'unknown', [CUSTOM_ELEMENTS_SCHEMA])).toBeTruthy();
expect(registry.hasElement('custom-like', [CUSTOM_ELEMENTS_SCHEMA])).toBeTruthy();
});
it('should return true for all elements if the NO_ERRORS_SCHEMA was used', () => {
expect(registry.hasProperty('custom-like', 'unknown', [NO_ERRORS_SCHEMA])).toBeTruthy();
expect(registry.hasProperty('a', 'unknown', [NO_ERRORS_SCHEMA])).toBeTruthy();
expect(registry.hasElement('custom-like', [NO_ERRORS_SCHEMA])).toBeTruthy();
expect(registry.hasElement('unknown', [NO_ERRORS_SCHEMA])).toBeTruthy();
});
it('should re-map property names that are specified in DOM facade',

View File

@ -6,38 +6,43 @@
* found in the LICENSE file at https://angular.io/license
*/
import {isPresent, isString} from '../../src/facade/lang';
const SVG_PREFIX = ':svg:';
const HTMLELEMENT_NAMES =
'abbr,address,article,aside,b,bdi,bdo,cite,code,dd,dfn,dt,em,figcaption,figure,footer,header,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,section,small,strong,sub,sup,u,var,wbr';
const HTMLELEMENT_NAME = 'abbr';
var document = typeof(global as any /** TODO #???? */)['document'] == 'object' ?
(global as any /** TODO #???? */)['document'] :
null;
const _G: any = global;
const document: any = typeof _G['document'] == 'object' ? _G['document'] : null;
export function extractSchema(): Map<string, string[]> {
var SVGGraphicsElement = (global as any /** TODO #???? */)['SVGGraphicsElement'];
var SVGAnimationElement = (global as any /** TODO #???? */)['SVGAnimationElement'];
var SVGGeometryElement = (global as any /** TODO #???? */)['SVGGeometryElement'];
var SVGComponentTransferFunctionElement =
(global as any /** TODO #???? */)['SVGComponentTransferFunctionElement'];
var SVGGradientElement = (global as any /** TODO #???? */)['SVGGradientElement'];
var SVGTextContentElement = (global as any /** TODO #???? */)['SVGTextContentElement'];
var SVGTextPositioningElement = (global as any /** TODO #???? */)['SVGTextPositioningElement'];
if (!document || !SVGGraphicsElement) return null;
var descMap: Map<string, string[]> = new Map();
var visited: {[name: string]: boolean} = {};
var element = document.createElement('video');
var svgAnimation = document.createElementNS('http://www.w3.org/2000/svg', 'set');
var svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
var svgFeFuncA = document.createElementNS('http://www.w3.org/2000/svg', 'feFuncA');
var svgGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
var svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
if (!document) return null;
const SVGGraphicsElement = _G['SVGGraphicsElement'];
if (!SVGGraphicsElement) return null;
const SVGAnimationElement = _G['SVGAnimationElement'];
const SVGGeometryElement = _G['SVGGeometryElement'];
const SVGComponentTransferFunctionElement = _G['SVGComponentTransferFunctionElement'];
const SVGGradientElement = _G['SVGGradientElement'];
const SVGTextContentElement = _G['SVGTextContentElement'];
const SVGTextPositioningElement = _G['SVGTextPositioningElement'];
const element = document.createElement('video');
const svgAnimation = document.createElementNS('http://www.w3.org/2000/svg', 'set');
const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const svgFeFuncA = document.createElementNS('http://www.w3.org/2000/svg', 'feFuncA');
const svgGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
const svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
const descMap: Map<string, string[]> = new Map();
let visited: {[name: string]: boolean} = {};
// HTML top level
extractProperties(Node, element, visited, descMap, '*', '');
extractProperties(Element, element, visited, descMap, '*', '');
extractProperties(HTMLElement, element, visited, descMap, '', '*');
extractProperties(HTMLMediaElement, element, visited, descMap, 'media', '');
extractProperties(SVGElement, svgText, visited, descMap, SVG_PREFIX, '*');
extractProperties(HTMLElement, element, visited, descMap, HTMLELEMENT_NAMES, '*');
extractProperties(HTMLMediaElement, element, visited, descMap, 'media', HTMLELEMENT_NAME);
// SVG top level
extractProperties(SVGElement, svgText, visited, descMap, SVG_PREFIX, HTMLELEMENT_NAME);
extractProperties(
SVGGraphicsElement, svgText, visited, descMap, SVG_PREFIX + 'graphics', SVG_PREFIX);
extractProperties(
@ -55,37 +60,64 @@ export function extractSchema(): Map<string, string[]> {
extractProperties(
SVGTextPositioningElement, svgText, visited, descMap, SVG_PREFIX + 'textPositioning',
SVG_PREFIX + 'textContent');
var keys = Object.keys(window).filter(
k => k.endsWith('Element') && (k.startsWith('HTML') || k.startsWith('SVG')));
keys.sort();
keys.forEach(
name =>
extractRecursiveProperties(visited, descMap, (window as any /** TODO #???? */)[name]));
// Get all element types
const types = Object.getOwnPropertyNames(window).filter(k => /^(HTML|SVG).*?Element$/.test(k));
types.sort();
types.forEach(type => { extractRecursiveProperties(visited, descMap, (window as any)[type]); });
return descMap;
}
function extractRecursiveProperties(
visited: {[name: string]: boolean}, descMap: Map<string, string[]>, type: Function): string {
var name = extractName(type);
if (visited[name]) return name; // already been here
var superName = '';
if (name != '*') {
superName = extractRecursiveProperties(visited, descMap, type.prototype.__proto__.constructor);
const name = extractName(type);
if (visited[name]) {
return name;
}
var instance: HTMLElement = null;
let superName: string;
switch (name) {
case '*':
superName = '';
break;
case HTMLELEMENT_NAME:
superName = '*';
break;
default:
superName =
extractRecursiveProperties(visited, descMap, type.prototype.__proto__.constructor);
}
// If the ancestor is an HTMLElement, use one of the multiple implememtation
superName = superName.split(',')[0];
let instance: HTMLElement = null;
name.split(',').forEach(tagName => {
instance = isSVG(type) ?
document.createElementNS('http://www.w3.org/2000/svg', tagName.replace(SVG_PREFIX, '')) :
document.createElement(tagName);
var htmlType = type;
if (tagName == 'cite') htmlType = HTMLElement;
let htmlType: Function;
switch (tagName) {
case 'cite':
htmlType = HTMLElement;
break;
default:
htmlType = type;
}
if (!(instance instanceof htmlType)) {
throw new Error(`Tag <${tagName}> is not an instance of ${htmlType['name']}`);
}
});
extractProperties(type, instance, visited, descMap, name, superName);
return name;
}
@ -93,21 +125,26 @@ function extractProperties(
type: Function, instance: any, visited: {[name: string]: boolean},
descMap: Map<string, string[]>, name: string, superName: string) {
if (!type) return;
visited[name] = true;
const fullName = name + (superName ? '^' + superName : '');
let props: string[] = descMap.has(fullName) ? descMap.get(fullName) : [];
var prototype = type.prototype;
var keys = Object.keys(prototype);
const props: string[] = descMap.has(fullName) ? descMap.get(fullName) : [];
const prototype = type.prototype;
let keys = Object.getOwnPropertyNames(prototype);
keys.sort();
keys.forEach((n) => {
if (n.startsWith('on')) {
props.push('*' + n.substr(2));
keys.forEach((name) => {
if (name.startsWith('on')) {
props.push('*' + name.substr(2));
} else {
var typeCh = typeMap[typeof instance[n]];
var descriptor = Object.getOwnPropertyDescriptor(prototype, n);
var isSetter = descriptor && isPresent(descriptor.set);
if (isString(typeCh) && !n.startsWith('webkit') && isSetter) {
props.push(typeCh + n);
const typeCh = _TYPE_MNEMONICS[typeof instance[name]];
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
const isSetter = descriptor && descriptor.set;
if (typeCh !== void 0 && !name.startsWith('webkit') && isSetter) {
props.push(typeCh + name);
}
}
});
@ -117,44 +154,76 @@ function extractProperties(
}
function extractName(type: Function): string {
var name = type['name'];
if (name == 'Element') return '*';
if (name == 'HTMLImageElement') return 'img';
if (name == 'HTMLAnchorElement') return 'a';
if (name == 'HTMLDListElement') return 'dl';
if (name == 'HTMLDirectoryElement') return 'dir';
if (name == 'HTMLHeadingElement') return 'h1,h2,h3,h4,h5,h6';
if (name == 'HTMLModElement') return 'ins,del';
if (name == 'HTMLOListElement') return 'ol';
if (name == 'HTMLParagraphElement') return 'p';
if (name == 'HTMLQuoteElement') return 'q,blockquote,cite';
if (name == 'HTMLTableCaptionElement') return 'caption';
if (name == 'HTMLTableCellElement') return 'th,td';
if (name == 'HTMLTableColElement') return 'col,colgroup';
if (name == 'HTMLTableRowElement') return 'tr';
if (name == 'HTMLTableSectionElement') return 'tfoot,thead,tbody';
if (name == 'HTMLUListElement') return 'ul';
if (name == 'SVGGraphicsElement') return SVG_PREFIX + 'graphics';
if (name == 'SVGMPathElement') return SVG_PREFIX + 'mpath';
if (name == 'SVGSVGElement') return SVG_PREFIX + 'svg';
if (name == 'SVGTSpanElement') return SVG_PREFIX + 'tspan';
var isSVG = name.startsWith('SVG');
if (name.startsWith('HTML') || isSVG) {
name = name.replace('HTML', '').replace('SVG', '').replace('Element', '');
if (isSVG && name.startsWith('FE')) {
name = 'fe' + name.substring(2);
} else if (name) {
name = name.charAt(0).toLowerCase() + name.substring(1);
}
return isSVG ? SVG_PREFIX + name : name.toLowerCase();
} else {
return null;
let name = type['name'];
switch (name) {
// see https://www.w3.org/TR/html5/index.html
// TODO(vicb): generate this map from all the element types
case 'Element':
return '*';
case 'HTMLElement':
return HTMLELEMENT_NAME;
case 'HTMLImageElement':
return 'img';
case 'HTMLAnchorElement':
return 'a';
case 'HTMLDListElement':
return 'dl';
case 'HTMLDirectoryElement':
return 'dir';
case 'HTMLHeadingElement':
return 'h1,h2,h3,h4,h5,h6';
case 'HTMLModElement':
return 'ins,del';
case 'HTMLOListElement':
return 'ol';
case 'HTMLParagraphElement':
return 'p';
case 'HTMLQuoteElement':
return 'q,blockquote,cite';
case 'HTMLTableCaptionElement':
return 'caption';
case 'HTMLTableCellElement':
return 'th,td';
case 'HTMLTableColElement':
return 'col,colgroup';
case 'HTMLTableRowElement':
return 'tr';
case 'HTMLTableSectionElement':
return 'tfoot,thead,tbody';
case 'HTMLUListElement':
return 'ul';
case 'SVGGraphicsElement':
return SVG_PREFIX + 'graphics';
case 'SVGMPathElement':
return SVG_PREFIX + 'mpath';
case 'SVGSVGElement':
return SVG_PREFIX + 'svg';
case 'SVGTSpanElement':
return SVG_PREFIX + 'tspan';
default:
const isSVG = name.startsWith('SVG');
if (name.startsWith('HTML') || isSVG) {
name = name.replace('HTML', '').replace('SVG', '').replace('Element', '');
if (isSVG && name.startsWith('FE')) {
name = 'fe' + name.substring(2);
} else if (name) {
name = name.charAt(0).toLowerCase() + name.substring(1);
}
return isSVG ? SVG_PREFIX + name : name.toLowerCase();
}
}
return null;
}
function isSVG(type: Function): boolean {
return type['name'].startsWith('SVG');
}
const typeMap =
<{[type: string]: string}>{'string': '', 'number': '#', 'boolean': '!', 'object': '%'};
const _TYPE_MNEMONICS: {[type: string]: string} = {
'string': '',
'number': '#',
'boolean': '!',
'object': '%',
};

View File

@ -22,11 +22,12 @@ import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/ml_pa
import {MockSchemaRegistry} from '../../testing/index';
import {unparse} from '../expression_parser/unparser';
var someModuleUrl = 'package:someModule';
const someModuleUrl = 'package:someModule';
var MOCK_SCHEMA_REGISTRY = [{
const MOCK_SCHEMA_REGISTRY = [{
provide: ElementSchemaRegistry,
useValue: new MockSchemaRegistry({'invalidProp': false}, {'mappedAttr': 'mappedProp'})
useValue: new MockSchemaRegistry(
{'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false}),
}];
export function main() {
@ -256,7 +257,7 @@ export function main() {
});
describe('errors', () => {
it('should throw error when binding to an unkonown property', () => {
it('should throw error when binding to an unknown property', () => {
expect(() => parse('<my-component [invalidProp]="bar"></my-component>', []))
.toThrowError(`Template parse errors:
Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
@ -264,6 +265,20 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
2. If 'my-component' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message.
("<my-component [ERROR ->][invalidProp]="bar"></my-component>"): TestComp@0:14`);
});
it('should throw error when binding to an unknown element w/o bindings', () => {
expect(() => parse('<unknown></unknown>', [])).toThrowError(`Template parse errors:
'unknown' is not a known element:
1. If 'unknown' is an Angular component, then verify that it is part of this module.
2. If 'unknown' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message. ("[ERROR ->]<unknown></unknown>"): TestComp@0:0`);
});
it('should throw error when binding to an unknown custom element w/o bindings', () => {
expect(() => parse('<un-known></un-known>', [])).toThrowError(`Template parse errors:
'un-known' is not a known element:
1. If 'un-known' is an Angular component, then verify that it is part of this module.
2. If 'un-known' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message. ("[ERROR ->]<un-known></un-known>"): TestComp@0:0`);
});
});
it('should parse bound properties via [...] and not report them as attributes', () => {