feat(ivy): support injectable sanitization service (#23809)

PR Close #23809
This commit is contained in:
Matias Niemelä 2018-05-09 15:30:16 -07:00
parent d2a86872a9
commit 816bc8af17
9 changed files with 363 additions and 23 deletions

View File

@ -11,6 +11,7 @@
import {Type} from '../core';
import {Injector} from '../di/injector';
import {ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory';
import {Sanitizer} from '../sanitization/security';
import {assertComponentType, assertNotNull} from './assert';
import {queueInitHooks, queueLifecycleHooks} from './hooks';
@ -29,6 +30,9 @@ export interface CreateComponentOptions {
/** Which renderer factory to use. */
rendererFactory?: RendererFactory3;
/** A custom sanitizer instance */
sanitizer?: Sanitizer;
/**
* Host element on which the component will be bootstrapped. If not specified,
* the component definition's `tag` is used to query the existing DOM for the
@ -120,6 +124,7 @@ export function renderComponent<T>(
opts: CreateComponentOptions = {}): T {
ngDevMode && assertComponentType(componentType);
const rendererFactory = opts.rendererFactory || domRendererFactory3;
const sanitizer = opts.sanitizer || null;
const componentDef = (componentType as ComponentType<T>).ngComponentDef as ComponentDef<T>;
if (componentDef.type != componentType) componentDef.type = componentType;
let component: T;
@ -144,7 +149,7 @@ export function renderComponent<T>(
if (rendererFactory.begin) rendererFactory.begin();
// Create element node at index 0 in data array
elementNode = hostElement(componentTag, hostNode, componentDef);
elementNode = hostElement(componentTag, hostNode, componentDef, sanitizer);
// Create directive instance with factory() and store at index 0 in directives array
component = rootContext.component =

View File

@ -25,6 +25,7 @@ import {isDifferent, stringify} from './util';
import {executeHooks, queueLifecycleHooks, queueInitHooks, executeInitHooks} from './hooks';
import {ViewRef} from './view_ref';
import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors';
import {Sanitizer} from '../sanitization/security';
/**
* Directive (D) sets a property on all component instances using this constant as a key and the
@ -42,7 +43,7 @@ const _CLEAN_PROMISE = Promise.resolve(null);
/**
* Function used to sanitize the value before writing it into the renderer.
*/
export type Sanitizer = (value: any) => string;
export type SanitizerFn = (value: any) => string;
/**
* Directive and element indices for top-level directive.
@ -84,6 +85,10 @@ export function getRenderer(): Renderer3 {
return renderer;
}
export function getCurrentSanitizer(): Sanitizer|null {
return currentView && currentView.sanitizer;
}
/** Used to set the parent property when nodes are created. */
let previousOrParentNode: LNode;
@ -298,7 +303,7 @@ export function executeInitAndContentHooks(): void {
export function createLView<T>(
viewId: number, renderer: Renderer3, tView: TView, template: ComponentTemplate<T>| null,
context: T | null, flags: LViewFlags): LView {
context: T | null, flags: LViewFlags, sanitizer?: Sanitizer | null): LView {
const newView = {
parent: currentView,
id: viewId, // -1 for component views
@ -320,6 +325,7 @@ export function createLView<T>(
lifecycleStage: LifecycleStage.Init,
queries: null,
injector: currentView && currentView.injector,
sanitizer: sanitizer || null
};
return newView;
@ -450,8 +456,8 @@ function resetApplicationState() {
export function renderTemplate<T>(
hostNode: RElement, template: ComponentTemplate<T>, context: T,
providedRendererFactory: RendererFactory3, host: LElementNode | null,
directives?: DirectiveDefListOrFactory | null,
pipes?: PipeDefListOrFactory | null): LElementNode {
directives?: DirectiveDefListOrFactory | null, pipes?: PipeDefListOrFactory | null,
sanitizer?: Sanitizer | null): LElementNode {
if (host == null) {
resetApplicationState();
rendererFactory = providedRendererFactory;
@ -460,7 +466,7 @@ export function renderTemplate<T>(
null, LNodeType.Element, hostNode,
createLView(
-1, providedRendererFactory.createRenderer(null, null), tView, null, {},
LViewFlags.CheckAlways));
LViewFlags.CheckAlways, sanitizer));
}
const hostView = host.data !;
ngDevMode && assertNotNull(hostView, 'Host node should have an LView defined in host.data.');
@ -491,7 +497,8 @@ export function renderEmbeddedTemplate<T>(
previousOrParentNode = null !;
if (viewNode == null) {
const lView = createLView(-1, renderer, tView, template, context, LViewFlags.CheckAlways);
const lView = createLView(
-1, renderer, tView, template, context, LViewFlags.CheckAlways, getCurrentSanitizer());
viewNode = createLNode(null, LNodeType.View, null, lView);
rf = RenderFlags.Create;
@ -859,13 +866,14 @@ export function locateHostElement(
* @returns LElementNode created
*/
export function hostElement(
tag: string, rNode: RElement | null, def: ComponentDef<any>): LElementNode {
tag: string, rNode: RElement | null, def: ComponentDef<any>,
sanitizer?: Sanitizer | null): LElementNode {
resetApplicationState();
const node = createLNode(
0, LNodeType.Element, rNode,
createLView(
-1, renderer, getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs), null, null,
def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways));
def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, sanitizer));
if (firstTemplatePass) {
node.tNode = createTNode(tag as string, null, null);
@ -958,7 +966,7 @@ export function elementEnd() {
* @param sanitizer An optional function used to sanitize the value.
*/
export function elementAttribute(
index: number, name: string, value: any, sanitizer?: Sanitizer): void {
index: number, name: string, value: any, sanitizer?: SanitizerFn): void {
if (value !== NO_CHANGE) {
const element: LElementNode = data[index];
if (value == null) {
@ -989,7 +997,7 @@ export function elementAttribute(
*/
export function elementProperty<T>(
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: Sanitizer): void {
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void {
if (value === NO_CHANGE) return;
const node = data[index] as LElementNode;
const tNode = node.tNode !;
@ -1152,10 +1160,10 @@ export function elementClass<T>(index: number, value: T | NO_CHANGE): void {
export function elementStyleNamed<T>(
index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void;
export function elementStyleNamed<T>(
index: number, styleName: string, value: T | NO_CHANGE, sanitizer?: Sanitizer): void;
index: number, styleName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void;
export function elementStyleNamed<T>(
index: number, styleName: string, value: T | NO_CHANGE,
suffixOrSanitizer?: string | Sanitizer): void {
suffixOrSanitizer?: string | SanitizerFn): void {
if (value !== NO_CHANGE) {
const lElement: LElementNode = data[index];
if (value == null) {
@ -1305,7 +1313,8 @@ function addComponentLogic<T>(index: number, instance: T, def: ComponentDef<T>):
currentView, createLView(
-1, rendererFactory.createRenderer(
previousOrParentNode.native as RElement, def.rendererType),
tView, null, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways));
tView, null, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways,
getCurrentSanitizer()));
(previousOrParentNode.data as any) = hostView;
(hostView.node as any) = previousOrParentNode;
@ -1596,7 +1605,7 @@ export function embeddedViewStart(viewBlockId: number): RenderFlags {
// When we create a new LView, we always reset the state of the instructions.
const newView = createLView(
viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container), null, null,
LViewFlags.CheckAlways);
LViewFlags.CheckAlways, getCurrentSanitizer());
if (lContainer.queries) {
newView.queries = lContainer.queries.enterView(lContainer.nextIndex);
}

View File

@ -7,6 +7,8 @@
*/
import {Injector} from '../../di/injector';
import {Sanitizer} from '../../sanitization/security';
import {LContainer} from './container';
import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDef, PipeDefList} from './definition';
import {LElementNode, LViewNode, TNode} from './node';
@ -195,6 +197,11 @@ export interface LView {
* An optional Module Injector to be used as fall back after Element Injectors are consulted.
*/
injector: Injector|null;
/**
* An optional custom sanitizer
*/
sanitizer: Sanitizer|null;
}
/** Flags associated with an LView (saved in LView.flags) */

View File

@ -6,9 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {getCurrentSanitizer} from '../render3/instructions';
import {stringify} from '../render3/util';
import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer';
import {SecurityContext} from './security';
import {_sanitizeStyle as _sanitizeStyle} from './style_sanitizer';
import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer';
@ -79,6 +81,10 @@ export interface TrustedResourceUrlString extends TrustedString {
* and urls have been removed.
*/
export function sanitizeHtml(unsafeHtml: any): string {
const s = getCurrentSanitizer();
if (s) {
return s.sanitize(SecurityContext.HTML, unsafeHtml) || '';
}
if (unsafeHtml instanceof String && (unsafeHtml as TrustedHtmlString)[BRAND] === 'Html') {
return unsafeHtml.toString();
}
@ -99,6 +105,10 @@ export function sanitizeHtml(unsafeHtml: any): string {
* dangerous javascript and urls have been removed.
*/
export function sanitizeStyle(unsafeStyle: any): string {
const s = getCurrentSanitizer();
if (s) {
return s.sanitize(SecurityContext.STYLE, unsafeStyle) || '';
}
if (unsafeStyle instanceof String && (unsafeStyle as TrustedStyleString)[BRAND] === 'Style') {
return unsafeStyle.toString();
}
@ -120,6 +130,10 @@ export function sanitizeStyle(unsafeStyle: any): string {
* all of the dangerous javascript has been removed.
*/
export function sanitizeUrl(unsafeUrl: any): string {
const s = getCurrentSanitizer();
if (s) {
return s.sanitize(SecurityContext.URL, unsafeUrl) || '';
}
if (unsafeUrl instanceof String && (unsafeUrl as TrustedUrlString)[BRAND] === 'Url') {
return unsafeUrl.toString();
}
@ -136,6 +150,10 @@ export function sanitizeUrl(unsafeUrl: any): string {
* only trusted `url`s have been allowed to pass.
*/
export function sanitizeResourceUrl(unsafeResourceUrl: any): string {
const s = getCurrentSanitizer();
if (s) {
return s.sanitize(SecurityContext.RESOURCE_URL, unsafeResourceUrl) || '';
}
if (unsafeResourceUrl instanceof String &&
(unsafeResourceUrl as TrustedResourceUrlString)[BRAND] === 'ResourceUrl') {
return unsafeResourceUrl.toString();
@ -153,6 +171,10 @@ export function sanitizeResourceUrl(unsafeResourceUrl: any): string {
* because only trusted `scripts`s have been allowed to pass.
*/
export function sanitizeScript(unsafeScript: any): string {
const s = getCurrentSanitizer();
if (s) {
return s.sanitize(SecurityContext.SCRIPT, unsafeScript) || '';
}
if (unsafeScript instanceof String && (unsafeScript as TrustedScriptString)[BRAND] === 'Script') {
return unsafeScript.toString();
}

View File

@ -101,6 +101,9 @@
{
"name": "firstTemplatePass"
},
{
"name": "getCurrentSanitizer"
},
{
"name": "getDirectiveInstance"
},

View File

@ -383,6 +383,9 @@
{
"name": "generatePropertyAliases"
},
{
"name": "getCurrentSanitizer"
},
{
"name": "getDirectiveInstance"
},

View File

@ -13,17 +13,28 @@ import {defineComponent} from '../../src/render3/definition';
import {bind, container, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, interpolation1, renderTemplate, text, textBinding} from '../../src/render3/instructions';
import {LElementNode, LNode} from '../../src/render3/interfaces/node';
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
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('elementAttribute', () => {
it('should use sanitizer function', () => {
const t = new TemplateFixture(createDiv);
@ -177,4 +188,210 @@ describe('instructions', () => {
});
});
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(`<a href="${outputValue}"></a>`);
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(`<a href="${outputValue}"></a>`);
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(`<a href="${outputValue}"></a>`);
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(`<div style="${outputValue}"></div>`);
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(`<div style="${outputValue}"></div>`);
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(`<div style="${outputValue}"></div>`);
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(`<script src="${outputValue}"></script>`);
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(`<script src="${outputValue}"></script>`);
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(`<script src="${outputValue}"></script>`);
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(`<script>${outputValue}</script>`);
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(`<script>${outputValue}</script>`);
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(`<script>${outputValue}</script>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
it('should work for html sanitization', () => {
const s = new LocalMockSanitizer(value => `${value} <!--sanitized-->`);
const t = new TemplateFixture(createDiv, undefined, null, null, s);
const inputValue = '<header></header>';
const outputValue = '<header></header> <!--sanitized-->';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml));
expect(t.html).toEqual(`<div>${outputValue}</div>`);
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('<div onclick="alert(123)"></div>');
const outputValue = '<div onclick="alert(123)"></div>';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml));
expect(t.html).toEqual(`<div>${outputValue}</div>`);
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('<div onclick="alert(123)"></div>');
const outputValue = '<div onclick="alert(123)"></div>-ivy';
t.update(() => elementProperty(0, 'innerHTML', inputValue, sanitizeHtml));
expect(t.html).toEqual(`<div>${outputValue}</div>`);
expect(s.lastSanitizedValue).toBeFalsy();
});
});
});
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, ':');
}

View File

@ -11,6 +11,8 @@ import {RenderFlags} from '@angular/core/src/render3';
import {defineComponent, defineDirective} from '../../src/render3/index';
import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, elementStyleNamed, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, loadDirective, projection, projectionDef, text, textBinding} from '../../src/render3/instructions';
import {LViewFlags} from '../../src/render3/interfaces/view';
import {sanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
import {ComponentFixture, containerEl, renderToHtml} from './render_util';
@ -847,4 +849,65 @@ describe('render3 integration test', () => {
});
describe('sanitization', () => {
it('should sanitize data using the provided sanitization interface', () => {
class SanitizationComp {
static ngComponentDef = defineComponent({
type: SanitizationComp,
selectors: [['sanitize-this']],
factory: () => new SanitizationComp(),
template: (rf: RenderFlags, ctx: SanitizationComp) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'a');
elementEnd();
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'href', bind(ctx.href), sanitizeUrl);
}
}
});
private href = '';
updateLink(href: any) { this.href = href; }
}
const sanitizer = new LocalSanitizer((value) => { return 'http://bar'; });
const fixture = new ComponentFixture(SanitizationComp, {sanitizer});
fixture.component.updateLink('http://foo');
fixture.update();
const element = fixture.hostElement.querySelector('a') !;
expect(element.getAttribute('href')).toEqual('http://bar');
fixture.component.updateLink(sanitizer.bypassSecurityTrustUrl('http://foo'));
fixture.update();
expect(element.getAttribute('href')).toEqual('http://foo');
});
});
});
class LocalSanitizedValue {
constructor(public value: any) {}
toString() { return this.value; }
}
class LocalSanitizer implements Sanitizer {
constructor(private _interceptor: (value: string|null|any) => string) {}
sanitize(context: SecurityContext, value: LocalSanitizedValue|string|null): string|null {
if (value instanceof LocalSanitizedValue) {
return value.toString();
}
return this._interceptor(value);
}
bypassSecurityTrustHtml(value: string) {}
bypassSecurityTrustStyle(value: string) {}
bypassSecurityTrustScript(value: string) {}
bypassSecurityTrustResourceUrl(value: string) {}
bypassSecurityTrustUrl(value: string) { return new LocalSanitizedValue(value); }
}

View File

@ -16,6 +16,7 @@ import {NG_HOST_SYMBOL, renderTemplate} from '../../src/render3/instructions';
import {DirectiveDefList, DirectiveDefListOrFactory, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeDefListOrFactory, PipeTypesOrFactory} from '../../src/render3/interfaces/definition';
import {LElementNode} from '../../src/render3/interfaces/node';
import {RElement, RText, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {Sanitizer} from '../../src/sanitization/security';
import {Type} from '../../src/type';
import {getRendererFactory2} from './imported_renderer2';
@ -51,6 +52,8 @@ export class TemplateFixture extends BaseFixture {
hostNode: LElementNode;
private _directiveDefs: DirectiveDefList|null;
private _pipeDefs: PipeDefList|null;
private _sanitizer: Sanitizer|null;
/**
*
* @param createBlock Instructions which go into the creation block:
@ -60,10 +63,12 @@ export class TemplateFixture extends BaseFixture {
*/
constructor(
private createBlock: () => void, private updateBlock: () => void = noop,
directives?: DirectiveTypesOrFactory|null, pipes?: PipeTypesOrFactory|null) {
directives?: DirectiveTypesOrFactory|null, pipes?: PipeTypesOrFactory|null,
sanitizer?: Sanitizer) {
super();
this._directiveDefs = toDefs(directives, extractDirectiveDef);
this._pipeDefs = toDefs(pipes, extractPipeDef);
this._sanitizer = sanitizer || null;
this.hostNode = renderTemplate(this.hostElement, (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
this.createBlock();
@ -71,7 +76,7 @@ export class TemplateFixture extends BaseFixture {
if (rf & RenderFlags.Update) {
this.updateBlock();
}
}, null !, domRendererFactory3, null, this._directiveDefs, this._pipeDefs);
}, null !, domRendererFactory3, null, this._directiveDefs, this._pipeDefs, sanitizer);
}
/**
@ -82,7 +87,7 @@ export class TemplateFixture extends BaseFixture {
update(updateBlock?: () => void): void {
renderTemplate(
this.hostNode.native, updateBlock || this.updateBlock, null !, domRendererFactory3,
this.hostNode, this._directiveDefs, this._pipeDefs);
this.hostNode, this._directiveDefs, this._pipeDefs, this._sanitizer);
}
}
@ -94,7 +99,9 @@ export class ComponentFixture<T> extends BaseFixture {
component: T;
requestAnimationFrame: {(fn: () => void): void; flush(): void; queue: (() => void)[];};
constructor(private componentType: ComponentType<T>, opts: {injector?: Injector} = {}) {
constructor(
private componentType: ComponentType<T>,
opts: {injector?: Injector, sanitizer?: Sanitizer} = {}) {
super();
this.requestAnimationFrame = function(fn: () => void) {
requestAnimationFrame.queue.push(fn);
@ -106,9 +113,12 @@ export class ComponentFixture<T> extends BaseFixture {
}
};
this.component = _renderComponent(
componentType,
{host: this.hostElement, scheduler: this.requestAnimationFrame, injector: opts.injector});
this.component = _renderComponent(componentType, {
host: this.hostElement,
scheduler: this.requestAnimationFrame,
injector: opts.injector,
sanitizer: opts.sanitizer
});
}
update(): void {
@ -195,6 +205,7 @@ export function renderComponent<T>(type: ComponentType<T>, opts?: CreateComponen
rendererFactory: opts && opts.rendererFactory || testRendererFactory,
host: containerEl,
scheduler: requestAnimationFrame,
sanitizer: opts ? opts.sanitizer : undefined,
hostFeatures: opts && opts.hostFeatures
});
}