feat(ivy): bridge compile instructions to include sanitization helpers (#24938)

PR Close #24938
This commit is contained in:
Matias Niemelä 2018-07-11 10:58:18 -07:00 committed by Victor Berchet
parent 13f3157823
commit 169e9dd2c8
21 changed files with 963 additions and 490 deletions

View File

@ -48,12 +48,12 @@ export class LargeTableComponent {
{ {
if (rf2 & RenderFlags.Create) { if (rf2 & RenderFlags.Create) {
E(0, 'td'); E(0, 'td');
s(c0); s(null, c0);
{ T(1); } { T(1); }
e(); e();
} }
if (rf2 & RenderFlags.Update) { if (rf2 & RenderFlags.Update) {
sp(0, 0, cell.row % 2 ? '' : 'grey'); sp(0, 0, null, cell.row % 2 ? '' : 'grey');
t(1, b(cell.value)); t(1, b(cell.value));
} }
} }

View File

@ -41,7 +41,7 @@ export class TreeComponent {
template: function(rf: RenderFlags, ctx: TreeComponent) { template: function(rf: RenderFlags, ctx: TreeComponent) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
E(0, 'span'); E(0, 'span');
s(c0); s(null, c0);
{ T(1); } { T(1); }
e(); e();
C(2); C(2);
@ -114,7 +114,7 @@ export function TreeTpl(rf: RenderFlags, ctx: TreeNode) {
E(0, 'tree'); E(0, 'tree');
{ {
E(1, 'span'); E(1, 'span');
s(c1); s(null, c1);
{ T(2); } { T(2); }
e(); e();
C(3); C(3);

View File

@ -53,7 +53,7 @@ describe('compiler compliance', () => {
template: function MyComponent_Template(rf, ctx) { template: function MyComponent_Template(rf, ctx) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, "div", $c1$); $r3$.ɵE(0, "div", $c1$);
$r3$.ɵs(null, $c2$); $r3$.ɵs($c2$);
$r3$.ɵNS(); $r3$.ɵNS();
$r3$.ɵE(1, "svg"); $r3$.ɵE(1, "svg");
$r3$.ɵEe(2, "circle", $c3$); $r3$.ɵEe(2, "circle", $c3$);
@ -103,7 +103,7 @@ describe('compiler compliance', () => {
template: function MyComponent_Template(rf, ctx) { template: function MyComponent_Template(rf, ctx) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, "div", $c1$); $r3$.ɵE(0, "div", $c1$);
$r3$.ɵs(null, $c2$); $r3$.ɵs($c2$);
$r3$.ɵNM(); $r3$.ɵNM();
$r3$.ɵE(1, "math"); $r3$.ɵE(1, "math");
$r3$.ɵEe(2, "infinity"); $r3$.ɵEe(2, "infinity");
@ -153,7 +153,7 @@ describe('compiler compliance', () => {
template: function MyComponent_Template(rf, ctx) { template: function MyComponent_Template(rf, ctx) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, "div", $c1$); $r3$.ɵE(0, "div", $c1$);
$r3$.ɵs(null, $c2$); $r3$.ɵs($c2$);
$r3$.ɵT(1, "Hello "); $r3$.ɵT(1, "Hello ");
$r3$.ɵE(2, "b"); $r3$.ɵE(2, "b");
$r3$.ɵT(3, "World"); $r3$.ɵT(3, "World");
@ -329,8 +329,8 @@ describe('compiler compliance', () => {
const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }'; const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }';
const template = ` const template = `
const _c0 = ["background-color"]; const _c0 = ["error"];
const _c1 = ["error"]; const _c1 = ["background-color"];
MyComponent.ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[["my-component"]], MyComponent.ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[["my-component"]],
factory:function MyComponent_Factory(){ factory:function MyComponent_Factory(){

View File

@ -43,11 +43,11 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) { template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, "div"); $r3$.ɵE(0, "div");
$r3$.ɵs(); $r3$.ɵs(null, null, $r3$.ɵzss);
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵsm(0, $ctx$.myStyleExp); $r3$.ɵsm(0, null, $ctx$.myStyleExp);
$r3$.ɵsa(0); $r3$.ɵsa(0);
} }
} }
@ -96,15 +96,15 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) { template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, "div"); $r3$.ɵE(0, "div");
$r3$.ɵs(_c0); $r3$.ɵs(null, _c0, $r3$.ɵzss);
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵsm(0, $ctx$.myStyleExp); $r3$.ɵsm(0, null, $ctx$.myStyleExp);
$r3$.ɵsp(0, 1, $ctx$.myWidth); $r3$.ɵsp(0, 1, $ctx$.myWidth);
$r3$.ɵsp(0, 2, $ctx$.myHeight); $r3$.ɵsp(0, 2, $ctx$.myHeight);
$r3$.ɵsa(0); $r3$.ɵsa(0);
$r3$.ɵa(0, "style", $r3$.ɵb("border-width: 10px")); $r3$.ɵa(0, "style", $r3$.ɵb("border-width: 10px"), $r3$.ɵzs);
} }
} }
}); });
@ -113,6 +113,59 @@ describe('compiler compliance: styling', () => {
const result = compile(files, angularFiles); const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template'); expectEmit(result.source, template, 'Incorrect template');
}); });
it('should assign a sanitizer instance to the element style allocation instruction if any url-based properties are detected',
() => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div [style.background-image]="myImage">\`
})
export class MyComponent {
myImage = 'url(foo.jpg)';
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
const _c0 = ["background-image"];
export class MyComponent {
constructor() {
this.myImage = 'url(foo.jpg)';
}
}
MyComponent.ngComponentDef = i0.ɵdefineComponent({
type: MyComponent,
selectors: [["my-component"]],
factory: function MyComponent_Factory() {
return new MyComponent();
},
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
i0.ɵE(0, "div");
i0.ɵs(null, _c0, i0.ɵzss);
i0.ɵe();
}
if (rf & 2) {
i0.ɵsp(0, 0, ctx.myImage);
i0.ɵsa(0);
}
}
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
}); });
describe('[class]', () => { describe('[class]', () => {
@ -144,7 +197,7 @@ describe('compiler compliance: styling', () => {
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵsm(0,null,$ctx$.myClassExp); $r3$.ɵsm(0,$ctx$.myClassExp);
$r3$.ɵsa(0); $r3$.ɵsa(0);
} }
} }
@ -193,11 +246,11 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) { template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, "div"); $r3$.ɵE(0, "div");
$r3$.ɵs(null, _c0); $r3$.ɵs(_c0);
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵsm(0, null, $ctx$.myClassExp); $r3$.ɵsm(0, $ctx$.myClassExp);
$r3$.ɵcp(0, 1, $ctx$.yesToApple); $r3$.ɵcp(0, 1, $ctx$.yesToApple);
$r3$.ɵcp(0, 2, $ctx$.yesToOrange); $r3$.ɵcp(0, 2, $ctx$.yesToOrange);
$r3$.ɵsa(0); $r3$.ɵsa(0);
@ -234,8 +287,8 @@ describe('compiler compliance: styling', () => {
}; };
const template = ` const template = `
const _c0 = ["width",${InitialStylingFlags.VALUES_MODE},"width","100px"]; const _c0 = ["foo",${InitialStylingFlags.VALUES_MODE},"foo",true];
const _c1 = ["foo",${InitialStylingFlags.VALUES_MODE},"foo",true]; const _c1 = ["width",${InitialStylingFlags.VALUES_MODE},"width","100px"];
MyComponent.ngComponentDef = i0.ɵdefineComponent({ MyComponent.ngComponentDef = i0.ɵdefineComponent({
type: MyComponent, type: MyComponent,
@ -251,7 +304,7 @@ describe('compiler compliance: styling', () => {
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵa(0, "class", $r3$.ɵb("round")); $r3$.ɵa(0, "class", $r3$.ɵb("round"));
$r3$.ɵa(0, "style", $r3$.ɵb("height:100px")); $r3$.ɵa(0, "style", $r3$.ɵb("height:100px"), $r3$.ɵzs);
} }
} }
}); });

View File

@ -154,4 +154,12 @@ export class Identifiers {
// Reserve slots for pure functions // Reserve slots for pure functions
static reserveSlots: o.ExternalReference = {name: 'ɵrS', moduleName: CORE}; static reserveSlots: o.ExternalReference = {name: 'ɵrS', moduleName: CORE};
// sanitization-related functions
static sanitizeHtml: o.ExternalReference = {name: 'ɵzh', moduleName: CORE};
static sanitizeStyle: o.ExternalReference = {name: 'ɵzs', moduleName: CORE};
static defaultStyleSanitizer: o.ExternalReference = {name: 'ɵzss', moduleName: CORE};
static sanitizeResourceUrl: o.ExternalReference = {name: 'ɵzr', moduleName: CORE};
static sanitizeScript: o.ExternalReference = {name: 'ɵzc', moduleName: CORE};
static sanitizeUrl: o.ExternalReference = {name: 'ɵzu', moduleName: CORE};
} }

View File

@ -368,10 +368,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} }
}); });
let hasMapBasedStyling = false;
for (let i = 0; i < styleInputs.length; i++) { for (let i = 0; i < styleInputs.length; i++) {
const input = styleInputs[i]; const input = styleInputs[i];
const isMapBasedStyleBinding = i === 0 && input.name === 'style'; const isMapBasedStyleBinding = i === 0 && input.name === 'style';
if (!isMapBasedStyleBinding && !stylesIndexMap.hasOwnProperty(input.name)) { if (isMapBasedStyleBinding) {
hasMapBasedStyling = true;
} else if (!stylesIndexMap.hasOwnProperty(input.name)) {
stylesIndexMap[input.name] = currStyleIndex++; stylesIndexMap[input.name] = currStyleIndex++;
} }
} }
@ -384,9 +387,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} }
} }
// in the event that a [style] binding is used then sanitization will
// always be imported because it is not possible to know ahead of time
// whether style bindings will use or not use any sanitizable properties
// that isStyleSanitizable() will detect
let useDefaultStyleSanitizer = hasMapBasedStyling;
// this will build the instructions so that they fall into the following syntax // this will build the instructions so that they fall into the following syntax
// => [prop1, prop2, prop3, 0, prop1, value1, prop2, value2] // => [prop1, prop2, prop3, 0, prop1, value1, prop2, value2]
Object.keys(stylesIndexMap).forEach(prop => { Object.keys(stylesIndexMap).forEach(prop => {
useDefaultStyleSanitizer = useDefaultStyleSanitizer || isStyleSanitizable(prop);
initialStyleDeclarations.push(o.literal(prop)); initialStyleDeclarations.push(o.literal(prop));
}); });
@ -473,18 +483,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (hasStylingInstructions) { if (hasStylingInstructions) {
const paramsList: (o.Expression)[] = []; const paramsList: (o.Expression)[] = [];
if (initialStyleDeclarations.length) {
// the template compiler handles initial style (e.g. style="foo") values
// in a special command called `elementStyle` so that the initial styles
// can be processed during runtime. These initial styles values are bound to
// a constant because the inital style values do not change (since they're static).
paramsList.push(
this.constantPool.getConstLiteral(o.literalArr(initialStyleDeclarations), true));
} else if (initialClassDeclarations.length) {
// no point in having an extra `null` value unless there are follow-up params
paramsList.push(o.NULL_EXPR);
}
if (initialClassDeclarations.length) { if (initialClassDeclarations.length) {
// the template compiler handles initial class styling (e.g. class="foo") values // the template compiler handles initial class styling (e.g. class="foo") values
// in a special command called `elementClass` so that the initial class // in a special command called `elementClass` so that the initial class
@ -492,6 +490,26 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// a constant because the inital class values do not change (since they're static). // a constant because the inital class values do not change (since they're static).
paramsList.push( paramsList.push(
this.constantPool.getConstLiteral(o.literalArr(initialClassDeclarations), true)); this.constantPool.getConstLiteral(o.literalArr(initialClassDeclarations), true));
} else if (initialStyleDeclarations.length || useDefaultStyleSanitizer) {
// no point in having an extra `null` value unless there are follow-up params
paramsList.push(o.NULL_EXPR);
}
if (initialStyleDeclarations.length) {
// the template compiler handles initial style (e.g. style="foo") values
// in a special command called `elementStyle` so that the initial styles
// can be processed during runtime. These initial styles values are bound to
// a constant because the inital style values do not change (since they're static).
paramsList.push(
this.constantPool.getConstLiteral(o.literalArr(initialStyleDeclarations), true));
} else if (useDefaultStyleSanitizer) {
// no point in having an extra `null` value unless there are follow-up params
paramsList.push(o.NULL_EXPR);
}
if (useDefaultStyleSanitizer) {
paramsList.push(o.importExpr(R3.defaultStyleSanitizer));
} }
this._creationCode.push(o.importExpr(R3.elementStyling).callFn(paramsList).toStmt()); this._creationCode.push(o.importExpr(R3.elementStyling).callFn(paramsList).toStmt());
@ -532,13 +550,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const stylingInput = mapBasedStyleInput || mapBasedClassInput; const stylingInput = mapBasedStyleInput || mapBasedClassInput;
if (stylingInput) { if (stylingInput) {
const params: o.Expression[] = []; const params: o.Expression[] = [];
if (mapBasedStyleInput) {
params.push(this.convertPropertyBinding(implicit, mapBasedStyleInput.value, true));
} else if (mapBasedClassInput) {
params.push(o.NULL_EXPR);
}
if (mapBasedClassInput) { if (mapBasedClassInput) {
params.push(this.convertPropertyBinding(implicit, mapBasedClassInput.value, true)); params.push(this.convertPropertyBinding(implicit, mapBasedClassInput.value, true));
} else if (mapBasedStyleInput) {
params.push(o.NULL_EXPR);
}
if (mapBasedStyleInput) {
params.push(this.convertPropertyBinding(implicit, mapBasedStyleInput.value, true));
} }
this.instruction( this.instruction(
this._bindingCode, stylingInput.sourceSpan, R3.elementStylingMap, indexLiteral, this._bindingCode, stylingInput.sourceSpan, R3.elementStylingMap, indexLiteral,
@ -551,11 +569,17 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
for (i; i < styleInputs.length; i++) { for (i; i < styleInputs.length; i++) {
const input = styleInputs[i]; const input = styleInputs[i];
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true); const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
const params = [convertedBinding];
const sanitizationRef = resolveSanitizationFn(input, input.securityContext);
if (sanitizationRef) {
params.push(sanitizationRef);
}
const key = input.name; const key = input.name;
const styleIndex: number = stylesIndexMap[key] !; const styleIndex: number = stylesIndexMap[key] !;
this.instruction( this.instruction(
this._bindingCode, input.sourceSpan, R3.elementStyleProp, indexLiteral, this._bindingCode, input.sourceSpan, R3.elementStyleProp, indexLiteral,
o.literal(styleIndex), convertedBinding); o.literal(styleIndex), ...params);
} }
lastInputCommand = styleInputs[styleInputs.length - 1]; lastInputCommand = styleInputs[styleInputs.length - 1];
@ -566,11 +590,17 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
for (i; i < classInputs.length; i++) { for (i; i < classInputs.length; i++) {
const input = classInputs[i]; const input = classInputs[i];
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true); const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
const params = [convertedBinding];
const sanitizationRef = resolveSanitizationFn(input, input.securityContext);
if (sanitizationRef) {
params.push(sanitizationRef);
}
const key = input.name; const key = input.name;
const classIndex: number = classesIndexMap[key] !; const classIndex: number = classesIndexMap[key] !;
this.instruction( this.instruction(
this._bindingCode, input.sourceSpan, R3.elementClassProp, indexLiteral, this._bindingCode, input.sourceSpan, R3.elementClassProp, indexLiteral,
o.literal(classIndex), convertedBinding); o.literal(classIndex), ...params);
} }
lastInputCommand = classInputs[classInputs.length - 1]; lastInputCommand = classInputs[classInputs.length - 1];
@ -588,12 +618,19 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} }
const convertedBinding = this.convertPropertyBinding(implicit, input.value); const convertedBinding = this.convertPropertyBinding(implicit, input.value);
const instruction = mapBindingToInstruction(input.type); const instruction = mapBindingToInstruction(input.type);
if (instruction) { if (instruction) {
const params = [convertedBinding];
const sanitizationRef = resolveSanitizationFn(input, input.securityContext);
if (sanitizationRef) {
params.push(sanitizationRef);
}
// TODO(chuckj): runtime: security context? // TODO(chuckj): runtime: security context?
this.instruction( this.instruction(
this._bindingCode, input.sourceSpan, instruction, o.literal(elementIndex), this._bindingCode, input.sourceSpan, instruction, o.literal(elementIndex),
o.literal(input.name), convertedBinding); o.literal(input.name), ...params);
} else { } else {
this._unsupported(`binding type ${input.type}`); this._unsupported(`binding type ${input.type}`);
} }
@ -1061,3 +1098,36 @@ export function makeBindingParser(): BindingParser {
function isClassBinding(input: t.BoundAttribute): boolean { function isClassBinding(input: t.BoundAttribute): boolean {
return input.name == 'className' || input.name == 'class'; return input.name == 'className' || input.name == 'class';
} }
function resolveSanitizationFn(input: t.BoundAttribute, context: core.SecurityContext) {
switch (context) {
case core.SecurityContext.HTML:
return o.importExpr(R3.sanitizeHtml);
case core.SecurityContext.SCRIPT:
return o.importExpr(R3.sanitizeScript);
case core.SecurityContext.STYLE:
// the compiler does not fill in an instruction for [style.prop?] binding
// values because the style algorithm knows internally what props are subject
// to sanitization (only [attr.style] values are explicitly sanitized)
return input.type === BindingType.Attribute ? o.importExpr(R3.sanitizeStyle) : null;
case core.SecurityContext.URL:
return o.importExpr(R3.sanitizeUrl);
case core.SecurityContext.RESOURCE_URL:
return o.importExpr(R3.sanitizeResourceUrl);
default:
return null;
}
}
function isStyleSanitizable(prop: string): boolean {
switch (prop) {
case 'background-image':
case 'background':
case 'border-image':
case 'filter':
case 'list-style':
case 'list-style-image':
return true;
}
return false;
}

View File

@ -102,14 +102,16 @@ export {
} from './render3/index'; } from './render3/index';
export {NgModuleDef as ɵNgModuleDef} from './metadata/ng_module'; export {NgModuleDef as ɵNgModuleDef} from './metadata/ng_module';
export { export {
bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml,
bypassSanitizationTrustStyle as ɵbypassSanitizationTrustStyle,
bypassSanitizationTrustScript as ɵbypassSanitizationTrustScript,
bypassSanitizationTrustUrl as ɵbypassSanitizationTrustUrl,
bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl,
sanitizeHtml as ɵsanitizeHtml, sanitizeHtml as ɵsanitizeHtml,
sanitizeStyle as ɵsanitizeStyle, sanitizeStyle as ɵsanitizeStyle,
sanitizeUrl as ɵsanitizeUrl, sanitizeUrl as ɵsanitizeUrl,
sanitizeResourceUrl as ɵsanitizeResourceUrl, sanitizeResourceUrl as ɵsanitizeResourceUrl,
} from './sanitization/sanitization'; } from './sanitization/sanitization';
export {
bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml,
bypassSanitizationTrustStyle as ɵbypassSanitizationTrustStyle,
bypassSanitizationTrustScript as ɵbypassSanitizationTrustScript,
bypassSanitizationTrustUrl as ɵbypassSanitizationTrustUrl,
bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl,
} from './sanitization/bypass';
// clang-format on // clang-format on

View File

@ -10,6 +10,7 @@ import './ng_dev_mode';
import {QueryList} from '../linker'; import {QueryList} from '../linker';
import {Sanitizer} from '../sanitization/security'; import {Sanitizer} from '../sanitization/security';
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual} from './assert'; import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual} from './assert';
import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors';
@ -25,7 +26,7 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, Curre
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation'; import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {StylingContext, StylingIndex, allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling'; import {StylingContext, allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling';
import {assertDataInRangeInternal, isDifferent, loadElementInternal, loadInternal, stringify} from './util'; import {assertDataInRangeInternal, isDifferent, loadElementInternal, loadInternal, stringify} from './util';
import {ViewRef} from './view_ref'; import {ViewRef} from './view_ref';
@ -1291,25 +1292,29 @@ export function elementClassProp<T>(
* (Note that this is not the element index, but rather an index value allocated * (Note that this is not the element index, but rather an index value allocated
* specifically for element styling--the index must be the next index after the element * specifically for element styling--the index must be the next index after the element
* index.) * index.)
* @param styleDeclarations A key/value array of CSS styles that will be registered on the element.
* Each individual style will be used on the element as long as it is not overridden
* by any styles placed on the element by multiple (`[style]`) or singular (`[style.prop]`)
* bindings. If a style binding changes its value to null then the initial styling
* values that are passed in here will be applied to the element (if matched).
* @param classDeclarations A key/value array of CSS classes that will be registered on the element. * @param classDeclarations A key/value array of CSS classes that will be registered on the element.
* Each individual style will be used on the element as long as it is not overridden * Each individual style will be used on the element as long as it is not overridden
* by any classes placed on the element by multiple (`[class]`) or singular (`[class.named]`) * by any classes placed on the element by multiple (`[class]`) or singular (`[class.named]`)
* bindings. If a class binding changes its value to a falsy value then the matching initial * bindings. If a class binding changes its value to a falsy value then the matching initial
* class value that are passed in here will be applied to the element (if matched). * class value that are passed in here will be applied to the element (if matched).
* @param styleDeclarations A key/value array of CSS styles that will be registered on the element.
* Each individual style will be used on the element as long as it is not overridden
* by any styles placed on the element by multiple (`[style]`) or singular (`[style.prop]`)
* bindings. If a style binding changes its value to null then the initial styling
* values that are passed in here will be applied to the element (if matched).
* @param styleSanitizer An optional sanitizer function that will be used (if provided)
* to sanitize the any CSS property values that are applied to the element (during rendering).
*/ */
export function elementStyling<T>( export function elementStyling<T>(
classDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
classDeclarations?: (string | boolean | InitialStylingFlags)[] | null): void { styleSanitizer?: StyleSanitizeFn | null): void {
const lElement = currentElementNode !; const lElement = currentElementNode !;
const tNode = lElement.tNode; const tNode = lElement.tNode;
if (!tNode.stylingTemplate) { if (!tNode.stylingTemplate) {
// initialize the styling template. // initialize the styling template.
tNode.stylingTemplate = createStylingContextTemplate(styleDeclarations, classDeclarations); tNode.stylingTemplate =
createStylingContextTemplate(classDeclarations, styleDeclarations, styleSanitizer);
} }
if (styleDeclarations && styleDeclarations.length || if (styleDeclarations && styleDeclarations.length ||
classDeclarations && classDeclarations.length) { classDeclarations && classDeclarations.length) {
@ -1377,22 +1382,23 @@ export function elementStylingApply<T>(index: number): void {
* renaming as part of minification. * renaming as part of minification.
* @param value New value to write (null to remove). * @param value New value to write (null to remove).
* @param suffix Optional suffix. Used with scalar values to add unit such as `px`. * @param suffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param sanitizer An optional function used to transform the value typically used for * Note that when a suffix is provided then the underlying sanitizer will
* sanitization. * be ignored.
*/ */
export function elementStyleProp<T>( export function elementStyleProp<T>(
index: number, styleIndex: number, value: T | null, suffix?: string): void; index: number, styleIndex: number, value: T | null, suffix?: string): void {
export function elementStyleProp<T>(
index: number, styleIndex: number, value: T | null, sanitizer?: SanitizerFn): void;
export function elementStyleProp<T>(
index: number, styleIndex: number, value: T | null,
suffixOrSanitizer?: string | SanitizerFn): void {
let valueToAdd: string|null = null; let valueToAdd: string|null = null;
if (value) { if (value) {
valueToAdd = if (suffix) {
typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value); // when a suffix is applied then it will bypass
if (typeof suffixOrSanitizer == 'string') { // sanitization entirely (b/c a new string is created)
valueToAdd = valueToAdd + suffixOrSanitizer; valueToAdd = stringify(value) + suffix;
} else {
// sanitization happens by dealing with a String value
// this means that the string value will be passed through
// into the style rendering later (which is where the value
// will be sanitized before it is applied)
valueToAdd = value as any as string;
} }
} }
updateElementStyleProp(getStylingContext(index), styleIndex, valueToAdd); updateElementStyleProp(getStylingContext(index), styleIndex, valueToAdd);
@ -1412,17 +1418,17 @@ export function elementStyleProp<T>(
* (Note that this is not the element index, but rather an index value allocated * (Note that this is not the element index, but rather an index value allocated
* specifically for element styling--the index must be the next index after the element * specifically for element styling--the index must be the next index after the element
* index.) * index.)
* @param styles A key/value style map of the styles that will be applied to the given element.
* Any missing styles (that have already been applied to the element beforehand) will be
* removed (unset) from the element's styling.
* @param classes A key/value style map of CSS classes that will be added to the given element. * @param classes A key/value style map of CSS classes that will be added to the given element.
* Any missing classes (that have already been applied to the element beforehand) will be * Any missing classes (that have already been applied to the element beforehand) will be
* removed (unset) from the element's list of CSS classes. * removed (unset) from the element's list of CSS classes.
* @param styles A key/value style map of the styles that will be applied to the given element.
* Any missing styles (that have already been applied to the element beforehand) will be
* removed (unset) from the element's styling.
*/ */
export function elementStylingMap<T>( export function elementStylingMap<T>(
index: number, styles: {[styleName: string]: any} | null, index: number, classes: {[key: string]: any} | string | null,
classes?: {[key: string]: any} | string | null): void { styles?: {[styleName: string]: any} | null): void {
updateStylingMap(getStylingContext(index), styles, classes); updateStylingMap(getStylingContext(index), classes, styles);
} }
////////////////////////// //////////////////////////

View File

@ -9,6 +9,7 @@
import {defineInjectable, defineInjector,} from '../../di/defs'; import {defineInjectable, defineInjector,} from '../../di/defs';
import {inject} from '../../di/injector'; import {inject} from '../../di/injector';
import * as r3 from '../index'; import * as r3 from '../index';
import * as sanitization from '../../sanitization/sanitization';
/** /**
@ -88,4 +89,11 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵt': r3.t, 'ɵt': r3.t,
'ɵV': r3.V, 'ɵV': r3.V,
'ɵv': r3.v, 'ɵv': r3.v,
'ɵzh': sanitization.sanitizeHtml,
'ɵzs': sanitization.sanitizeStyle,
'ɵzss': sanitization.defaultStyleSanitizer,
'ɵzr': sanitization.sanitizeResourceUrl,
'ɵzc': sanitization.sanitizeScript,
'ɵzu': sanitization.sanitizeUrl
}; };

View File

@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {InitialStylingFlags} from './interfaces/definition'; import {InitialStylingFlags} from './interfaces/definition';
import {LElementNode} from './interfaces/node'; import {LElementNode} from './interfaces/node';
import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer';
/** /**
* The styling context acts as a styling manifest (shaped as an array) for determining which * The styling context acts as a styling manifest (shaped as an array) for determining which
* styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp` * styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp`
@ -51,42 +53,44 @@ import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces
* *
* ``` * ```
* context = [ * context = [
* element,
* styleSanitizer | null,
* [null, '100px', '200px', true], // property names are not needed since they have already been * [null, '100px', '200px', true], // property names are not needed since they have already been
* written to DOM. * written to DOM.
* *
* configMasterVal,
* 1, // this instructs how many `style` values there are so that class index values can be * 1, // this instructs how many `style` values there are so that class index values can be
* offsetted * offsetted
* * 'last class string applied',
* configMasterVal,
*
* // 3
* 'width',
* pointers(1, 12); // Point to static `width`: `100px` and multi `width`.
* null,
* *
* // 6 * // 6
* 'height', * 'width',
* pointers(2, 15); // Point to static `height`: `200px` and multi `height`. * pointers(1, 15); // Point to static `width`: `100px` and multi `width`.
* null, * null,
* *
* // 9 * // 9
* 'foo', * 'height',
* pointers(1, 18); // Point to static `foo`: `true` and multi `foo`. * pointers(2, 18); // Point to static `height`: `200px` and multi `height`.
* null, * null,
* *
* // 12 * // 12
* 'width', * 'foo',
* pointers(1, 3); // Point to static `width`: `100px` and single `width`. * pointers(1, 21); // Point to static `foo`: `true` and multi `foo`.
* null, * null,
* *
* // 15 * // 15
* 'height', * 'width',
* pointers(2, 6); // Point to static `height`: `200px` and single `height`. * pointers(1, 6); // Point to static `width`: `100px` and single `width`.
* null, * null,
* *
* // 18 * // 18
* 'height',
* pointers(2, 9); // Point to static `height`: `200px` and single `height`.
* null,
*
* // 21
* 'foo', * 'foo',
* pointers(3, 9); // Point to static `foo`: `true` and single `foo`. * pointers(3, 12); // Point to static `foo`: `true` and single `foo`.
* null, * null,
* ] * ]
* *
@ -111,36 +115,41 @@ import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces
* `updateStylingMap` can include new CSS properties that will be added to the context). * `updateStylingMap` can include new CSS properties that will be added to the context).
*/ */
export interface StylingContext extends export interface StylingContext extends
Array<InitialStyles|number|string|boolean|LElementNode|null> { Array<InitialStyles|number|string|boolean|LElementNode|StyleSanitizeFn|null> {
/** /**
* Location of element that is used as a target for this context. * Location of element that is used as a target for this context.
*/ */
[0]: LElementNode|null; [0]: LElementNode|null;
/**
* The style sanitizer that is used within this context
*/
[1]: StyleSanitizeFn|null;
/** /**
* Location of initial data shared by all instances of this style. * Location of initial data shared by all instances of this style.
*/ */
[1]: InitialStyles; [2]: InitialStyles;
/** /**
* A numeric value representing the configuration status (whether the context is dirty or not) * A numeric value representing the configuration status (whether the context is dirty or not)
* mixed together (using bit shifting) with a index value which tells the starting index value * mixed together (using bit shifting) with a index value which tells the starting index value
* of where the multi style entries begin. * of where the multi style entries begin.
*/ */
[2]: number; [3]: number;
/** /**
* A numeric value representing the class index offset value. Whenever a single class is * A numeric value representing the class index offset value. Whenever a single class is
* applied (using `elementClassProp`) it should have an styling index value that doesn't * applied (using `elementClassProp`) it should have an styling index value that doesn't
* need to take into account any style values that exist in the context. * need to take into account any style values that exist in the context.
*/ */
[3]: number; [4]: number;
/** /**
* The last CLASS STRING VALUE that was interpreted by elementStylingMap. This is cached * The last CLASS STRING VALUE that was interpreted by elementStylingMap. This is cached
* So that the algorithm can exit early incase the string has not changed. * So that the algorithm can exit early incase the string has not changed.
*/ */
[4]: string|null; [5]: string|null;
} }
/** /**
@ -159,31 +168,35 @@ export interface InitialStyles extends Array<string|null|boolean> { [0]: null; }
*/ */
export const enum StylingFlags { export const enum StylingFlags {
// Implies no configurations // Implies no configurations
None = 0b00, None = 0b000,
// Whether or not the entry or context itself is dirty // Whether or not the entry or context itself is dirty
Dirty = 0b01, Dirty = 0b001,
// Whether or not this is a class-based assignment // Whether or not this is a class-based assignment
Class = 0b10, Class = 0b010,
// Whether or not a sanitizer was applied to this property
Sanitize = 0b100,
// The max amount of bits used to represent these configuration values // The max amount of bits used to represent these configuration values
BitCountSize = 2, BitCountSize = 3,
// There are only two bits here // There are only three bits here
BitMask = 0b11 BitMask = 0b111
} }
/** Used as numeric pointer values to determine what cells to update in the `StylingContext` */ /** Used as numeric pointer values to determine what cells to update in the `StylingContext` */
export const enum StylingIndex { export const enum StylingIndex {
// Position of where the initial styles are stored in the styling context // Position of where the initial styles are stored in the styling context
ElementPosition = 0, ElementPosition = 0,
// Position of where the style sanitizer is stored within the styling context
StyleSanitizerPosition = 1,
// Position of where the initial styles are stored in the styling context // Position of where the initial styles are stored in the styling context
InitialStylesPosition = 1, InitialStylesPosition = 2,
// Index of location where the start of single properties are stored. (`updateStyleProp`) // Index of location where the start of single properties are stored. (`updateStyleProp`)
MasterFlagPosition = 2, MasterFlagPosition = 3,
// Index of location where the class index offset value is located // Index of location where the class index offset value is located
ClassOffsetPosition = 3, ClassOffsetPosition = 4,
// Position of where the last string-based CSS class value was stored // Position of where the last string-based CSS class value was stored
CachedCssClassString = 4, CachedCssClassString = 5,
// Location of single (prop) value entries are stored within the context // Location of single (prop) value entries are stored within the context
SingleStylesStartPosition = 5, SingleStylesStartPosition = 6,
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue // Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue
FlagsOffset = 0, FlagsOffset = 0,
PropertyOffset = 1, PropertyOffset = 1,
@ -191,9 +204,9 @@ export const enum StylingIndex {
// Size of each multi or single entry (flag + prop + value) // Size of each multi or single entry (flag + prop + value)
Size = 3, Size = 3,
// Each flag has a binary digit length of this value // Each flag has a binary digit length of this value
BitCountSize = 15, // (32 - 1) / 2 = ~15 BitCountSize = 14, // (32 - 3) / 2 = ~14
// The binary digit value as a mask // The binary digit value as a mask
BitMask = 0b111111111111111 // 15 bits BitMask = 0b11111111111111 // 14 bits
} }
/** /**
@ -233,10 +246,11 @@ export function allocStylingContext(
* class will be applied to the element as an initial class since it's true * class will be applied to the element as an initial class since it's true
*/ */
export function createStylingContextTemplate( export function createStylingContextTemplate(
initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null): StylingContext { styleSanitizer?: StyleSanitizeFn | null): StylingContext {
const initialStylingValues: InitialStyles = [null]; const initialStylingValues: InitialStyles = [null];
const context: StylingContext = [null, initialStylingValues, 0, 0, null]; const context: StylingContext = [null, styleSanitizer || null, initialStylingValues, 0, 0, null];
// we use two maps since a class name might collide with a CSS style prop // we use two maps since a class name might collide with a CSS style prop
const stylesLookup: {[key: string]: number} = {}; const stylesLookup: {[key: string]: number} = {};
@ -314,7 +328,7 @@ export function createStylingContextTemplate(
const indexForMulti = i * StylingIndex.Size + multiStart; const indexForMulti = i * StylingIndex.Size + multiStart;
const indexForSingle = i * StylingIndex.Size + singleStart; const indexForSingle = i * StylingIndex.Size + singleStart;
const initialFlag = isClassBased ? StylingFlags.Class : StylingFlags.None; const initialFlag = prepareInitialFlag(prop, isClassBased, styleSanitizer || null);
setFlag(context, indexForSingle, pointers(initialFlag, indexForInitial, indexForMulti)); setFlag(context, indexForSingle, pointers(initialFlag, indexForInitial, indexForMulti));
setProp(context, indexForSingle, prop); setProp(context, indexForSingle, prop);
@ -347,12 +361,12 @@ const EMPTY_OBJ: {[key: string]: any} = {};
* *
* @param context The styling context that will be updated with the * @param context The styling context that will be updated with the
* newly provided style values. * newly provided style values.
* @param styles The key/value map of CSS styles that will be used for the update.
* @param classes The key/value map of CSS class names that will be used for the update. * @param classes The key/value map of CSS class names that will be used for the update.
* @param styles The key/value map of CSS styles that will be used for the update.
*/ */
export function updateStylingMap( export function updateStylingMap(
context: StylingContext, styles: {[key: string]: any} | null, context: StylingContext, classes: {[key: string]: any} | string | null,
classes?: {[key: string]: any} | string | null): void { styles?: {[key: string]: any} | null): void {
let classNames: string[] = EMPTY_ARR; let classNames: string[] = EMPTY_ARR;
let applyAllClasses = false; let applyAllClasses = false;
let ignoreAllClassUpdates = false; let ignoreAllClassUpdates = false;
@ -407,10 +421,10 @@ export function updateStylingMap(
const prop = getProp(context, ctxIndex); const prop = getProp(context, ctxIndex);
if (prop === newProp) { if (prop === newProp) {
const value = getValue(context, ctxIndex); const value = getValue(context, ctxIndex);
if (value !== newValue) { const flag = getPointers(context, ctxIndex);
if (hasValueChanged(flag, value, newValue)) {
setValue(context, ctxIndex, newValue); setValue(context, ctxIndex, newValue);
const flag = getPointers(context, ctxIndex);
const initialValue = getInitialValue(context, flag); const initialValue = getInitialValue(context, flag);
// there is no point in setting this to dirty if the previously // there is no point in setting this to dirty if the previously
@ -437,7 +451,8 @@ export function updateStylingMap(
} }
} else { } else {
// we only care to do this if the insertion is in the middle // we only care to do this if the insertion is in the middle
insertNewMultiProperty(context, ctxIndex, isClassBased, newProp, newValue); const newFlag = prepareInitialFlag(newProp, isClassBased, getStyleSanitizer(context));
insertNewMultiProperty(context, ctxIndex, isClassBased, newProp, newFlag, newValue);
dirty = true; dirty = true;
} }
} }
@ -468,6 +483,7 @@ export function updateStylingMap(
// this means that there are left-over properties in the context that // this means that there are left-over properties in the context that
// were not detected in the context during the loop above. In that // were not detected in the context during the loop above. In that
// case we want to add the new entries into the list // case we want to add the new entries into the list
const sanitizer = getStyleSanitizer(context);
while (propIndex < propLimit) { while (propIndex < propLimit) {
const isClassBased = propIndex >= classesStartIndex; const isClassBased = propIndex >= classesStartIndex;
if (ignoreAllClassUpdates && isClassBased) break; if (ignoreAllClassUpdates && isClassBased) break;
@ -476,7 +492,7 @@ export function updateStylingMap(
const prop = isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex]; const prop = isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex];
const value: string|boolean = const value: string|boolean =
isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop]; isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop];
const flag = StylingFlags.Dirty | (isClassBased ? StylingFlags.Class : StylingFlags.None); const flag = prepareInitialFlag(prop, isClassBased, sanitizer) | StylingFlags.Dirty;
context.push(flag, prop, value); context.push(flag, prop, value);
propIndex++; propIndex++;
dirty = true; dirty = true;
@ -508,7 +524,7 @@ export function updateStyleProp(
const currFlag = getPointers(context, singleIndex); const currFlag = getPointers(context, singleIndex);
// didn't change ... nothing to make a note of // didn't change ... nothing to make a note of
if (currValue !== value) { if (hasValueChanged(currFlag, currValue, value)) {
// the value will always get updated (even if the dirty flag is skipped) // the value will always get updated (even if the dirty flag is skipped)
setValue(context, singleIndex, value); setValue(context, singleIndex, value);
const indexForMulti = getMultiOrSingleIndex(currFlag); const indexForMulti = getMultiOrSingleIndex(currFlag);
@ -573,6 +589,7 @@ export function renderStyling(
if (isContextDirty(context)) { if (isContextDirty(context)) {
const native = context[StylingIndex.ElementPosition] !.native; const native = context[StylingIndex.ElementPosition] !.native;
const multiStartIndex = getMultiStartIndex(context); const multiStartIndex = getMultiStartIndex(context);
const styleSanitizer = getStyleSanitizer(context);
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length; for (let i = StylingIndex.SingleStylesStartPosition; i < context.length;
i += StylingIndex.Size) { i += StylingIndex.Size) {
// there is no point in rendering styles that have not changed on screen // there is no point in rendering styles that have not changed on screen
@ -607,7 +624,8 @@ export function renderStyling(
if (isClassBased) { if (isClassBased) {
setClass(native, prop, valueToApply ? true : false, renderer, classStore); setClass(native, prop, valueToApply ? true : false, renderer, classStore);
} else { } else {
setStyle(native, prop, valueToApply as string | null, renderer, styleStore); const sanitizer = (flag & StylingFlags.Sanitize) ? styleSanitizer : null;
setStyle(native, prop, valueToApply as string | null, renderer, sanitizer, styleStore);
} }
setDirty(context, i, false); setDirty(context, i, false);
} }
@ -631,7 +649,8 @@ export function renderStyling(
*/ */
function setStyle( function setStyle(
native: any, prop: string, value: string | null, renderer: Renderer3, native: any, prop: string, value: string | null, renderer: Renderer3,
store?: {[key: string]: any}) { sanitizer: StyleSanitizeFn | null, store?: {[key: string]: any}) {
value = sanitizer && value ? sanitizer(prop, value) : value;
if (store) { if (store) {
store[prop] = value; store[prop] = value;
} else if (value) { } else if (value) {
@ -697,6 +716,12 @@ function isClassBased(context: StylingContext, index: number): boolean {
return ((context[adjustedIndex] as number) & StylingFlags.Class) == StylingFlags.Class; return ((context[adjustedIndex] as number) & StylingFlags.Class) == StylingFlags.Class;
} }
function isSanitizable(context: StylingContext, index: number): boolean {
const adjustedIndex =
index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index;
return ((context[adjustedIndex] as number) & StylingFlags.Sanitize) == StylingFlags.Sanitize;
}
function pointers(configFlag: number, staticIndex: number, dynamicIndex: number) { function pointers(configFlag: number, staticIndex: number, dynamicIndex: number) {
return (configFlag & StylingFlags.BitMask) | (staticIndex << StylingFlags.BitCountSize) | return (configFlag & StylingFlags.BitMask) | (staticIndex << StylingFlags.BitCountSize) |
(dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize)); (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize));
@ -721,6 +746,10 @@ function getMultiStartIndex(context: StylingContext): number {
return getMultiOrSingleIndex(context[StylingIndex.MasterFlagPosition]) as number; return getMultiOrSingleIndex(context[StylingIndex.MasterFlagPosition]) as number;
} }
function getStyleSanitizer(context: StylingContext): StyleSanitizeFn|null {
return context[StylingIndex.StyleSanitizerPosition];
}
function setProp(context: StylingContext, index: number, prop: string) { function setProp(context: StylingContext, index: number, prop: string) {
context[index + StylingIndex.PropertyOffset] = prop; context[index + StylingIndex.PropertyOffset] = prop;
} }
@ -808,7 +837,8 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition:
const singleFlag = getPointers(context, singleIndex); const singleFlag = getPointers(context, singleIndex);
const initialIndexForSingle = getInitialIndex(singleFlag); const initialIndexForSingle = getInitialIndex(singleFlag);
const flagValue = (isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None) | const flagValue = (isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None) |
(isClassBased(context, singleIndex) ? StylingFlags.Class : StylingFlags.None); (isClassBased(context, singleIndex) ? StylingFlags.Class : StylingFlags.None) |
(isSanitizable(context, singleIndex) ? StylingFlags.Sanitize : StylingFlags.None);
const updatedFlag = pointers(flagValue, initialIndexForSingle, i); const updatedFlag = pointers(flagValue, initialIndexForSingle, i);
setFlag(context, singleIndex, updatedFlag); setFlag(context, singleIndex, updatedFlag);
} }
@ -816,14 +846,14 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition:
} }
function insertNewMultiProperty( function insertNewMultiProperty(
context: StylingContext, index: number, classBased: boolean, name: string, context: StylingContext, index: number, classBased: boolean, name: string, flag: number,
value: string | boolean): void { value: string | boolean): void {
const doShift = index < context.length; const doShift = index < context.length;
// prop does not exist in the list, add it in // prop does not exist in the list, add it in
context.splice( context.splice(
index, 0, StylingFlags.Dirty | (classBased ? StylingFlags.Class : StylingFlags.None), name, index, 0, flag | StylingFlags.Dirty | (classBased ? StylingFlags.Class : StylingFlags.None),
value); name, value);
if (doShift) { if (doShift) {
// because the value was inserted midway into the array then we // because the value was inserted midway into the array then we
@ -839,3 +869,30 @@ function valueExists(value: string | null | boolean, isClassBased?: boolean) {
} }
return value !== null; return value !== null;
} }
function prepareInitialFlag(
name: string, isClassBased: boolean, sanitizer?: StyleSanitizeFn | null) {
if (isClassBased) {
return StylingFlags.Class;
} else if (sanitizer && sanitizer(name)) {
return StylingFlags.Sanitize;
}
return StylingFlags.None;
}
function hasValueChanged(
flag: number, a: string | boolean | null, b: string | boolean | null): boolean {
const isClassBased = flag & StylingFlags.Class;
const hasValues = a && b;
const usesSanitizer = flag & StylingFlags.Sanitize;
// the toString() comparison ensures that a value is checked
// ... otherwise (during sanitization bypassing) the === comparsion
// would fail since a new String() instance is created
if (!isClassBased && hasValues && usesSanitizer) {
// we know for sure we're dealing with strings at this point
return (a as string).toString() !== (b as string).toString();
}
// everything else is safe to check with a normal equality check
return a !== b;
}

View File

@ -0,0 +1,143 @@
/**
* @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
*/
const BRAND = '__SANITIZER_TRUSTED_BRAND__';
export const enum BypassType {
Url = 'Url',
Html = 'Html',
ResourceUrl = 'ResourceUrl',
Script = 'Script',
Style = 'Style',
}
/**
* A branded trusted string used with sanitization.
*
* See: {@link TrustedHtmlString}, {@link TrustedResourceUrlString}, {@link TrustedScriptString},
* {@link TrustedStyleString}, {@link TrustedUrlString}
*/
export interface TrustedString extends String { [BRAND]: BypassType; }
/**
* A branded trusted string used with sanitization of `html` strings.
*
* See: {@link bypassSanitizationTrustHtml} and {@link htmlSanitizer}.
*/
export interface TrustedHtmlString extends TrustedString { [BRAND]: BypassType.Html; }
/**
* A branded trusted string used with sanitization of `style` strings.
*
* See: {@link bypassSanitizationTrustStyle} and {@link styleSanitizer}.
*/
export interface TrustedStyleString extends TrustedString { [BRAND]: BypassType.Style; }
/**
* A branded trusted string used with sanitization of `url` strings.
*
* See: {@link bypassSanitizationTrustScript} and {@link scriptSanitizer}.
*/
export interface TrustedScriptString extends TrustedString { [BRAND]: BypassType.Script; }
/**
* A branded trusted string used with sanitization of `url` strings.
*
* See: {@link bypassSanitizationTrustUrl} and {@link urlSanitizer}.
*/
export interface TrustedUrlString extends TrustedString { [BRAND]: BypassType.Url; }
/**
* A branded trusted string used with sanitization of `resourceUrl` strings.
*
* See: {@link bypassSanitizationTrustResourceUrl} and {@link resourceUrlSanitizer}.
*/
export interface TrustedResourceUrlString extends TrustedString { [BRAND]: BypassType.ResourceUrl; }
export function allowSanitizationBypass(value: any, type: BypassType): boolean {
return (value instanceof String && (value as TrustedStyleString)[BRAND] === type) ? true : false;
}
/**
* Mark `html` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link htmlSanitizer} to be trusted implicitly.
*
* @param trustedHtml `html` string which needs to be implicitly trusted.
* @returns a `html` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustHtml(trustedHtml: string): TrustedHtmlString {
return bypassSanitizationTrustString(trustedHtml, BypassType.Html);
}
/**
* Mark `style` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link styleSanitizer} to be trusted implicitly.
*
* @param trustedStyle `style` string which needs to be implicitly trusted.
* @returns a `style` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustStyle(trustedStyle: string): TrustedStyleString {
return bypassSanitizationTrustString(trustedStyle, BypassType.Style);
}
/**
* Mark `script` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link scriptSanitizer} to be trusted implicitly.
*
* @param trustedScript `script` string which needs to be implicitly trusted.
* @returns a `script` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustScript(trustedScript: string): TrustedScriptString {
return bypassSanitizationTrustString(trustedScript, BypassType.Script);
}
/**
* Mark `url` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link urlSanitizer} to be trusted implicitly.
*
* @param trustedUrl `url` string which needs to be implicitly trusted.
* @returns a `url` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustUrl(trustedUrl: string): TrustedUrlString {
return bypassSanitizationTrustString(trustedUrl, BypassType.Url);
}
/**
* Mark `url` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link resourceUrlSanitizer} to be trusted implicitly.
*
* @param trustedResourceUrl `url` string which needs to be implicitly trusted.
* @returns a `url` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string):
TrustedResourceUrlString {
return bypassSanitizationTrustString(trustedResourceUrl, BypassType.ResourceUrl);
}
function bypassSanitizationTrustString(
trustedString: string, mode: BypassType.Html): TrustedHtmlString;
function bypassSanitizationTrustString(
trustedString: string, mode: BypassType.Style): TrustedStyleString;
function bypassSanitizationTrustString(
trustedString: string, mode: BypassType.Script): TrustedScriptString;
function bypassSanitizationTrustString(
trustedString: string, mode: BypassType.Url): TrustedUrlString;
function bypassSanitizationTrustString(
trustedString: string, mode: BypassType.ResourceUrl): TrustedResourceUrlString;
function bypassSanitizationTrustString(trustedString: string, mode: BypassType): TrustedString {
const trusted = new String(trustedString) as TrustedString;
trusted[BRAND] = mode;
return trusted;
}

View File

@ -9,63 +9,13 @@
import {getCurrentSanitizer} from '../render3/instructions'; import {getCurrentSanitizer} from '../render3/instructions';
import {stringify} from '../render3/util'; import {stringify} from '../render3/util';
import {BypassType, allowSanitizationBypass} from './bypass';
import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer'; import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer';
import {SecurityContext} from './security'; import {SecurityContext} from './security';
import {_sanitizeStyle as _sanitizeStyle} from './style_sanitizer'; import {StyleSanitizeFn, _sanitizeStyle as _sanitizeStyle} from './style_sanitizer';
import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer'; import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer';
const BRAND = '__SANITIZER_TRUSTED_BRAND__';
/**
* A branded trusted string used with sanitization.
*
* See: {@link TrustedHtmlString}, {@link TrustedResourceUrlString}, {@link TrustedScriptString},
* {@link TrustedStyleString}, {@link TrustedUrlString}
*/
export interface TrustedString extends String {
'__SANITIZER_TRUSTED_BRAND__': 'Html'|'Style'|'Script'|'Url'|'ResourceUrl';
}
/**
* A branded trusted string used with sanitization of `html` strings.
*
* See: {@link bypassSanitizationTrustHtml} and {@link htmlSanitizer}.
*/
export interface TrustedHtmlString extends TrustedString { '__SANITIZER_TRUSTED_BRAND__': 'Html'; }
/**
* A branded trusted string used with sanitization of `style` strings.
*
* See: {@link bypassSanitizationTrustStyle} and {@link styleSanitizer}.
*/
export interface TrustedStyleString extends TrustedString {
'__SANITIZER_TRUSTED_BRAND__': 'Style';
}
/**
* A branded trusted string used with sanitization of `url` strings.
*
* See: {@link bypassSanitizationTrustScript} and {@link scriptSanitizer}.
*/
export interface TrustedScriptString extends TrustedString {
'__SANITIZER_TRUSTED_BRAND__': 'Script';
}
/**
* A branded trusted string used with sanitization of `url` strings.
*
* See: {@link bypassSanitizationTrustUrl} and {@link urlSanitizer}.
*/
export interface TrustedUrlString extends TrustedString { '__SANITIZER_TRUSTED_BRAND__': 'Url'; }
/**
* A branded trusted string used with sanitization of `resourceUrl` strings.
*
* See: {@link bypassSanitizationTrustResourceUrl} and {@link resourceUrlSanitizer}.
*/
export interface TrustedResourceUrlString extends TrustedString {
'__SANITIZER_TRUSTED_BRAND__': 'ResourceUrl';
}
/** /**
* An `html` sanitizer which converts untrusted `html` **string** into trusted string by removing * An `html` sanitizer which converts untrusted `html` **string** into trusted string by removing
@ -85,7 +35,7 @@ export function sanitizeHtml(unsafeHtml: any): string {
if (s) { if (s) {
return s.sanitize(SecurityContext.HTML, unsafeHtml) || ''; return s.sanitize(SecurityContext.HTML, unsafeHtml) || '';
} }
if (unsafeHtml instanceof String && (unsafeHtml as TrustedHtmlString)[BRAND] === 'Html') { if (allowSanitizationBypass(unsafeHtml, BypassType.Html)) {
return unsafeHtml.toString(); return unsafeHtml.toString();
} }
return _sanitizeHtml(document, stringify(unsafeHtml)); return _sanitizeHtml(document, stringify(unsafeHtml));
@ -109,7 +59,7 @@ export function sanitizeStyle(unsafeStyle: any): string {
if (s) { if (s) {
return s.sanitize(SecurityContext.STYLE, unsafeStyle) || ''; return s.sanitize(SecurityContext.STYLE, unsafeStyle) || '';
} }
if (unsafeStyle instanceof String && (unsafeStyle as TrustedStyleString)[BRAND] === 'Style') { if (allowSanitizationBypass(unsafeStyle, BypassType.Style)) {
return unsafeStyle.toString(); return unsafeStyle.toString();
} }
return _sanitizeStyle(stringify(unsafeStyle)); return _sanitizeStyle(stringify(unsafeStyle));
@ -134,7 +84,7 @@ export function sanitizeUrl(unsafeUrl: any): string {
if (s) { if (s) {
return s.sanitize(SecurityContext.URL, unsafeUrl) || ''; return s.sanitize(SecurityContext.URL, unsafeUrl) || '';
} }
if (unsafeUrl instanceof String && (unsafeUrl as TrustedUrlString)[BRAND] === 'Url') { if (allowSanitizationBypass(unsafeUrl, BypassType.Url)) {
return unsafeUrl.toString(); return unsafeUrl.toString();
} }
return _sanitizeUrl(stringify(unsafeUrl)); return _sanitizeUrl(stringify(unsafeUrl));
@ -154,8 +104,7 @@ export function sanitizeResourceUrl(unsafeResourceUrl: any): string {
if (s) { if (s) {
return s.sanitize(SecurityContext.RESOURCE_URL, unsafeResourceUrl) || ''; return s.sanitize(SecurityContext.RESOURCE_URL, unsafeResourceUrl) || '';
} }
if (unsafeResourceUrl instanceof String && if (allowSanitizationBypass(unsafeResourceUrl, BypassType.ResourceUrl)) {
(unsafeResourceUrl as TrustedResourceUrlString)[BRAND] === 'ResourceUrl') {
return unsafeResourceUrl.toString(); return unsafeResourceUrl.toString();
} }
throw new Error('unsafe value used in a resource URL context (see http://g.co/ng/security#xss)'); throw new Error('unsafe value used in a resource URL context (see http://g.co/ng/security#xss)');
@ -175,85 +124,22 @@ export function sanitizeScript(unsafeScript: any): string {
if (s) { if (s) {
return s.sanitize(SecurityContext.SCRIPT, unsafeScript) || ''; return s.sanitize(SecurityContext.SCRIPT, unsafeScript) || '';
} }
if (unsafeScript instanceof String && (unsafeScript as TrustedScriptString)[BRAND] === 'Script') { if (allowSanitizationBypass(unsafeScript, BypassType.Script)) {
return unsafeScript.toString(); return unsafeScript.toString();
} }
throw new Error('unsafe value used in a script context'); throw new Error('unsafe value used in a script context');
} }
/** /**
* Mark `html` string as trusted. * The default style sanitizer will handle sanitization for style properties by
* * sanitizing any CSS property that can include a `url` value (usually image-based properties)
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link htmlSanitizer} to be trusted implicitly.
*
* @param trustedHtml `html` string which needs to be implicitly trusted.
* @returns a `html` `String` which has been branded to be implicitly trusted.
*/ */
export function bypassSanitizationTrustHtml(trustedHtml: string): TrustedHtmlString { export const defaultStyleSanitizer = (function(prop: string, value?: string): string | boolean {
return bypassSanitizationTrustString(trustedHtml, 'Html'); if (value === undefined) {
} return prop === 'background-image' || prop === 'background' || prop === 'border-image' ||
/** prop === 'filter' || prop === 'filter' || prop === 'list-style' ||
* Mark `style` string as trusted. prop === 'list-style-image';
* }
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link styleSanitizer} to be trusted implicitly.
*
* @param trustedStyle `style` string which needs to be implicitly trusted.
* @returns a `style` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustStyle(trustedStyle: string): TrustedStyleString {
return bypassSanitizationTrustString(trustedStyle, 'Style');
}
/**
* Mark `script` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link scriptSanitizer} to be trusted implicitly.
*
* @param trustedScript `script` string which needs to be implicitly trusted.
* @returns a `script` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustScript(trustedScript: string): TrustedScriptString {
return bypassSanitizationTrustString(trustedScript, 'Script');
}
/**
* Mark `url` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link urlSanitizer} to be trusted implicitly.
*
* @param trustedUrl `url` string which needs to be implicitly trusted.
* @returns a `url` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustUrl(trustedUrl: string): TrustedUrlString {
return bypassSanitizationTrustString(trustedUrl, 'Url');
}
/**
* Mark `url` string as trusted.
*
* This function wraps the trusted string in `String` and brands it in a way which makes it
* recognizable to {@link resourceUrlSanitizer} to be trusted implicitly.
*
* @param trustedResourceUrl `url` string which needs to be implicitly trusted.
* @returns a `url` `String` which has been branded to be implicitly trusted.
*/
export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string):
TrustedResourceUrlString {
return bypassSanitizationTrustString(trustedResourceUrl, 'ResourceUrl');
}
return sanitizeStyle(value);
function bypassSanitizationTrustString(trustedString: string, mode: 'Html'): TrustedHtmlString; } as StyleSanitizeFn);
function bypassSanitizationTrustString(trustedString: string, mode: 'Style'): TrustedStyleString;
function bypassSanitizationTrustString(trustedString: string, mode: 'Script'): TrustedScriptString;
function bypassSanitizationTrustString(trustedString: string, mode: 'Url'): TrustedUrlString;
function bypassSanitizationTrustString(
trustedString: string, mode: 'ResourceUrl'): TrustedResourceUrlString;
function bypassSanitizationTrustString(
trustedString: string,
mode: 'Html' | 'Style' | 'Script' | 'Url' | 'ResourceUrl'): TrustedString {
const trusted = new String(trustedString) as TrustedString;
trusted[BRAND] = mode;
return trusted;
}

View File

@ -101,3 +101,19 @@ export function _sanitizeStyle(value: string): string {
return 'unsafe'; return 'unsafe';
} }
/**
* Used to intercept and sanitize style values before they are written to the renderer.
*
* This function is designed to be called in two modes. When a value is not provided
* then the function will return a boolean whether a property will be sanitized later.
* If a value is provided then the sanitized version of that will be returned.
*/
export interface StyleSanitizeFn {
/** This mode is designed to instruct whether the property will be used for sanitization
* at a later point */
(prop: string): boolean;
/** This mode is designed to sanitize the provided value */
(prop: string, value: string): string;
}

View File

@ -539,6 +539,9 @@
{ {
"name": "getRootView" "name": "getRootView"
}, },
{
"name": "getStyleSanitizer"
},
{ {
"name": "getStylingContext" "name": "getStylingContext"
}, },
@ -557,6 +560,9 @@
{ {
"name": "getValue" "name": "getValue"
}, },
{
"name": "hasValueChanged"
},
{ {
"name": "hostElement" "name": "hostElement"
}, },
@ -668,6 +674,9 @@
{ {
"name": "pointers" "name": "pointers"
}, },
{
"name": "prepareInitialFlag"
},
{ {
"name": "projectionNodeStack" "name": "projectionNodeStack"
}, },

View File

@ -284,7 +284,7 @@ describe('elements', () => {
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs(null, c1); $r3$.ɵs(c1);
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
@ -323,7 +323,7 @@ describe('elements', () => {
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs(c0); $r3$.ɵs(null, c0);
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
@ -356,8 +356,8 @@ describe('elements', () => {
it('should bind to many and keep order', () => { it('should bind to many and keep order', () => {
type $MyComponent$ = MyComponent; type $MyComponent$ = MyComponent;
const c0 = ['color', InitialStylingFlags.VALUES_MODE, 'color', 'red']; const c0 = ['foo'];
const c1 = ['foo']; const c1 = ['color', InitialStylingFlags.VALUES_MODE, 'color', 'red'];
@Component({ @Component({
selector: 'my-component', selector: 'my-component',
@ -416,7 +416,7 @@ describe('elements', () => {
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵsm(0, ctx.styleExp, ctx.classExp); $r3$.ɵsm(0, ctx.classExp, ctx.styleExp);
$r3$.ɵsa(0); $r3$.ɵsa(0);
} }
} }

View File

@ -51,7 +51,7 @@ describe('compiler sanitization', () => {
if (rf & 2) { if (rf & 2) {
$r3$.ɵp(0, 'innerHTML', $r3$.ɵb(ctx.innerHTML), $r3$.ɵsanitizeHtml); $r3$.ɵp(0, 'innerHTML', $r3$.ɵb(ctx.innerHTML), $r3$.ɵsanitizeHtml);
$r3$.ɵp(0, 'hidden', $r3$.ɵb(ctx.hidden)); $r3$.ɵp(0, 'hidden', $r3$.ɵb(ctx.hidden));
$r3$.ɵsp(0, 0, ctx.style, $r3$.ɵsanitizeStyle); $r3$.ɵsp(0, 0, ctx.style);
$r3$.ɵsa(0); $r3$.ɵsa(0);
$r3$.ɵp(1, 'src', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl); $r3$.ɵp(1, 'src', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
$r3$.ɵa(1, 'srcset', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl); $r3$.ɵa(1, 'srcset', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);

View File

@ -213,7 +213,7 @@ describe('exports', () => {
function Template(rf: RenderFlags, ctx: any) { function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'div'); elementStart(0, 'div');
elementStyling(null, [InitialStylingFlags.VALUES_MODE, 'red', true]); elementStyling([InitialStylingFlags.VALUES_MODE, 'red', true]);
elementEnd(); elementEnd();
elementStart(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']); elementStart(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']);
elementEnd(); elementEnd();

View File

@ -14,8 +14,10 @@ import {bind, container, element, elementAttribute, elementEnd, elementProperty,
import {InitialStylingFlags} from '../../src/render3/interfaces/definition'; import {InitialStylingFlags} from '../../src/render3/interfaces/definition';
import {AttributeMarker, LElementNode, LNode} from '../../src/render3/interfaces/node'; import {AttributeMarker, LElementNode, LNode} from '../../src/render3/interfaces/node';
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer'; 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 {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl} from '../../src/sanitization/bypass';
import {defaultStyleSanitizer, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
import {StyleSanitizeFn} from '../../src/sanitization/style_sanitizer';
import {NgForOf} from './common_with_def'; import {NgForOf} from './common_with_def';
import {ComponentFixture, TemplateFixture} from './render_util'; import {ComponentFixture, TemplateFixture} from './render_util';
@ -27,9 +29,10 @@ describe('instructions', () => {
elementEnd(); elementEnd();
} }
function createDiv(initialStyles?: (string | number)[]) { function createDiv(initialStyles?: (string | number)[], styleSanitizer?: StyleSanitizeFn) {
elementStart(0, 'div'); elementStart(0, 'div');
elementStyling(initialStyles && Array.isArray(initialStyles) ? initialStyles : null); elementStyling(
[], initialStyles && Array.isArray(initialStyles) ? initialStyles : null, styleSanitizer);
elementEnd(); elementEnd();
} }
@ -190,39 +193,87 @@ describe('instructions', () => {
}); });
describe('elementStyleProp', () => { describe('elementStyleProp', () => {
it('should use sanitizer function', () => { it('should automatically sanitize unless a bypass operation is applied', () => {
const t = new TemplateFixture(() => { return createDiv(['background-image']); }); const t = new TemplateFixture(
() => { return createDiv(['background-image'], defaultStyleSanitizer); });
t.update(() => { t.update(() => {
elementStyleProp(0, 0, 'url("http://server")', sanitizeStyle); elementStyleProp(0, 0, 'url("http://server")');
elementStylingApply(0); elementStylingApply(0);
}); });
// nothing is set because sanitizer suppresses it. // nothing is set because sanitizer suppresses it.
expect(t.html).toEqual('<div></div>'); expect(t.html).toEqual('<div></div>');
t.update(() => { t.update(() => {
elementStyleProp(0, 0, bypassSanitizationTrustStyle('url("http://server")'), sanitizeStyle); elementStyleProp(0, 0, bypassSanitizationTrustStyle('url("http://server2")'));
elementStylingApply(0); elementStylingApply(0);
}); });
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('url("http://server")'); .toEqual('url("http://server2")');
});
it('should not re-apply the style value even if it is a newly bypassed again', () => {
const sanitizerInterceptor = new MockSanitizerInterceptor();
const t = createTemplateFixtureWithSanitizer(
() => createDiv(['background-image'], sanitizerInterceptor.getStyleSanitizer()),
sanitizerInterceptor);
t.update(() => {
elementStyleProp(0, 0, bypassSanitizationTrustStyle('apple'));
elementStylingApply(0);
});
expect(sanitizerInterceptor.lastValue !).toEqual('apple');
sanitizerInterceptor.lastValue = null;
t.update(() => {
elementStyleProp(0, 0, bypassSanitizationTrustStyle('apple'));
elementStylingApply(0);
});
expect(sanitizerInterceptor.lastValue).toEqual(null);
}); });
}); });
describe('elementStyleMap', () => { describe('elementStyleMap', () => {
function createDivWithStyle() { function createDivWithStyle() {
elementStart(0, 'div'); elementStart(0, 'div');
elementStyling(['height', InitialStylingFlags.VALUES_MODE, 'height', '10px']); elementStyling([], ['height', InitialStylingFlags.VALUES_MODE, 'height', '10px']);
elementEnd(); elementEnd();
} }
it('should add style', () => { it('should add style', () => {
const fixture = new TemplateFixture(createDivWithStyle); const fixture = new TemplateFixture(createDivWithStyle);
fixture.update(() => { fixture.update(() => {
elementStylingMap(0, {'background-color': 'red'}); elementStylingMap(0, null, {'background-color': 'red'});
elementStylingApply(0); elementStylingApply(0);
}); });
expect(fixture.html).toEqual('<div style="height: 10px; background-color: red;"></div>'); expect(fixture.html).toEqual('<div style="height: 10px; background-color: red;"></div>');
}); });
it('should sanitize new styles that may contain `url` properties', () => {
const detectedValues: string[] = [];
const sanitizerInterceptor =
new MockSanitizerInterceptor(value => { detectedValues.push(value); });
const fixture = createTemplateFixtureWithSanitizer(
() => createDiv([], sanitizerInterceptor.getStyleSanitizer()), sanitizerInterceptor);
fixture.update(() => {
elementStylingMap(0, null, {
'background-image': 'background-image',
'background': 'background',
'border-image': 'border-image',
'list-style': 'list-style',
'list-style-image': 'list-style-image',
'filter': 'filter',
'width': 'width'
});
elementStylingApply(0);
});
const props = detectedValues.sort();
expect(props).toEqual([
'background', 'background-image', 'border-image', 'filter', 'list-style', 'list-style-image'
]);
});
}); });
describe('elementClass', () => { describe('elementClass', () => {
@ -235,7 +286,7 @@ describe('instructions', () => {
it('should add class', () => { it('should add class', () => {
const fixture = new TemplateFixture(createDivWithStyling); const fixture = new TemplateFixture(createDivWithStyling);
fixture.update(() => { fixture.update(() => {
elementStylingMap(0, null, 'multiple classes'); elementStylingMap(0, 'multiple classes');
elementStylingApply(0); elementStylingApply(0);
}); });
expect(fixture.html).toEqual('<div class="multiple classes"></div>'); expect(fixture.html).toEqual('<div class="multiple classes"></div>');
@ -504,7 +555,23 @@ class LocalMockSanitizer implements Sanitizer {
bypassSecurityTrustResourceUrl(value: string) { return new LocalSanitizedValue(value); } bypassSecurityTrustResourceUrl(value: string) { return new LocalSanitizedValue(value); }
} }
class MockSanitizerInterceptor {
public lastValue: string|null = null;
constructor(private _interceptorFn?: ((value: any) => any)|null) {}
getStyleSanitizer() { return defaultStyleSanitizer; }
sanitize(context: SecurityContext, value: LocalSanitizedValue|string|null|any): string|null {
if (this._interceptorFn) {
this._interceptorFn(value);
}
return this.lastValue = value;
}
}
function stripStyleWsCharacters(value: string): string { function stripStyleWsCharacters(value: string): string {
// color: blue; => color:blue // color: blue; => color:blue
return value.replace(/;/g, '').replace(/:\s+/g, ':'); return value.replace(/;/g, '').replace(/:\s+/g, ':');
} }
function createTemplateFixtureWithSanitizer(buildFn: () => any, sanitizer: Sanitizer) {
return new TemplateFixture(buildFn, () => {}, null, null, sanitizer);
}

View File

@ -748,7 +748,7 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) { function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'span'); elementStart(0, 'span');
elementStyling(['border-color']); elementStyling(null, ['border-color']);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
@ -767,7 +767,7 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) { function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'span'); elementStart(0, 'span');
elementStyling(['font-size']); elementStyling(null, ['font-size']);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
@ -788,7 +788,7 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) { function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'span'); elementStart(0, 'span');
elementStyling(null, ['active']); elementStyling(['active']);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
@ -814,7 +814,7 @@ describe('render3 integration test', () => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'span'); elementStart(0, 'span');
elementStyling( elementStyling(
null, ['existing', 'active', InitialStylingFlags.VALUES_MODE, 'existing', true]); ['existing', 'active', InitialStylingFlags.VALUES_MODE, 'existing', true]);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl} from '../../src/sanitization/bypass';
import {sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
describe('sanitization', () => { describe('sanitization', () => {
class Wrap { class Wrap {
@ -64,4 +65,4 @@ describe('sanitization', () => {
expect(() => sanitizeScript(bypassSanitizationTrustHtml('true'))).toThrowError(ERROR); expect(() => sanitizeScript(bypassSanitizationTrustHtml('true'))).toThrowError(ERROR);
expect(sanitizeScript(bypassSanitizationTrustScript('true'))).toEqual('true'); expect(sanitizeScript(bypassSanitizationTrustScript('true'))).toEqual('true');
}); });
}); });