refactor(ivy): enable sanitization support for the new styling algorithm (#30667)

This patch is one of the final patches to refactor the styling algorithm
to be more efficient, performant and less complex.

This patch enables sanitization support for map-based and prop-based
style bindings.

PR Close #30667
This commit is contained in:
Matias Niemelä 2019-05-24 13:49:57 -07:00
parent d72479b628
commit 82682bb93f
23 changed files with 725 additions and 102 deletions

View File

@ -7,7 +7,9 @@
*/
import {AttributeMarker, ViewEncapsulation} from '@angular/compiler/src/core';
import {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state';
import {setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
describe('compiler compliance: styling', () => {
@ -1463,4 +1465,122 @@ describe('compiler compliance: styling', () => {
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
describe('new styling refactor', () => {
beforeEach(() => { compilerSetStylingMode(CompilerStylingMode.UseNew); });
afterEach(() => { compilerSetStylingMode(CompilerStylingMode.UseOld); });
it('should generate a `styleSanitizer` instruction when one or more sanitizable style properties are statically detected',
() => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-app',
template: \`
<div [style.background-image]="bgExp"></div>
\`
})
export class MyAppComp {
bgExp = '';
}
`
}
};
const template = `
template: function MyAppComp_Template(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵselect(0);
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleProp(0, ctx.bgExp);
$r3$.ɵɵstylingApply();
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should generate a `styleSanitizer` instruction when a `styleMap` instruction is used',
() => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-app',
template: \`
<div [style]="mapExp"></div>
\`
})
export class MyAppComp {
mapExp = {};
}
`
}
};
const template = `
template: function MyAppComp_Template(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵselect(0);
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵstyleMap(ctx.mapExp);
$r3$.ɵɵstylingApply();
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('shouldn\'t generate a `styleSanitizer` instruction when class-based instructions are used',
() => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-app',
template: \`
<div [class]="mapExp" [class.name]="nameExp"></div>
\`
})
export class MyAppComp {
mapExp = {};
nameExp = true;
}
`
}
};
const template = `
template: function MyAppComp_Template(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵselect(0);
$r3$.ɵɵclassMap(ctx.mapExp);
$r3$.ɵɵclassProp(0, ctx.nameExp);
$r3$.ɵɵstylingApply();
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});
});

View File

@ -80,6 +80,8 @@ export class Identifiers {
static stylingApply: o.ExternalReference = {name: 'ɵɵstylingApply', moduleName: CORE};
static styleSanitizer: o.ExternalReference = {name: 'ɵɵstyleSanitizer', moduleName: CORE};
static elementHostAttrs: o.ExternalReference = {name: 'ɵɵelementHostAttrs', moduleName: CORE};
static containerCreate: o.ExternalReference = {name: 'ɵɵcontainer', moduleName: CORE};

View File

@ -89,6 +89,7 @@ export class StylingBuilder {
/** an array of each [class.name] input */
private _singleClassInputs: BoundStylingEntry[]|null = null;
private _lastStylingInput: BoundStylingEntry|null = null;
private _firstStylingInput: BoundStylingEntry|null = null;
// maps are used instead of hash maps because a Map will
// retain the ordering of the keys
@ -181,6 +182,7 @@ export class StylingBuilder {
registerIntoMap(this._stylesIndex, property);
}
this._lastStylingInput = entry;
this._firstStylingInput = this._firstStylingInput || entry;
this.hasBindings = true;
return entry;
}
@ -200,6 +202,7 @@ export class StylingBuilder {
registerIntoMap(this._classesIndex, property);
}
this._lastStylingInput = entry;
this._firstStylingInput = this._firstStylingInput || entry;
this.hasBindings = true;
return entry;
}
@ -453,6 +456,15 @@ export class StylingBuilder {
};
}
private _buildSanitizerFn() {
return {
sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null,
reference: R3.styleSanitizer,
allocateBindingSlots: 0,
buildParams: () => [o.importExpr(R3.defaultStyleSanitizer)]
};
}
/**
* Constructs all instructions which contain the expressions that will be placed
* into the update block of a template function or a directive hostBindings function.
@ -460,6 +472,9 @@ export class StylingBuilder {
buildUpdateLevelInstructions(valueConverter: ValueConverter) {
const instructions: Instruction[] = [];
if (this.hasBindings) {
if (compilerIsNewStylingInUse() && this._useDefaultSanitizer) {
instructions.push(this._buildSanitizerFn());
}
const styleMapInstruction = this.buildStyleMapInstruction(valueConverter);
if (styleMapInstruction) {
instructions.push(styleMapInstruction);

View File

@ -195,8 +195,8 @@ export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|nul
let styles: DebugNewStyling|null = null;
let classes: DebugNewStyling|null = null;
if (runtimeIsNewStylingInUse()) {
styles = tNode.newStyles ? new NodeStylingDebug(tNode.newStyles, lView) : null;
classes = tNode.newClasses ? new NodeStylingDebug(tNode.newClasses, lView) : null;
styles = tNode.newStyles ? new NodeStylingDebug(tNode.newStyles, lView, false) : null;
classes = tNode.newClasses ? new NodeStylingDebug(tNode.newClasses, lView, true) : null;
}
debugNodes.push({

View File

@ -102,6 +102,7 @@ export {
ɵɵselect,
ɵɵstyleMap,
ɵɵstyleProp,
ɵɵstyleSanitizer,
ɵɵstyling,
ɵɵstylingApply,
ɵɵtemplate,

View File

@ -45,5 +45,6 @@ export * from './property';
export * from './property_interpolation';
export * from './select';
export * from './styling';
export {styleSanitizer as ɵɵstyleSanitizer} from '../styling_next/instructions';
export * from './text';
export * from './text_interpolation';

View File

@ -121,6 +121,7 @@ export const angularCoreEnv: {[name: string]: Function} =
'ɵɵstyling': r3.ɵɵstyling,
'ɵɵstyleMap': r3.ɵɵstyleMap,
'ɵɵstyleProp': r3.ɵɵstyleProp,
'ɵɵstyleSanitizer': r3.ɵɵstyleSanitizer,
'ɵɵstylingApply': r3.ɵɵstylingApply,
'ɵɵclassProp': r3.ɵɵclassProp,
'ɵɵselect': r3.ɵɵselect,

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {AttributeMarker, TAttributes} from '../interfaces/node';
import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player';
@ -943,7 +943,9 @@ function updateSingleStylingValue(
if (currDirective !== directiveIndex) {
const prop = getProp(context, singleIndex);
const sanitizer = getStyleSanitizer(context, directiveIndex);
setSanitizeFlag(context, singleIndex, (sanitizer && sanitizer(prop)) ? true : false);
setSanitizeFlag(
context, singleIndex,
(sanitizer && sanitizer(prop, null, StyleSanitizeMode.ValidateProperty)) ? true : false);
}
// the value will always get updated (even if the dirty flag is skipped)
@ -1141,7 +1143,8 @@ export function setStyle(
native: any, prop: string, value: string | null, renderer: Renderer3,
sanitizer: StyleSanitizeFn | null, store?: BindingStore | null,
playerBuilder?: ClassAndStylePlayerBuilder<any>| null) {
value = sanitizer && value ? sanitizer(prop, value) : value;
value =
sanitizer && value ? sanitizer(prop, value, StyleSanitizeMode.ValidateAndSanitize) : value;
if (store || playerBuilder) {
if (store) {
store.setValue(prop, value);
@ -1461,7 +1464,9 @@ function valueExists(value: string | null | boolean, isClassBased?: boolean) {
function prepareInitialFlag(
context: StylingContext, prop: string, entryIsClassBased: boolean,
sanitizer?: StyleSanitizeFn | null) {
let flag = (sanitizer && sanitizer(prop)) ? StylingFlags.Sanitize : StylingFlags.None;
let flag = (sanitizer && sanitizer(prop, null, StyleSanitizeMode.ValidateProperty)) ?
StylingFlags.Sanitize :
StylingFlags.None;
let initialIndex: number;
if (entryIsClassBased) {

View File

@ -5,10 +5,11 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, LStylingMap, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces';
import {allowStylingFlush, getBindingValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasValueChanged, isContextLocked, isStylingValueDefined, lockContext} from './util';
import {ApplyStylingFn, LStylingData, LStylingMap, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
import {allowStylingFlush, getBindingValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasValueChanged, isContextLocked, isSanitizationRequired, isStylingValueDefined, lockContext, setGuardMask} from './util';
/**
@ -49,7 +50,7 @@ let currentStyleIndex = STYLING_INDEX_START_VALUE;
let currentClassIndex = STYLING_INDEX_START_VALUE;
let stylesBitMask = 0;
let classesBitMask = 0;
let deferredBindingQueue: (TStylingContext | number | string | null)[] = [];
let deferredBindingQueue: (TStylingContext | number | string | null | boolean)[] = [];
/**
* Visits a class-based binding and updates the new value (if changed).
@ -64,11 +65,11 @@ let deferredBindingQueue: (TStylingContext | number | string | null)[] = [];
export function updateClassBinding(
context: TStylingContext, data: LStylingData, prop: string | null, bindingIndex: number,
value: boolean | string | null | undefined | LStylingMap, deferRegistration: boolean,
forceUpdate?: boolean): void {
forceUpdate: boolean): void {
const isMapBased = !prop;
const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : currentClassIndex++;
const updated = updateBindingData(
context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate);
context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, false);
if (updated || forceUpdate) {
classesBitMask |= 1 << index;
}
@ -86,12 +87,16 @@ export function updateClassBinding(
*/
export function updateStyleBinding(
context: TStylingContext, data: LStylingData, prop: string | null, bindingIndex: number,
value: String | string | number | null | undefined | LStylingMap, deferRegistration: boolean,
forceUpdate?: boolean): void {
value: String | string | number | null | undefined | LStylingMap,
sanitizer: StyleSanitizeFn | null, deferRegistration: boolean, forceUpdate: boolean): void {
const isMapBased = !prop;
const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : currentStyleIndex++;
const sanitizationRequired = isMapBased ?
true :
(sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false);
const updated = updateBindingData(
context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate);
context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate,
sanitizationRequired);
if (updated || forceUpdate) {
stylesBitMask |= 1 << index;
}
@ -114,10 +119,10 @@ function updateBindingData(
context: TStylingContext, data: LStylingData, counterIndex: number, prop: string | null,
bindingIndex: number,
value: string | String | number | boolean | null | undefined | LStylingMap,
deferRegistration?: boolean, forceUpdate?: boolean): boolean {
deferRegistration: boolean, forceUpdate: boolean, sanitizationRequired: boolean): boolean {
if (!isContextLocked(context)) {
if (deferRegistration) {
deferBindingRegistration(context, counterIndex, prop, bindingIndex);
deferBindingRegistration(context, counterIndex, prop, bindingIndex, sanitizationRequired);
} else {
deferredBindingQueue.length && flushDeferredBindings();
@ -127,7 +132,7 @@ function updateBindingData(
// update pass is executed (remember that all styling instructions
// are run in the update phase, and, as a result, are no more
// styling instructions that are run in the creation phase).
registerBinding(context, counterIndex, prop, bindingIndex);
registerBinding(context, counterIndex, prop, bindingIndex, sanitizationRequired);
}
}
@ -150,8 +155,9 @@ function updateBindingData(
* after the inheritance chain exits.
*/
function deferBindingRegistration(
context: TStylingContext, counterIndex: number, prop: string | null, bindingIndex: number) {
deferredBindingQueue.splice(0, 0, context, counterIndex, prop, bindingIndex);
context: TStylingContext, counterIndex: number, prop: string | null, bindingIndex: number,
sanitizationRequired: boolean) {
deferredBindingQueue.unshift(context, counterIndex, prop, bindingIndex, sanitizationRequired);
}
/**
@ -165,7 +171,8 @@ function flushDeferredBindings() {
const count = deferredBindingQueue[i++] as number;
const prop = deferredBindingQueue[i++] as string;
const bindingIndex = deferredBindingQueue[i++] as number | null;
registerBinding(context, count, prop, bindingIndex);
const sanitizationRequired = deferredBindingQueue[i++] as boolean;
registerBinding(context, count, prop, bindingIndex, sanitizationRequired);
}
deferredBindingQueue.length = 0;
}
@ -208,7 +215,7 @@ function flushDeferredBindings() {
*/
export function registerBinding(
context: TStylingContext, countId: number, prop: string | null,
bindingValue: number | null | string | boolean) {
bindingValue: number | null | string | boolean, sanitizationRequired?: boolean) {
// prop-based bindings (e.g `<div [style.width]="w" [class.foo]="f">`)
if (prop) {
let found = false;
@ -220,7 +227,7 @@ export function registerBinding(
if (found) {
// all style/class bindings are sorted by property name
if (prop < p) {
allocateNewContextEntry(context, i, prop);
allocateNewContextEntry(context, i, prop, sanitizationRequired);
}
addBindingIntoContext(context, false, i, bindingValue, countId);
break;
@ -229,7 +236,7 @@ export function registerBinding(
}
if (!found) {
allocateNewContextEntry(context, context.length, prop);
allocateNewContextEntry(context, context.length, prop, sanitizationRequired);
addBindingIntoContext(context, false, i, bindingValue, countId);
}
} else {
@ -241,15 +248,18 @@ export function registerBinding(
}
}
function allocateNewContextEntry(context: TStylingContext, index: number, prop: string) {
function allocateNewContextEntry(
context: TStylingContext, index: number, prop: string, sanitizationRequired?: boolean) {
// 1,2: splice index locations
// 3: each entry gets a guard mask value that is used to check against updates
// 3: each entry gets a config value (guard mask + flags)
// 4. each entry gets a size value (which is always one because there is always a default binding
// value)
// 5. the property that is getting allocated into the context
// 6. the default binding value (usually `null`)
context.splice(
index, 0, DEFAULT_GUARD_MASK_VALUE, DEFAULT_SIZE_VALUE, prop, DEFAULT_BINDING_VALUE);
const config = sanitizationRequired ? TStylingContextPropConfigFlags.SanitizationRequired :
TStylingContextPropConfigFlags.Default;
context.splice(index, 0, config, DEFAULT_SIZE_VALUE, prop, DEFAULT_BINDING_VALUE);
setGuardMask(context, index, DEFAULT_GUARD_MASK_VALUE);
}
/**
@ -285,7 +295,12 @@ function addBindingIntoContext(
if (typeof bindingValue === 'number') {
context.splice(lastValueIndex, 0, bindingValue);
(context[index + TStylingContextIndex.ValuesCountOffset] as number)++;
(context[index + TStylingContextIndex.GuardOffset] as number) |= 1 << countId;
// now that a new binding index has been added to the property
// the guard mask bit value (at the `countId` position) needs
// to be included into the existing mask value.
const guardMask = getGuardMask(context, index) | (1 << countId);
setGuardMask(context, index, guardMask);
} else if (typeof bindingValue === 'string' && context[lastValueIndex] == null) {
context[lastValueIndex] = bindingValue;
}
@ -294,37 +309,49 @@ function addBindingIntoContext(
/**
* Applies all class entries in the provided context to the provided element and resets
* any counter and/or bitMask values associated with class bindings.
*
* @returns whether or not the classes were flushed to the element.
*/
export function applyClasses(
renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, context: TStylingContext,
element: RElement, directiveIndex: number) {
element: RElement, directiveIndex: number): boolean {
let classesFlushed = false;
if (allowStylingFlush(context, directiveIndex)) {
const isFirstPass = !isContextLocked(context);
isFirstPass && lockContext(context);
if (classesBitMask) {
applyStyling(context, renderer, element, data, classesBitMask, setClass);
// there is no way to sanitize a class value therefore `sanitizer=null`
applyStyling(context, renderer, element, data, classesBitMask, setClass, null);
classesBitMask = 0;
classesFlushed = true;
}
currentClassIndex = STYLING_INDEX_START_VALUE;
}
return classesFlushed;
}
/**
* Applies all style entries in the provided context to the provided element and resets
* any counter and/or bitMask values associated with style bindings.
*
* @returns whether or not the styles were flushed to the element.
*/
export function applyStyles(
renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, context: TStylingContext,
element: RElement, directiveIndex: number) {
element: RElement, directiveIndex: number, sanitizer: StyleSanitizeFn | null): boolean {
let stylesFlushed = false;
if (allowStylingFlush(context, directiveIndex)) {
const isFirstPass = !isContextLocked(context);
isFirstPass && lockContext(context);
if (stylesBitMask) {
applyStyling(context, renderer, element, data, stylesBitMask, setStyle);
applyStyling(context, renderer, element, data, stylesBitMask, setStyle, sanitizer);
stylesBitMask = 0;
stylesFlushed = true;
}
currentStyleIndex = STYLING_INDEX_START_VALUE;
return true;
}
return stylesFlushed;
}
/**
@ -355,7 +382,8 @@ export function applyStyles(
*/
export function applyStyling(
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn) {
bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn | null) {
deferredBindingQueue.length && flushDeferredBindings();
const bitMask = normalizeBitMaskValue(bitMaskValue);
@ -380,9 +408,12 @@ export function applyStyling(
// value gets set for the styling binding
for (let j = 0; j < valuesCountUpToDefault; j++) {
const bindingIndex = getBindingValue(context, i, j) as number;
const valueToApply = bindingData[bindingIndex];
if (isStylingValueDefined(valueToApply)) {
applyStylingFn(renderer, element, prop, valueToApply, bindingIndex);
const value = bindingData[bindingIndex];
if (isStylingValueDefined(value)) {
const finalValue = sanitizer && isSanitizationRequired(context, i) ?
sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) :
value;
applyStylingFn(renderer, element, prop, finalValue, bindingIndex);
valueApplied = true;
break;
}
@ -397,7 +428,8 @@ export function applyStyling(
const mode = mapsMode | (valueApplied ? StylingMapsSyncMode.SkipTargetProp :
StylingMapsSyncMode.ApplyTargetProp);
const valueAppliedWithinMap = stylingMapsSyncFn(
context, renderer, element, bindingData, applyStylingFn, mode, prop, defaultValue);
context, renderer, element, bindingData, applyStylingFn, sanitizer, mode, prop,
defaultValue);
valueApplied = valueApplied || valueAppliedWithinMap;
}
@ -417,7 +449,7 @@ export function applyStyling(
// values. For this reason, one more call to the sync function
// needs to be issued at the end.
if (stylingMapsSyncFn) {
stylingMapsSyncFn(context, renderer, element, bindingData, applyStylingFn, mapsMode);
stylingMapsSyncFn(context, renderer, element, bindingData, applyStylingFn, sanitizer, mapsMode);
}
}

View File

@ -5,20 +5,24 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Sanitizer} from '../../sanitization/security';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {LContainer} from '../interfaces/container';
import {AttributeMarker, TAttributes, TNode, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {StylingContext as OldStylingContext, StylingIndex as OldStylingIndex} from '../interfaces/styling';
import {BINDING_INDEX, HEADER_OFFSET, HOST, LView, RENDERER} from '../interfaces/view';
import {BINDING_INDEX, HEADER_OFFSET, HOST, LView, RENDERER, SANITIZER} from '../interfaces/view';
import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getLView, getSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils';
import {getTNode, isStylingContext as isOldStylingContext} from '../util/view_utils';
import {applyClasses, applyStyles, registerBinding, updateClassBinding, updateStyleBinding} from './bindings';
import {TStylingContext} from './interfaces';
import {activeStylingMapFeature, normalizeIntoStylingMap} from './map_based_bindings';
import {getCurrentStyleSanitizer, setCurrentStyleSanitizer} from './state';
import {attachStylingDebugObject} from './styling_debug';
import {allocStylingContext, hasValueChanged, updateContextDirectiveIndex} from './util';
import {allocTStylingContext, getCurrentOrLViewSanitizer, hasValueChanged, updateContextDirectiveIndex} from './util';
@ -51,12 +55,32 @@ export function stylingInit() {
updateLastDirectiveIndex(tNode, getActiveDirectiveStylingIndex());
}
/**
* Sets the current style sanitizer function which will then be used
* within all follow-up prop and map-based style binding instructions
* for the given element.
*
* Note that once styling has been applied to the element (i.e. once
* `select(n)` is executed or the hostBindings/template function exits)
* then the active `sanitizerFn` will be set to `null`. This means that
* once styling is applied to another element then a another call to
* `styleSanitizer` will need to be made.
*
* @param sanitizerFn The sanitization function that will be used to
* process style prop/value entries.
*
* @codeGenApi
*/
export function styleSanitizer(sanitizer: Sanitizer | StyleSanitizeFn | null): void {
setCurrentStyleSanitizer(sanitizer);
}
/**
* Mirror implementation of the `styleProp()` instruction (found in `instructions/styling.ts`).
*/
export function styleProp(
prop: string, value: string | number | String | null, suffix?: string | null): void {
_stylingProp(prop, value, false);
_stylingProp(prop, resolveStylePropValue(value, suffix), false);
}
/**
@ -79,10 +103,12 @@ function _stylingProp(
if (isClassBased) {
updateClassBinding(
getClassesContext(tNode), lView, prop, bindingIndex, value as string | boolean | null,
defer);
defer, false);
} else {
const sanitizer = getCurrentOrLViewSanitizer(lView);
updateStyleBinding(
getStylesContext(tNode), lView, prop, bindingIndex, value as string | null, defer);
getStylesContext(tNode), lView, prop, bindingIndex, value as string | null, sanitizer,
defer, false);
}
}
@ -122,8 +148,10 @@ function _stylingMap(value: {[key: string]: any} | string | null, isClassBased:
updateClassBinding(
getClassesContext(tNode), lView, null, bindingIndex, lStylingMap, defer, valueHasChanged);
} else {
const sanitizer = getCurrentOrLViewSanitizer(lView);
updateStyleBinding(
getStylesContext(tNode), lView, null, bindingIndex, lStylingMap, defer, valueHasChanged);
getStylesContext(tNode), lView, null, bindingIndex, lStylingMap, sanitizer, defer,
valueHasChanged);
}
}
}
@ -151,7 +179,11 @@ export function stylingApply() {
const native = getNativeFromLView(index, lView);
const directiveIndex = getActiveDirectiveStylingIndex();
applyClasses(renderer, lView, getClassesContext(tNode), native, directiveIndex);
applyStyles(renderer, lView, getStylesContext(tNode), native, directiveIndex);
const sanitizer = getCurrentOrLViewSanitizer(lView);
applyStyles(renderer, lView, getStylesContext(tNode), native, directiveIndex, sanitizer);
setCurrentStyleSanitizer(null);
}
/**
@ -202,10 +234,10 @@ export function registerInitialStylingIntoContext(
mode = attr;
} else if (mode == AttributeMarker.Classes) {
classesContext = classesContext || getClassesContext(tNode);
registerBinding(classesContext, -1, attr as string, true);
registerBinding(classesContext, -1, attr as string, true, false);
} else if (mode == AttributeMarker.Styles) {
stylesContext = stylesContext || getStylesContext(tNode);
registerBinding(stylesContext, -1, attr as string, attrs[++i] as string);
registerBinding(stylesContext, -1, attr as string, attrs[++i] as string, false);
}
}
}
@ -254,7 +286,7 @@ function getClassesContext(tNode: TNode): TStylingContext {
function getContext(tNode: TNode, isClassBased: boolean) {
let context = isClassBased ? tNode.newClasses : tNode.newStyles;
if (!context) {
context = allocStylingContext();
context = allocTStylingContext();
if (ngDevMode) {
attachStylingDebugObject(context);
}
@ -266,3 +298,22 @@ function getContext(tNode: TNode, isClassBased: boolean) {
}
return context;
}
function resolveStylePropValue(
value: string | number | String | null, suffix: string | null | undefined) {
let resolvedValue: string|null = null;
if (value !== null) {
if (suffix) {
// when a suffix is applied then it will bypass
// sanitization entirely (b/c a new string is created)
resolvedValue = renderStringify(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)
resolvedValue = value as any as string;
}
}
return resolvedValue;
}

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer';
import {LView} from '../interfaces/view';
@ -200,16 +201,16 @@ import {LView} from '../interfaces/view';
* In order to figure out which value to apply, the following
* binding prioritization is adhered to:
*
* 1. First template-level styling bindings are applied (if present).
* This includes things like `[style.width]` and `[class.active]`.
* 1. First template-level styling bindings are applied (if present).
* This includes things like `[style.width]` and `[class.active]`.
*
* 2. Second are styling-level host bindings present in directives.
* (if there are sub/super directives present then the sub directives
* are applied first).
* 2. Second are styling-level host bindings present in directives.
* (if there are sub/super directives present then the sub directives
* are applied first).
*
* 3. Third are styling-level host bindings present in components.
* (if there are sub/super components present then the sub directives
* are applied first).
* 3. Third are styling-level host bindings present in components.
* (if there are sub/super components present then the sub directives
* are applied first).
*
* This means that in the code above the styling binding present in the
* template is applied first and, only if its falsy, then the directive
@ -225,7 +226,39 @@ import {LView} from '../interfaces/view';
* For the algorithm to apply styling values efficiently, the
* styling map entries must be applied in sync (property by property)
* with prop-based bindings. (The map-based algorithm is described
* more inside of the `render3/stlying_next/map_based_bindings.ts` file.)
* more inside of the `render3/styling_next/map_based_bindings.ts` file.)
*
* ## Sanitization
* Sanitization is used to prevent invalid style values from being applied to
* the element.
*
* It is enabled in two cases:
*
* 1. The `styleSanitizer(sanitizerFn)` instruction was called (just before any other
* styling instructions are run).
*
* 2. The component/directive `LView` instance has a sanitizer object attached to it
* (this happens when `renderComponent` is executed with a `sanitizer` value or
* if the ngModule contains a sanitizer provider attached to it).
*
* If and when sanitization is active then all property/value entries will be evaluated
* through the active sanitizer before they are applied to the element (or the styling
* debug handler).
*
* If a `Sanitizer` object is used (via the `LView[SANITIZER]` value) then that object
* will be used for every property.
*
* If a `StyleSanitizerFn` function is used (via the `styleSanitizer`) then it will be
* called in two ways:
*
* 1. property validation mode: this will be called early to mark whether a property
* should be sanitized or not at during the flushing stage.
*
* 2. value sanitization mode: this will be called during the flushing stage and will
* run the sanitizer function against the value before applying it to the element.
*
* If sanitization returns an empty value then that empty value will be applied
* to the element.
*/
export interface TStylingContext extends Array<number|string|number|boolean|null|LStylingMap> {
/** Configuration data for the context */
@ -289,12 +322,22 @@ export const enum TStylingContextIndex {
// each tuple entry in the context
// (mask, count, prop, ...bindings||default-value)
GuardOffset = 0,
ConfigAndGuardOffset = 0,
ValuesCountOffset = 1,
PropOffset = 2,
BindingsStartOffset = 3,
}
/**
* A series of flags used for each property entry within the `TStylingContext`.
*/
export const enum TStylingContextPropConfigFlags {
Default = 0b0,
SanitizationRequired = 0b1,
TotalBits = 1,
Mask = 0b1,
}
/**
* A function used to apply or remove styling from an element for a given property.
*/
@ -370,8 +413,8 @@ export const enum LStylingMapIndex {
*/
export interface SyncStylingMapsFn {
(context: TStylingContext, renderer: Renderer3|ProceduralRenderer3|null, element: RElement,
data: LStylingData, applyStylingFn: ApplyStylingFn, mode: StylingMapsSyncMode,
targetProp?: string|null, defaultValue?: string|null): boolean;
data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn|null,
mode: StylingMapsSyncMode, targetProp?: string|null, defaultValue?: string|null): boolean;
}
/**

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer';
import {setStylingMapsSyncFn} from './bindings';
@ -103,8 +104,9 @@ import {getBindingValue, getValuesCount, isStylingValueDefined} from './util';
*/
export const syncStylingMap: SyncStylingMapsFn =
(context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
data: LStylingData, applyStylingFn: ApplyStylingFn, mode: StylingMapsSyncMode,
targetProp?: string | null, defaultValue?: string | null): boolean => {
data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn | null,
mode: StylingMapsSyncMode, targetProp?: string | null,
defaultValue?: string | null): boolean => {
let targetPropValueWasApplied = false;
// once the map-based styling code is activate it is never deactivated. For this reason a
@ -125,8 +127,8 @@ export const syncStylingMap: SyncStylingMapsFn =
if (runTheSyncAlgorithm) {
targetPropValueWasApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, mode, targetProp || null, 0,
defaultValue || null);
context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp || null,
0, defaultValue || null);
}
if (loopUntilEnd) {
@ -148,8 +150,9 @@ export const syncStylingMap: SyncStylingMapsFn =
*/
function innerSyncStylingMap(
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
data: LStylingData, applyStylingFn: ApplyStylingFn, mode: StylingMapsSyncMode,
targetProp: string | null, currentMapIndex: number, defaultValue: string | null): boolean {
data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn | null,
mode: StylingMapsSyncMode, targetProp: string | null, currentMapIndex: number,
defaultValue: string | null): boolean {
let targetPropValueWasApplied = false;
const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition);
@ -176,7 +179,7 @@ function innerSyncStylingMap(
iteratedTooFar ? mode : resolveInnerMapMode(mode, valueIsDefined, isTargetPropMatched);
const innerProp = iteratedTooFar ? targetProp : prop;
let valueApplied = innerSyncStylingMap(
context, renderer, element, data, applyStylingFn, innerMode, innerProp,
context, renderer, element, data, applyStylingFn, sanitizer, innerMode, innerProp,
currentMapIndex + 1, defaultValue);
if (iteratedTooFar) {
@ -187,7 +190,10 @@ function innerSyncStylingMap(
const useDefault = isTargetPropMatched && !valueIsDefined;
const valueToApply = useDefault ? defaultValue : value;
const bindingIndexToApply = useDefault ? bindingIndex : null;
applyStylingFn(renderer, element, prop, valueToApply, bindingIndexToApply);
const finalValue = sanitizer ?
sanitizer(prop, valueToApply, StyleSanitizeMode.ValidateAndSanitize) :
valueToApply;
applyStylingFn(renderer, element, prop, finalValue, bindingIndexToApply);
valueApplied = true;
}

View File

@ -5,6 +5,21 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Sanitizer} from '../../sanitization/security';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
/**
* --------
*
* This file contains temporary code to incorporate the new styling refactor
* code to work alongside the existing instruction set.
*
* This file will be removed once `select(n)` is fully functional (once
* it is able to evaluate host bindings in sync element-by-element
* with template code).
*
* --------
*/
/**
* A temporary enum of states that inform the core whether or not
@ -35,3 +50,12 @@ export function runtimeIsNewStylingInUse() {
export function runtimeAllowOldStyling() {
return _stylingMode < RuntimeStylingMode.UseNew;
}
let _currentSanitizer: Sanitizer|StyleSanitizeFn|null;
export function setCurrentStyleSanitizer(sanitizer: Sanitizer | StyleSanitizeFn | null) {
_currentSanitizer = sanitizer;
}
export function getCurrentStyleSanitizer() {
return _currentSanitizer;
}

View File

@ -5,13 +5,19 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Sanitizer} from '../../sanitization/security';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {RElement} from '../interfaces/renderer';
import {LView, SANITIZER} from '../interfaces/view';
import {attachDebugObject} from '../util/debug_utils';
import {applyStyling} from './bindings';
import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex} from './interfaces';
import {activeStylingMapFeature} from './map_based_bindings';
import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased} from './util';
import {getCurrentStyleSanitizer} from './state';
import {getCurrentOrLViewSanitizer, getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased, isSanitizationRequired} from './util';
/**
* --------
@ -59,6 +65,11 @@ export interface DebugStyling {
* runtime values.
*/
values: {[key: string]: string | number | null | boolean};
/**
* Overrides the sanitizer used to process styles.
*/
overrideSanitizer(sanitizer: StyleSanitizeFn|null): void;
}
/**
@ -77,6 +88,11 @@ export interface TStylingTupleSummary {
*/
guardMask: number;
/**
* Whether or not the entry requires sanitization
*/
sanitizationRequired: boolean;
/**
* The default value that will be applied if any bindings are falsy.
*/
@ -127,6 +143,7 @@ class TStylingContextDebug {
const prop = getProp(context, i);
const guardMask = getGuardMask(context, i);
const defaultValue = getDefaultValue(context, i);
const sanitizationRequired = isSanitizationRequired(context, i);
const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset;
const sources: (number | string | null)[] = [];
@ -134,7 +151,7 @@ class TStylingContextDebug {
sources.push(context[bindingsStartPosition + j] as number | string | null);
}
entries[prop] = {prop, guardMask, valuesCount, defaultValue, sources};
entries[prop] = {prop, guardMask, sanitizationRequired, valuesCount, defaultValue, sources};
}
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
@ -150,7 +167,16 @@ class TStylingContextDebug {
* application has `ngDevMode` activated.
*/
export class NodeStylingDebug implements DebugStyling {
constructor(public context: TStylingContext, private _data: LStylingData) {}
private _sanitizer: StyleSanitizeFn|null = null;
constructor(
public context: TStylingContext, private _data: LStylingData,
private _isClassBased?: boolean) {}
/**
* Overrides the sanitizer used to process styles.
*/
overrideSanitizer(sanitizer: StyleSanitizeFn|null) { this._sanitizer = sanitizer; }
/**
* Returns a detailed summary of each styling entry in the context and
@ -190,6 +216,8 @@ export class NodeStylingDebug implements DebugStyling {
fn(prop, value, bindingIndex || null);
};
applyStyling(this.context, null, mockElement, this._data, true, mapFn);
const sanitizer = this._isClassBased ? null : (this._sanitizer ||
getCurrentOrLViewSanitizer(this._data as LView));
applyStyling(this.context, null, mockElement, this._data, true, mapFn, sanitizer);
}
}

View File

@ -5,10 +5,14 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Sanitizer, SecurityContext} from '../../sanitization/security';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {StylingContext} from '../interfaces/styling';
import {LView, SANITIZER} from '../interfaces/view';
import {getProp as getOldProp, getSinglePropIndexValue as getOldSinglePropIndexValue} from '../styling/class_and_style_bindings';
import {LStylingMap, LStylingMapIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex} from './interfaces';
import {LStylingMap, LStylingMapIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces';
import {getCurrentStyleSanitizer, setCurrentStyleSanitizer} from './state';
const MAP_BASED_ENTRY_PROP_NAME = '--MAP--';
@ -18,8 +22,14 @@ const MAP_BASED_ENTRY_PROP_NAME = '--MAP--';
* This function will also pre-fill the context with data
* for map-based bindings.
*/
export function allocStylingContext(): TStylingContext {
return [TStylingConfigFlags.Initial, 0, 0, 0, MAP_BASED_ENTRY_PROP_NAME];
export function allocTStylingContext(): TStylingContext {
// because map-based bindings deal with a dynamic set of values, there
// is no way to know ahead of time whether or not sanitization is required.
// For this reason the configuration will always mark sanitization as active
// (this means that when map-based values are applied then sanitization will
// be checked against each property).
const mapBasedConfig = TStylingContextPropConfigFlags.SanitizationRequired;
return [TStylingConfigFlags.Initial, 0, mapBasedConfig, 0, MAP_BASED_ENTRY_PROP_NAME];
}
/**
@ -53,8 +63,24 @@ export function getProp(context: TStylingContext, index: number) {
return context[index + TStylingContextIndex.PropOffset] as string;
}
function getPropConfig(context: TStylingContext, index: number): number {
return (context[index + TStylingContextIndex.ConfigAndGuardOffset] as number) &
TStylingContextPropConfigFlags.Mask;
}
export function isSanitizationRequired(context: TStylingContext, index: number) {
return (getPropConfig(context, index) & TStylingContextPropConfigFlags.SanitizationRequired) > 0;
}
export function getGuardMask(context: TStylingContext, index: number) {
return context[index + TStylingContextIndex.GuardOffset] as number;
const configGuardValue = context[index + TStylingContextIndex.ConfigAndGuardOffset] as number;
return configGuardValue >> TStylingContextPropConfigFlags.TotalBits;
}
export function setGuardMask(context: TStylingContext, index: number, maskValue: number) {
const config = getPropConfig(context, index);
const guardMask = maskValue << TStylingContextPropConfigFlags.TotalBits;
context[index + TStylingContextIndex.ConfigAndGuardOffset] = config | guardMask;
}
export function getValuesCount(context: TStylingContext, index: number) {
@ -115,3 +141,36 @@ export function isStylingValueDefined(value: any) {
// set a value to an empty string to remove it.
return value != null && value !== '';
}
/**
* Returns the current style sanitizer function for the given view.
*
* The default style sanitizer (which lives inside of `LView`) will
* be returned depending on whether the `styleSanitizer` instruction
* was called or not prior to any styling instructions running.
*/
export function getCurrentOrLViewSanitizer(lView: LView): StyleSanitizeFn|null {
const sanitizer: StyleSanitizeFn|null = (getCurrentStyleSanitizer() || lView[SANITIZER]) as any;
if (sanitizer && typeof sanitizer !== 'function') {
setCurrentStyleSanitizer(sanitizer);
return sanitizeUsingSanitizerObject;
}
return sanitizer;
}
/**
* Style sanitization function that internally uses a `Sanitizer` instance to handle style
* sanitization.
*/
const sanitizeUsingSanitizerObject: StyleSanitizeFn =
(prop: string, value: string, mode: StyleSanitizeMode) => {
const sanitizer = getCurrentStyleSanitizer() as Sanitizer;
if (sanitizer) {
if (mode & StyleSanitizeMode.SanitizeOnly) {
return sanitizer.sanitize(SecurityContext.STYLE, value);
} else {
return true;
}
}
return value;
};

View File

@ -13,7 +13,7 @@ import {renderStringify} from '../render3/util/misc_utils';
import {BypassType, allowSanitizationBypass} from './bypass';
import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer';
import {Sanitizer, SecurityContext} from './security';
import {StyleSanitizeFn, _sanitizeStyle as _sanitizeStyle} from './style_sanitizer';
import {StyleSanitizeFn, StyleSanitizeMode, _sanitizeStyle as _sanitizeStyle} from './style_sanitizer';
import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer';
@ -183,15 +183,22 @@ export function ɵɵsanitizeUrlOrResourceUrl(unsafeUrl: any, tag: string, prop:
*
* @publicApi
*/
export const ɵɵdefaultStyleSanitizer = (function(prop: string, value?: string): string | boolean {
if (value === undefined) {
return prop === 'background-image' || prop === 'background' || prop === 'border-image' ||
prop === 'filter' || prop === 'list-style' || prop === 'list-style-image' ||
prop === 'clip-path';
}
export const ɵɵdefaultStyleSanitizer =
(function(prop: string, value: string|null, mode?: StyleSanitizeMode): string | boolean | null {
mode = mode || StyleSanitizeMode.ValidateAndSanitize;
let doSanitizeValue = true;
if (mode & StyleSanitizeMode.ValidateProperty) {
doSanitizeValue = prop === 'background-image' || prop === 'background' ||
prop === 'border-image' || prop === 'filter' || prop === 'list-style' ||
prop === 'list-style-image' || prop === 'clip-path';
}
return ɵɵsanitizeStyle(value);
} as StyleSanitizeFn);
if (mode & StyleSanitizeMode.SanitizeOnly) {
return doSanitizeValue ? ɵɵsanitizeStyle(value) : value;
} else {
return doSanitizeValue;
}
} as StyleSanitizeFn);
export function validateAgainstEventProperties(name: string) {
if (name.toLowerCase().startsWith('on')) {

View File

@ -103,6 +103,30 @@ export function _sanitizeStyle(value: string): string {
}
/**
* A series of flags to instruct a style sanitizer to either validate
* or sanitize a value.
*
* Because sanitization is dependent on the style property (i.e. style
* sanitization for `width` is much different than for `background-image`)
* the sanitization function (e.g. `StyleSanitizerFn`) needs to check a
* property value first before it actually sanitizes any values.
*
* This enum exist to allow a style sanitization function to either only
* do validation (check the property to see whether a value will be
* sanitized or not) or to sanitize the value (or both).
*
* @publicApi
*/
export const enum StyleSanitizeMode {
/** Just check to see if the property is required to be sanitized or not */
ValidateProperty = 0b01,
/** Skip checking the property; just sanitize the value */
SanitizeOnly = 0b10,
/** Check the property and (if true) then sanitize the value */
ValidateAndSanitize = 0b11,
}
/**
* Used to intercept and sanitize style values before they are written to the renderer.
*
@ -111,9 +135,5 @@ export function _sanitizeStyle(value: string): string {
* 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;
(prop: string, value: string|null, mode?: StyleSanitizeMode): any;
}

View File

@ -7,11 +7,15 @@
*/
import {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state';
import {Component, Directive, HostBinding, Input, ViewChild} from '@angular/core';
import {SecurityContext} from '@angular/core/src/core';
import {getLContext} from '@angular/core/src/render3/context_discovery';
import {DebugNode, LViewDebug, toDebug} from '@angular/core/src/render3/debug';
import {SANITIZER} from '@angular/core/src/render3/interfaces/view';
import {RuntimeStylingMode, runtimeSetStylingMode} from '@angular/core/src/render3/styling_next/state';
import {loadLContextFromNode} from '@angular/core/src/render3/util/discovery_utils';
import {ngDevModeResetPerfCounters as resetStylingCounters} from '@angular/core/src/util/ng_dev_mode';
import {TestBed} from '@angular/core/testing';
import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
@ -574,6 +578,161 @@ describe('new styling integration', () => {
assertStyle(element, 'color', 'blue');
assertStyle(element, 'opacity', '');
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should sanitize style values before writing them', () => {
@Component({
template: `
<div [style.width]="widthExp"
[style.background-image]="bgImageExp"
[style]="styleMapExp"></div>
`
})
class Cmp {
widthExp = '';
bgImageExp = '';
styleMapExp: any = {};
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
const node = getDebugNode(element) !;
const styles = node.styles !;
const lastSanitizedProps: any[] = [];
styles.overrideSanitizer((prop, value) => {
lastSanitizedProps.push(prop);
return value;
});
comp.bgImageExp = '123';
fixture.detectChanges();
expect(styles.values).toEqual({
'background-image': '123',
'width': null,
});
expect(lastSanitizedProps).toEqual(['background-image']);
lastSanitizedProps.length = 0;
comp.styleMapExp = {'clip-path': '456'};
fixture.detectChanges();
expect(styles.values).toEqual({
'background-image': '123',
'clip-path': '456',
'width': null,
});
expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']);
lastSanitizedProps.length = 0;
comp.widthExp = '789px';
fixture.detectChanges();
expect(styles.values).toEqual({
'background-image': '123',
'clip-path': '456',
'width': '789px',
});
expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']);
lastSanitizedProps.length = 0;
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should apply a unit to a style before writing it', () => {
@Component({
template: `
<div [style.width.px]="widthExp"
[style.height.em]="heightExp"></div>
`
})
class Cmp {
widthExp: string|number|null = '';
heightExp: string|number|null = '';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
const node = getDebugNode(element) !;
const styles = node.styles !;
comp.widthExp = '200';
comp.heightExp = 10;
fixture.detectChanges();
expect(styles.values).toEqual({
'width': '200px',
'height': '10em',
});
comp.widthExp = 0;
comp.heightExp = null;
fixture.detectChanges();
expect(styles.values).toEqual({
'width': '0px',
'height': null,
});
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should pick up and use the sanitizer present in the lView', () => {
@Component({
template: `
<div [style.width]="w"></div>
`
})
class Cmp {
w = '100px';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
const lView = getLContext(element) !.lView;
lView[SANITIZER] = new MockSanitizer(value => { return `${value}-safe`; });
comp.w = '200px';
fixture.detectChanges();
const node = getDebugNode(element) !;
const styles = node.styles !;
expect(styles.values['width']).toEqual('200px-safe');
});
it('should be able to bind a SafeValue to clip-path', () => {
@Component({template: '<div [style.clip-path]="path"></div>'})
class Cmp {
path !: SafeStyle;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const sanitizer: DomSanitizer = TestBed.get(DomSanitizer);
fixture.componentInstance.path = sanitizer.bypassSecurityTrustStyle('url("#test")');
fixture.detectChanges();
const html = fixture.nativeElement.innerHTML;
// Note that check the raw HTML, because (at the time of writing) the Node-based renderer
// that we use to run tests doesn't support `clip-path` in `CSSStyleDeclaration`.
expect(html).toMatch(/style=["|']clip-path:\s*url\(.*#test.*\)/);
});
});
function assertStyleCounters(countForSet: number, countForRemove: number) {
@ -597,3 +756,8 @@ function getDebugNode(element: Node): DebugNode|null {
}
return null;
}
class MockSanitizer {
constructor(private _interceptorFn: ((value: any) => any)) {}
sanitize(context: SecurityContext, value: any): string|null { return this._interceptorFn(value); }
}

View File

@ -192,7 +192,7 @@
"name": "allocStylingContext"
},
{
"name": "allocStylingContext"
"name": "allocTStylingContext"
},
{
"name": "allocateNewContextEntry"
@ -362,6 +362,9 @@
{
"name": "getGlobal"
},
{
"name": "getGuardMask"
},
{
"name": "getHighestElementOrICUContainer"
},
@ -434,6 +437,9 @@
{
"name": "getProp"
},
{
"name": "getPropConfig"
},
{
"name": "getPropValuesStartPosition"
},
@ -686,6 +692,9 @@
{
"name": "setCurrentQueryIndex"
},
{
"name": "setGuardMask"
},
{
"name": "setHostBindings"
},

View File

@ -230,6 +230,9 @@
{
"name": "SWITCH_VIEW_CONTAINER_REF_FACTORY"
},
{
"name": "SecurityContext"
},
{
"name": "SkipSelf"
},
@ -444,7 +447,7 @@
"name": "allocStylingContext"
},
{
"name": "allocStylingContext"
"name": "allocTStylingContext"
},
{
"name": "allocateNewContextEntry"
@ -785,6 +788,12 @@
{
"name": "getContextLView"
},
{
"name": "getCurrentOrLViewSanitizer"
},
{
"name": "getCurrentStyleSanitizer"
},
{
"name": "getDebugContext"
},
@ -935,6 +944,9 @@
{
"name": "getProp"
},
{
"name": "getPropConfig"
},
{
"name": "getPropValuesStartPosition"
},
@ -1151,6 +1163,9 @@
{
"name": "isRootView"
},
{
"name": "isSanitizationRequired"
},
{
"name": "isStylingContext"
},
@ -1343,6 +1358,9 @@
{
"name": "runtimeIsNewStylingInUse"
},
{
"name": "sanitizeUsingSanitizerObject"
},
{
"name": "saveNameToExportMap"
},
@ -1391,12 +1409,18 @@
{
"name": "setCurrentQueryIndex"
},
{
"name": "setCurrentStyleSanitizer"
},
{
"name": "setDirty"
},
{
"name": "setFlag"
},
{
"name": "setGuardMask"
},
{
"name": "setHostBindings"
},

View File

@ -23,7 +23,7 @@ import {registerHostDirective} from '../../../src/render3/styling/host_instructi
import {BoundPlayerFactory, bindPlayerFactory} from '../../../src/render3/styling/player_factory';
import {allocStylingContext, createEmptyStylingContext} from '../../../src/render3/styling/util';
import {ɵɵdefaultStyleSanitizer} from '../../../src/sanitization/sanitization';
import {StyleSanitizeFn} from '../../../src/sanitization/style_sanitizer';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../../src/sanitization/style_sanitizer';
import {ComponentFixture, renderToHtml} from '../render_util';
import {MockPlayer} from './mock_player';
@ -2991,11 +2991,16 @@ describe('style and class based bindings', () => {
});
it('should sanitize styles before they are passed into the player', () => {
const sanitizer = (function(prop: string, value?: string): string | boolean {
if (value === undefined) {
return prop === 'width' || prop === 'height';
const sanitizer = (function(prop: string, value: string, mode: StyleSanitizeMode): any {
let allow = true;
if (mode & StyleSanitizeMode.ValidateProperty) {
allow = prop === 'width' || prop === 'height';
}
if (mode & StyleSanitizeMode.SanitizeOnly) {
return allow ? `${value}-safe!` : value;
} else {
return `${value}-safe!`;
return allow;
}
}) as StyleSanitizeFn;

View File

@ -7,8 +7,7 @@
*/
import {DEFAULT_GUARD_MASK_VALUE, registerBinding} from '@angular/core/src/render3/styling_next/bindings';
import {attachStylingDebugObject} from '@angular/core/src/render3/styling_next/styling_debug';
import {allocStylingContext} from '../../../src/render3/styling_next/util';
import {allocTStylingContext} from '../../../src/render3/styling_next/util';
describe('styling context', () => {
it('should register a series of entries into the context', () => {
@ -20,6 +19,7 @@ describe('styling context', () => {
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
sanitizationRequired: false,
guardMask: buildGuardMask(),
defaultValue: '100px',
sources: ['100px'],
@ -28,6 +28,7 @@ describe('styling context', () => {
registerBinding(context, 2, 'width', 20);
expect(debug.entries['width']).toEqual({
prop: 'width',
sanitizationRequired: false,
valuesCount: 2,
guardMask: buildGuardMask(2),
defaultValue: '100px',
@ -39,6 +40,7 @@ describe('styling context', () => {
expect(debug.entries['height']).toEqual({
prop: 'height',
valuesCount: 3,
sanitizationRequired: false,
guardMask: buildGuardMask(3, 4),
defaultValue: null,
sources: [10, 15, null],
@ -50,9 +52,11 @@ describe('styling context', () => {
const context = debug.context;
registerBinding(context, 1, 'width', null);
const x = debug.entries['width'];
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
sanitizationRequired: false,
guardMask: buildGuardMask(),
defaultValue: null,
sources: [null]
@ -62,6 +66,7 @@ describe('styling context', () => {
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
sanitizationRequired: false,
guardMask: buildGuardMask(),
defaultValue: '100px',
sources: ['100px']
@ -71,6 +76,7 @@ describe('styling context', () => {
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
sanitizationRequired: false,
guardMask: buildGuardMask(),
defaultValue: '100px',
sources: ['100px']
@ -79,7 +85,7 @@ describe('styling context', () => {
});
function makeContextWithDebug() {
const ctx = allocStylingContext();
const ctx = allocTStylingContext();
return attachStylingDebugObject(ctx);
}

View File

@ -7,7 +7,7 @@
*/
import {registerBinding} from '@angular/core/src/render3/styling_next/bindings';
import {NodeStylingDebug, attachStylingDebugObject} from '@angular/core/src/render3/styling_next/styling_debug';
import {allocStylingContext} from '@angular/core/src/render3/styling_next/util';
import {allocTStylingContext} from '@angular/core/src/render3/styling_next/util';
describe('styling debugging tools', () => {
describe('NodeStylingDebug', () => {
@ -64,6 +64,6 @@ describe('styling debugging tools', () => {
});
function makeContextWithDebug() {
const ctx = allocStylingContext();
const ctx = allocTStylingContext();
return attachStylingDebugObject(ctx);
}