/**
* @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 {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, 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';
import {NgForOf} from './common_with_def';
import {ComponentFixture, TemplateFixture} from './render_util';
describe('instructions', () => {
function createAnchor() {
elementStart(0, 'a');
elementEnd();
}
function createDiv() {
elementStart(0, 'div');
elementEnd();
}
function createScript() {
elementStart(0, 'script');
elementEnd();
}
describe('bind', () => {
it('should update bindings when value changes', () => {
const t = new TemplateFixture(createAnchor);
t.update(() => elementProperty(0, 'title', bind('Hello')));
expect(t.html).toEqual('');
t.update(() => elementProperty(0, 'title', bind('World')));
expect(t.html).toEqual('');
expect(ngDevMode).toHaveProperties({
firstTemplatePass: 1,
tNode: 2, // 1 for hostElement + 1 for the template under test
tView: 1,
rendererCreateElement: 1,
rendererSetProperty: 2
});
});
it('should not update bindings when value does not change', () => {
const idempotentUpdate = () => elementProperty(0, 'title', bind('Hello'));
const t = new TemplateFixture(createAnchor, idempotentUpdate);
t.update();
expect(t.html).toEqual('');
t.update();
expect(t.html).toEqual('');
expect(ngDevMode).toHaveProperties({
firstTemplatePass: 1,
tNode: 2, // 1 for hostElement + 1 for the template under test
tView: 1,
rendererCreateElement: 1,
rendererSetProperty: 1
});
});
});
describe('elementAttribute', () => {
it('should use sanitizer function', () => {
const t = new TemplateFixture(createDiv);
t.update(() => elementAttribute(0, 'title', 'javascript:true', sanitizeUrl));
expect(t.html).toEqual('
');
t.update(
() => elementAttribute(
0, 'title', bypassSanitizationTrustUrl('javascript:true'), sanitizeUrl));
expect(t.html).toEqual('');
expect(ngDevMode).toHaveProperties({
firstTemplatePass: 1,
tNode: 2, // 1 for div, 1 for host element
tView: 1,
rendererCreateElement: 1,
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', () => {
it('should use sanitizer function when available', () => {
const t = new TemplateFixture(createDiv);
t.update(() => elementProperty(0, 'title', 'javascript:true', sanitizeUrl));
expect(t.html).toEqual('');
t.update(
() => elementProperty(
0, 'title', bypassSanitizationTrustUrl('javascript:false'), sanitizeUrl));
expect(t.html).toEqual('');
expect(ngDevMode).toHaveProperties({
firstTemplatePass: 1,
tNode: 2, // 1 for div, 1 for host element
tView: 1,
rendererCreateElement: 1,
});
});
it('should not stringify non string values', () => {
const t = new TemplateFixture(createDiv);
t.update(() => elementProperty(0, 'hidden', false));
// The hidden property would be true if `false` was stringified into `"false"`.
expect((t.hostNode.native as HTMLElement).querySelector('div') !.hidden).toEqual(false);
expect(ngDevMode).toHaveProperties({
firstTemplatePass: 1,
tNode: 2, // 1 for div, 1 for host element
tView: 1,
rendererCreateElement: 1,
rendererSetProperty: 1
});
});
});
describe('elementStyleNamed', () => {
it('should use sanitizer function', () => {
const t = new TemplateFixture(createDiv);
t.update(
() => elementStyleNamed(0, 'background-image', 'url("http://server")', sanitizeStyle));
// nothing is set because sanitizer suppresses it.
expect(t.html).toEqual('');
t.update(
() => elementStyleNamed(
0, 'background-image', bypassSanitizationTrustStyle('url("http://server")'),
sanitizeStyle));
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('url("http://server")');
});
});
describe('elementStyle', () => {
function createDivWithStyle() {
elementStart(0, 'div', ['style', 'height: 10px']);
elementEnd();
}
it('should add style', () => {
const fixture = new TemplateFixture(createDivWithStyle);
fixture.update(() => elementStyle(0, {'background-color': 'red'}));
expect(fixture.html).toEqual('');
});
});
describe('elementClass', () => {
it('should add class', () => {
const fixture = new TemplateFixture(createDiv);
fixture.update(() => elementClass(0, 'multiple classes'));
expect(fixture.html).toEqual('');
});
});
describe('performance counters', () => {
it('should create tViews only once for each nested level', () => {
const _c0 = ['ngFor', '', 'ngForOf', ''];
/**
*
*
{{col}}
*
*/
class NestedLoops {
rows = [['a', 'b'], ['A', 'B'], ['a', 'b'], ['A', 'B']];
static ngComponentDef = defineComponent({
type: NestedLoops,
selectors: [['nested-loops']],
factory: function ToDoAppComponent_Factory() { return new NestedLoops(); },
template: function ToDoAppComponent_Template(rf: RenderFlags, ctx: NestedLoops) {
if (rf & RenderFlags.Create) {
container(0, ToDoAppComponent_NgForOf_Template_0, null, _c0);
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'ngForOf', bind(ctx.rows));
}
function ToDoAppComponent_NgForOf_Template_0(
rf: RenderFlags, ctx0: NgForOfContext) {
if (rf & RenderFlags.Create) {
elementStart(0, 'ul');
container(1, ToDoAppComponent_NgForOf_NgForOf_Template_1, null, _c0);
elementEnd();
}
if (rf & RenderFlags.Update) {
const row_r2 = ctx0.$implicit;
elementProperty(1, 'ngForOf', bind(row_r2));
}
function ToDoAppComponent_NgForOf_NgForOf_Template_1(
rf: RenderFlags, ctx1: NgForOfContext) {
if (rf & RenderFlags.Create) {
elementStart(0, 'li');
text(1);
elementEnd();
}
if (rf & RenderFlags.Update) {
const col_r3 = ctx1.$implicit;
textBinding(1, interpolation1('', col_r3, ''));
}
}
}
},
directives: [NgForOf]
});
}
const fixture = new ComponentFixture(NestedLoops);
expect(ngDevMode).toHaveProperties({
// Expect: fixture view/Host view + component + ngForRow + ngForCol
tView: 4, // should be: 4,
});
});
});
describe('sanitization injection compatibility', () => {
it('should work for url sanitization', () => {
const s = new LocalMockSanitizer(value => `${value}-sanitized`);
const t = new TemplateFixture(createAnchor, undefined, null, null, s);
const inputValue = 'http://foo';
const outputValue = 'http://foo-sanitized';
t.update(() => elementAttribute(0, 'href', inputValue, sanitizeUrl));
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toEqual(outputValue);
});
it('should bypass url sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createAnchor, undefined, null, null, s);
const inputValue = s.bypassSecurityTrustUrl('http://foo');
const outputValue = 'http://foo';
t.update(() => elementAttribute(0, 'href', inputValue, sanitizeUrl));
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should bypass ivy-level url sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createAnchor, undefined, null, null, s);
const inputValue = bypassSanitizationTrustUrl('http://foo');
const outputValue = 'http://foo-ivy';
t.update(() => elementAttribute(0, 'href', inputValue, sanitizeUrl));
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should work for style sanitization', () => {
const s = new LocalMockSanitizer(value => `color:blue`);
const t = new TemplateFixture(createDiv, undefined, null, null, s);
const inputValue = 'color:red';
const outputValue = 'color:blue';
t.update(() => elementAttribute(0, 'style', inputValue, sanitizeStyle));
expect(stripStyleWsCharacters(t.html)).toEqual(``);
expect(s.lastSanitizedValue).toEqual(outputValue);
});
it('should bypass style sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createDiv, undefined, null, null, s);
const inputValue = s.bypassSecurityTrustStyle('color:maroon');
const outputValue = 'color:maroon';
t.update(() => elementAttribute(0, 'style', inputValue, sanitizeStyle));
expect(stripStyleWsCharacters(t.html)).toEqual(``);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should bypass ivy-level style sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createDiv, undefined, null, null, s);
const inputValue = bypassSanitizationTrustStyle('font-family:foo');
const outputValue = 'font-family:foo-ivy';
t.update(() => elementAttribute(0, 'style', inputValue, sanitizeStyle));
expect(stripStyleWsCharacters(t.html)).toEqual(``);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should work for resourceUrl sanitization', () => {
const s = new LocalMockSanitizer(value => `${value}-sanitized`);
const t = new TemplateFixture(createScript, undefined, null, null, s);
const inputValue = 'http://resource';
const outputValue = 'http://resource-sanitized';
t.update(() => elementAttribute(0, 'src', inputValue, sanitizeResourceUrl));
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toEqual(outputValue);
});
it('should bypass resourceUrl sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, null, null, s);
const inputValue = s.bypassSecurityTrustResourceUrl('file://all-my-secrets.pdf');
const outputValue = 'file://all-my-secrets.pdf';
t.update(() => elementAttribute(0, 'src', inputValue, sanitizeResourceUrl));
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should bypass ivy-level resourceUrl sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, null, null, s);
const inputValue = bypassSanitizationTrustResourceUrl('file://all-my-secrets.pdf');
const outputValue = 'file://all-my-secrets.pdf-ivy';
t.update(() => elementAttribute(0, 'src', inputValue, sanitizeResourceUrl));
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should work for script sanitization', () => {
const s = new LocalMockSanitizer(value => `${value} //sanitized`);
const t = new TemplateFixture(createScript, undefined, null, null, s);
const inputValue = 'fn();';
const outputValue = 'fn(); //sanitized';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeScript));
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toEqual(outputValue);
});
it('should bypass script sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, null, null, s);
const inputValue = s.bypassSecurityTrustScript('alert("bar")');
const outputValue = 'alert("bar")';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeScript));
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should bypass ivy-level script sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createScript, undefined, null, null, s);
const inputValue = bypassSanitizationTrustScript('alert("bar")');
const outputValue = 'alert("bar")-ivy';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeScript));
expect(t.html).toEqual(``);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should work for html sanitization', () => {
const s = new LocalMockSanitizer(value => `${value} `);
const t = new TemplateFixture(createDiv, undefined, null, null, s);
const inputValue = '';
const outputValue = ' ';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml));
expect(t.html).toEqual(`
${outputValue}
`);
expect(s.lastSanitizedValue).toEqual(outputValue);
});
it('should bypass html sanitization if marked by the service', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createDiv, undefined, null, null, s);
const inputValue = s.bypassSecurityTrustHtml('');
const outputValue = '';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml));
expect(t.html).toEqual(`
${outputValue}
`);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should bypass ivy-level script sanitization if a custom sanitizer is used', () => {
const s = new LocalMockSanitizer(value => '');
const t = new TemplateFixture(createDiv, undefined, null, null, s);
const inputValue = bypassSanitizationTrustHtml('');
const outputValue = '-ivy';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml));
expect(t.html).toEqual(`
${outputValue}
`);
expect(s.lastSanitizedValue).toBeFalsy();
});
});
describe('namespace', () => {
it('should render SVG', () => {
const t = new TemplateFixture(() => {
elementStart(0, 'div', ['id', 'container']);
setSvgNS();
elementStart(1, 'svg', [
// id="display"
'id',
'display',
// width="400"
'width',
'400',
// 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();
elementEnd();
setHtmlNS();
elementEnd();
});
// Most browsers will print , some will print , both are valid
const standardHTML =
'';
const ie11HTML =
'';
expect([standardHTML, ie11HTML]).toContain(t.html);
});
it('should set an attribute with a namespace', () => {
const t = new TemplateFixture(() => {
elementStart(0, 'div', [
'id',
'container',
// test:title="abc"
NS.FULL,
'http://www.example.com/2014/test',
'title',
'abc',
]);
elementEnd();
});
const standardHTML = '';
const ie11HTML =
'';
expect([standardHTML, ie11HTML]).toContain(t.html);
});
it('should set attributes including more than one namespaced attribute', () => {
const t = new TemplateFixture(() => {
elementStart(0, 'div', [
'id',
'container',
// NS1:title="abc"
NS.FULL,
'http://www.example.com/2014/test',
'title',
'abc',
// style="background: #dead11"
'style',
'background: #dead11',
// NS1:whatever="wee"
NS.FULL,
'http://www.example.com/2014/test',
'whatever',
'wee',
// NS2:shazbot="wocka wocka"
NS.FULL,
'http://www.whatever.com/2016/blah',
'shazbot',
'wocka wocka',
]);
elementEnd();
});
const standardHTML =
'';
const ieHTML =
'';
expect([standardHTML, ieHTML]).toContain(t.html);
});
});
});
class LocalSanitizedValue {
constructor(public value: any) {}
toString() { return this.value; }
}
class LocalMockSanitizer implements Sanitizer {
public lastSanitizedValue: string|null;
constructor(private _interceptor: (value: string|null|any) => string) {}
sanitize(context: SecurityContext, value: LocalSanitizedValue|string|null|any): string|null {
if (value instanceof String) {
return value.toString() + '-ivy';
}
if (value instanceof LocalSanitizedValue) {
return value.toString();
}
return this.lastSanitizedValue = this._interceptor(value);
}
bypassSecurityTrustHtml(value: string) { return new LocalSanitizedValue(value); }
bypassSecurityTrustStyle(value: string) { return new LocalSanitizedValue(value); }
bypassSecurityTrustScript(value: string) { return new LocalSanitizedValue(value); }
bypassSecurityTrustUrl(value: string) { return new LocalSanitizedValue(value); }
bypassSecurityTrustResourceUrl(value: string) { return new LocalSanitizedValue(value); }
}
function stripStyleWsCharacters(value: string): string {
// color: blue; => color:blue
return value.replace(/;/g, '').replace(/:\s+/g, ':');
}