diff --git a/packages/compiler/src/schema/dom_element_schema_registry.ts b/packages/compiler/src/schema/dom_element_schema_registry.ts index 8644877645e9..ed2dca94ca0e 100644 --- a/packages/compiler/src/schema/dom_element_schema_registry.ts +++ b/packages/compiler/src/schema/dom_element_schema_registry.ts @@ -7,7 +7,8 @@ */ import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContext} from '../core'; -import {isNgContainer, isNgContent} from '../ml_parser/tags'; +import {isNgContainer, isNgContent, splitNsName} from '../ml_parser/tags'; +import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from '../template/pipeline/src/namespaces'; import {dashCaseToCamelCase} from '../util'; import {SECURITY_SCHEMA} from './dom_security_schema'; import {ElementSchemaRegistry} from './element_schema_registry'; @@ -17,6 +18,13 @@ const NUMBER = 'number'; const STRING = 'string'; const OBJECT = 'object'; +function normalizeTagName(tagName: string): string { + const tagNameLower = tagName.toLowerCase(); + const [ns, name] = splitNsName(tagNameLower, false); + + return ns === SVG_NAMESPACE || ns === MATH_ML_NAMESPACE ? `:${ns}:${name}` : name; +} + /** * This array represents the DOM schema. It encodes inheritance, properties, and events. * @@ -388,8 +396,9 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry { return true; } - if (tagName.indexOf('-') > -1) { - if (isNgContainer(tagName) || isNgContent(tagName)) { + const normalizedTag = normalizeTagName(tagName); + if (normalizedTag.includes('-')) { + if (isNgContainer(normalizedTag) || isNgContent(normalizedTag)) { return false; } @@ -400,8 +409,7 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry { } } - const elementProperties = - this._schema.get(tagName.toLowerCase()) || this._schema.get('unknown')!; + const elementProperties = this._schema.get(normalizedTag) || this._schema.get('unknown')!; return elementProperties.has(propName); } @@ -410,8 +418,9 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry { return true; } - if (tagName.indexOf('-') > -1) { - if (isNgContainer(tagName) || isNgContent(tagName)) { + const normalizedTag = normalizeTagName(tagName); + if (normalizedTag.includes('-')) { + if (isNgContainer(normalizedTag) || isNgContent(normalizedTag)) { return true; } @@ -421,7 +430,7 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry { } } - return this._schema.has(tagName.toLowerCase()); + return this._schema.has(normalizedTag); } /** @@ -444,12 +453,12 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry { propName = this.getMappedPropName(propName); } - tagName = tagName.toLowerCase(); + const normalizedTag = normalizeTagName(tagName); propName = propName.toLowerCase(); const securitySchema = SECURITY_SCHEMA(); const ctx = - securitySchema[tagName + '|' + propName] ?? + securitySchema[normalizedTag + '|' + propName] ?? securitySchema['*|' + propName] ?? SecurityContext.NONE; @@ -493,14 +502,15 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry { } allKnownAttributesOfElement(tagName: string): string[] { - const elementProperties = - this._schema.get(tagName.toLowerCase()) || this._schema.get('unknown')!; + const normalizedTag = normalizeTagName(tagName); + const elementProperties = this._schema.get(normalizedTag) || this._schema.get('unknown')!; // Convert properties to attributes. return Array.from(elementProperties.keys()).map((prop) => _PROP_TO_ATTR.get(prop) ?? prop); } allKnownEventsOfElement(tagName: string): string[] { - return Array.from(this._eventSchema.get(tagName.toLowerCase()) ?? []); + const normalizedTag = normalizeTagName(tagName); + return Array.from(this._eventSchema.get(normalizedTag) ?? []); } override normalizeAnimationStyleProperty(propName: string): string { diff --git a/packages/compiler/src/schema/dom_security_schema.ts b/packages/compiler/src/schema/dom_security_schema.ts index 12a674b7021f..1acc4ede7df7 100644 --- a/packages/compiler/src/schema/dom_security_schema.ts +++ b/packages/compiler/src/schema/dom_security_schema.ts @@ -115,6 +115,8 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} { ['object', ['codebase', 'data']], ]); + registerContext(SecurityContext.URL, SVG_NAMESPACE, [['a', ['href', 'xlink:href']]]); + // Keep this in sync with SECURITY_SENSITIVE_ELEMENTS in packages/core/src/sanitization/sanitization.ts // The `unknown` elements refer to cases when we need to validate the input/binding in a directive (host bindings) // and the directive can be applied to multiple different elements (with different tag names). In this case we generate diff --git a/packages/compiler/test/schema/dom_element_schema_registry_spec.ts b/packages/compiler/test/schema/dom_element_schema_registry_spec.ts index d11760732d41..839a206c5082 100644 --- a/packages/compiler/test/schema/dom_element_schema_registry_spec.ts +++ b/packages/compiler/test/schema/dom_element_schema_registry_spec.ts @@ -168,6 +168,12 @@ If 'onAnything' is a directive input, make sure the directive is imported by the expect(registry.securityContext(':svg:set', 'to', false)).toBe( SecurityContext.ATTRIBUTE_NO_BINDING, ); + + // SVG link attributes + expect(registry.securityContext(':svg:a', 'href', false)).toBe(SecurityContext.URL); + expect(registry.securityContext(':svg:a', 'xlink:href', false)).toBe(SecurityContext.URL); + expect(registry.securityContext(':svg:a', 'href', true)).toBe(SecurityContext.URL); + expect(registry.securityContext(':svg:a', 'xlink:href', true)).toBe(SecurityContext.URL); }); it('should detect properties on namespaced elements', () => { @@ -198,6 +204,23 @@ If 'onAnything' is a directive input, make sure the directive is imported by the }); }); + describe('Custom XML / XHTML namespaces', () => { + it('should support elements with custom namespaces', () => { + expect(registry.hasElement(':xhtml:a', [])).toBeTruthy(); + expect(registry.hasElement(':foo:div', [])).toBeTruthy(); + }); + + it('should support properties on custom namespaced elements', () => { + expect(registry.hasProperty(':xhtml:a', 'href', [])).toBeTruthy(); + expect(registry.hasProperty(':foo:div', 'id', [])).toBeTruthy(); + }); + + it('should return correct security contexts for custom namespaced elements', () => { + expect(registry.securityContext(':xhtml:a', 'href', false)).toBe(SecurityContext.URL); + expect(registry.securityContext(':foo:div', 'innerHTML', false)).toBe(SecurityContext.HTML); + }); + }); + // Uncomment to see the generated schema which can then be pasted to the DomElementSchemaRegistry // if (!isNode) { // it('generate a new schema', () => { diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts index 28a8441ea60b..4ef64c681618 100644 --- a/packages/core/src/render3/i18n/i18n_parse.ts +++ b/packages/core/src/render3/i18n/i18n_parse.ts @@ -73,6 +73,7 @@ import { } from './i18n_util'; import {createTNodeAtIndex} from '../tnode_manipulation'; import {allocExpando} from '../view/construction'; +import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from '../namespaces'; const BINDING_REGEXP = /�(\d+):?\d*�/gi; const ICU_REGEXP = /({\s*�\d+:?\d*�\s*,\s*\S{6}\s*,[\s\S]*})/gi; @@ -984,9 +985,34 @@ function addCreateAttribute( create.push((newIndex << IcuCreateOpCode.SHIFT_REF) | IcuCreateOpCode.Attr, attrName, attrValue); } +function splitNsName(elementName: string, fatal: boolean = true): [string | null, string] { + if (elementName[0] != ':') { + return [null, elementName]; + } + + const colonIndex = elementName.indexOf(':', 1); + + if (colonIndex === -1) { + if (fatal) { + throw new Error(`Unsupported format "${elementName}" expecting ":namespace:name"`); + } else { + return [null, elementName]; + } + } + + return [elementName.slice(1, colonIndex), elementName.slice(colonIndex + 1)]; +} + +function normalizeTagName(tagName: string): string { + const tagNameLower = tagName.toLowerCase(); + const [ns, name] = splitNsName(tagNameLower, false); + + return ns === SVG_NAMESPACE || ns === MATH_ML_NAMESPACE ? `:${ns}:${name}` : name; +} + function i18nResolveSanitizer(attrName: string, tagName?: string): SanitizerFn | null { const lowerAttrName = attrName.toLowerCase(); - const lowerTagName = tagName ? tagName.toLowerCase() : '*'; + const lowerTagName = tagName ? normalizeTagName(tagName) : '*'; const schema = SECURITY_SCHEMA(); const schemaContext = schema[`${lowerTagName}|${lowerAttrName}`] || diff --git a/packages/core/src/sanitization/dom_security_schema.ts b/packages/core/src/sanitization/dom_security_schema.ts index 12a674b7021f..1acc4ede7df7 100644 --- a/packages/core/src/sanitization/dom_security_schema.ts +++ b/packages/core/src/sanitization/dom_security_schema.ts @@ -115,6 +115,8 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} { ['object', ['codebase', 'data']], ]); + registerContext(SecurityContext.URL, SVG_NAMESPACE, [['a', ['href', 'xlink:href']]]); + // Keep this in sync with SECURITY_SENSITIVE_ELEMENTS in packages/core/src/sanitization/sanitization.ts // The `unknown` elements refer to cases when we need to validate the input/binding in a directive (host bindings) // and the directive can be applied to multiple different elements (with different tag names). In this case we generate diff --git a/packages/core/test/acceptance/security_spec.ts b/packages/core/test/acceptance/security_spec.ts index 4122b9aa40a4..8722b4b9c555 100644 --- a/packages/core/test/acceptance/security_spec.ts +++ b/packages/core/test/acceptance/security_spec.ts @@ -916,3 +916,57 @@ describe('SVG