diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 1c55e79fa6..4cc09c5b2d 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -260,8 +260,11 @@ export function injectAttribute(attrNameToInject: string): string|undefined { const attrs = tElement.attrs; if (attrs) { for (let i = 0; i < attrs.length; i = i + 2) { - const attrName = attrs[i]; + let attrName = attrs[i]; if (attrName === AttributeMarker.SELECT_ONLY) break; + if (attrName === 0) { // NS.FULL + attrName = attrs[i += 2]; + } if (attrName == attrNameToInject) { return attrs[i + 1] as string; } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 13952a0761..ef9e7d9540 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -842,15 +842,28 @@ export function createTView( function setUpAttributes(native: RElement, attrs: TAttributes): void { const isProc = isProceduralRenderer(renderer); for (let i = 0; i < attrs.length; i += 2) { - const attrName = attrs[i]; - if (attrName === AttributeMarker.SELECT_ONLY) break; - if (attrName !== NG_PROJECT_AS_ATTR_NAME) { - const attrVal = attrs[i + 1]; - ngDevMode && ngDevMode.rendererSetAttribute++; - isProc ? - (renderer as ProceduralRenderer3) - .setAttribute(native, attrName as string, attrVal as string) : - native.setAttribute(attrName as string, attrVal as string); + let attrName = attrs[i]; + if (attrName === 0) { // NS.FULL + // Namespaced attribute + const attrNS = attrs[i + 1] as string; + attrName = attrs[i + 2] as string; + const attrVal = attrs[i + 3] as string; + i += 2; + if (isProc) { + (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal, attrNS); + } else { + native.setAttributeNS(attrNS, attrName, attrVal); + } + } else { + if (attrName === AttributeMarker.SELECT_ONLY) break; + if (attrName !== NG_PROJECT_AS_ATTR_NAME) { + const attrVal = attrs[i + 1]; + ngDevMode && ngDevMode.rendererSetAttribute++; + isProc ? + (renderer as ProceduralRenderer3) + .setAttribute(native, attrName as string, attrVal as string) : + native.setAttribute(attrName as string, attrVal as string); + } } } } @@ -1493,7 +1506,8 @@ function generateInitialInputs( const attrs = tNode.attrs !; for (let i = 0; i < attrs.length; i += 2) { - const attrName = attrs[i]; + const first = attrs[i]; + const attrName = first === 0 ? attrs[i += 2] : first; // 0 = NS.FULL const minifiedInputName = inputs[attrName]; const attrValue = attrs[i + 1]; @@ -1906,7 +1920,7 @@ function appendToProjectionNode( * - 1 based index of the selector from the {@link projectionDef} */ export function projection( - nodeIndex: number, localIndex: number, selectorIndex: number = 0, attrs?: string[]): void { + nodeIndex: number, localIndex: number, selectorIndex: number = 0, attrs?: TAttributes): void { const node = createLNode( nodeIndex, TNodeType.Projection, null, null, attrs || null, {head: null, tail: null}); diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index d4ac991ada..00ea8220a7 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -5,7 +5,6 @@ * 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 {LContainer} from './container'; import {LInjector} from './injector'; import {LProjection} from './projection'; @@ -14,6 +13,16 @@ import {RElement, RNode, RText} from './renderer'; import {LView, TData, TView} from './view'; +/** + * Namespace attribute flags. + */ +export const enum NS { + /** + * Use the next value as the full namespaces URI, the values after that + * are then the name and the value, respectively. + */ + FULL = 0, +} /** * TNodeType corresponds to the TNode.type property. It contains information @@ -179,7 +188,7 @@ export const enum AttributeMarker { * - attribute names and values * - special markers acting as flags to alter attributes processing. */ -export type TAttributes = (string | AttributeMarker)[]; +export type TAttributes = (string | AttributeMarker | NS)[]; /** * LNode binding data (flyweight) for a particular node that is shared between all templates diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts index 73f2ab55ec..5f6f8b5ed0 100644 --- a/packages/core/src/render3/node_selector_matcher.ts +++ b/packages/core/src/render3/node_selector_matcher.ts @@ -106,8 +106,12 @@ function findAttrIndexInNode(name: string, attrs: TAttributes | null): number { if (attrs === null) return -1; for (let i = 0; i < attrs.length; i += step) { const attrName = attrs[i]; - if (attrName === name) return i; - if (attrName === AttributeMarker.SELECT_ONLY) { + if (attrName === 0) { + // NS.FULL + step = 2; + } else if (attrName === name) { + return i; + } else if (attrName === AttributeMarker.SELECT_ONLY) { step = 1; } } diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index 751ab50bd8..7056457990 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -11,7 +11,7 @@ import {NgForOfContext} from '@angular/common'; import {RenderFlags, directiveInject} from '../../src/render3'; import {defineComponent} from '../../src/render3/definition'; import {bind, container, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, interpolation1, renderTemplate, setHtmlNS, setSvgNS, text, textBinding} from '../../src/render3/instructions'; -import {LElementNode, LNode} from '../../src/render3/interfaces/node'; +import {LElementNode, LNode, NS} from '../../src/render3/interfaces/node'; import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer'; import {TrustedString, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; @@ -91,6 +91,43 @@ describe('instructions', () => { rendererSetAttribute: 2 }); }); + + it('should use sanitizer function even on elements with namespaced attributes', () => { + const t = new TemplateFixture(() => { + elementStart(0, 'div', [ + NS.FULL, + 'http://www.example.com/2004/test', + 'whatever', + 'abc', + ]); + elementEnd(); + }); + + t.update(() => elementAttribute(0, 'title', 'javascript:true', sanitizeUrl)); + + + let standardHTML = '
'; + let ieHTML = ''; + + expect([standardHTML, ieHTML]).toContain(t.html); + + t.update( + () => elementAttribute( + 0, 'title', bypassSanitizationTrustUrl('javascript:true'), sanitizeUrl)); + + standardHTML = ''; + ieHTML = ''; + + expect([standardHTML, ieHTML]).toContain(t.html); + + expect(ngDevMode).toHaveProperties({ + firstTemplatePass: 1, + tNode: 2, + tView: 1, + rendererCreateElement: 1, + rendererSetAttribute: 2 + }); + }); }); describe('elementProperty', () => { @@ -408,6 +445,11 @@ describe('instructions', () => { // height="300" 'height', '300', + // test:title="abc" + NS.FULL, + 'http://www.example.com/2014/test', + 'title', + 'abc', ]); elementStart(2, 'circle', ['cx', '200', 'cy', '150', 'fill', '#0000ff']); elementEnd(); @@ -419,12 +461,71 @@ describe('instructions', () => { // Most browsers will print