feat($compile): add support for arbitrary DOM property and event bindings
Properties:
Previously only arbitrary DOM attribute bindings were supported via interpolation such as
`my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of
distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM
properties. For example `ng-prop-foo="x"` will assign the value of the expression `x` to the
`foo` property, and re-assign whenever the expression `x` changes.
Events:
Previously only a distinct set of DOM events could be bound using directives such as `ng-click`,
`ng-blur` etc. `ng-on-*` adds support for binding expressions to any DOM event. For example
`ng-on-bar="barOccured($event)"` will add a listener to the "bar" event and invoke the
`barOccured($event)` expression.
Since HTML attributes are case-insensitive, property and event names are specified in snake_case
for `ng-prop-*` and `ng-on-*`. For example, to bind property `fooBar` use `ng-prop-foo_bar`, to
listen to event `fooBar` use `ng-on-foo_bar`.
Fixes #16428
Fixes #16235
Closes #16614
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
@ngdoc error
|
||||
@name $compile:ctxoverride
|
||||
@fullName DOM Property Security Context Override
|
||||
@description
|
||||
|
||||
This error occurs when the security context for a property is defined via {@link ng.$compileProvider#addPropertySecurityContext addPropertySecurityContext()} multiple times under different security contexts.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
$compileProvider.addPropertySecurityContext("my-element", "src", $sce.MEDIA_URL);
|
||||
$compileProvider.addPropertySecurityContext("my-element", "src", $sce.RESOURCE_URL); //throws
|
||||
```
|
||||
@@ -1,12 +1,12 @@
|
||||
@ngdoc error
|
||||
@name $compile:nodomevents
|
||||
@fullName Interpolated Event Attributes
|
||||
@fullName Event Attribute/Property Binding
|
||||
@description
|
||||
|
||||
This error occurs when one tries to create a binding for event handler attributes like `onclick`, `onload`, `onsubmit`, etc.
|
||||
This error occurs when one tries to create a binding for event handler attributes or properties like `onclick`, `onload`, `onsubmit`, etc.
|
||||
|
||||
There is no practical value in binding to these attributes and doing so only exposes your application to security vulnerabilities like XSS.
|
||||
For these reasons binding to event handler attributes (all attributes that start with `on` and `formaction` attribute) is not supported.
|
||||
There is no practical value in binding to these attributes/properties and doing so only exposes your application to security vulnerabilities like XSS.
|
||||
For these reasons binding to event handler attributes and properties (`formaction` and all starting with `on`) is not supported.
|
||||
|
||||
|
||||
An example code that would allow XSS vulnerability by evaluating user input in the window context could look like this:
|
||||
@@ -17,4 +17,4 @@ An example code that would allow XSS vulnerability by evaluating user input in t
|
||||
|
||||
Since the `onclick` evaluates the value as JavaScript code in the window context, setting the `username` model to a value like `javascript:alert('PWND')` would result in script injection when the `div` is clicked.
|
||||
|
||||
|
||||
Please use the `ng-*` or `ng-on-*` versions instead (such as `ng-click` or `ng-on-click` rather than `onclick`).
|
||||
|
||||
@@ -171,9 +171,15 @@
|
||||
/* ng/q.js */
|
||||
"markQExceptionHandled": false,
|
||||
|
||||
/* sce.js */
|
||||
"SCE_CONTEXTS": false,
|
||||
|
||||
/* ng/directive/directives.js */
|
||||
"ngDirective": false,
|
||||
|
||||
/* ng/directive/ngEventDirs.js */
|
||||
"createEventDirective": false,
|
||||
|
||||
/* ng/directive/input.js */
|
||||
"VALID_CLASS": false,
|
||||
"INVALID_CLASS": false,
|
||||
|
||||
+193
-34
@@ -1586,6 +1586,91 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
return cssClassDirectivesEnabledConfig;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The security context of DOM Properties.
|
||||
* @private
|
||||
*/
|
||||
var PROP_CONTEXTS = createMap();
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $compileProvider#addPropertySecurityContext
|
||||
* @description
|
||||
*
|
||||
* Defines the security context for DOM properties bound by ng-prop-*.
|
||||
*
|
||||
* @param {string} elementName The element name or '*' to match any element.
|
||||
* @param {string} propertyName The DOM property name.
|
||||
* @param {string} ctx The {@link $sce} security context in which this value is safe for use, e.g. `$sce.URL`
|
||||
* @returns {object} `this` for chaining
|
||||
*/
|
||||
this.addPropertySecurityContext = function(elementName, propertyName, ctx) {
|
||||
var key = (elementName.toLowerCase() + '|' + propertyName.toLowerCase());
|
||||
|
||||
if (key in PROP_CONTEXTS && PROP_CONTEXTS[key] !== ctx) {
|
||||
throw $compileMinErr('ctxoverride', 'Property context \'{0}.{1}\' already set to \'{2}\', cannot override to \'{3}\'.', elementName, propertyName, PROP_CONTEXTS[key], ctx);
|
||||
}
|
||||
|
||||
PROP_CONTEXTS[key] = ctx;
|
||||
return this;
|
||||
};
|
||||
|
||||
/* Default property contexts.
|
||||
*
|
||||
* Copy of https://github.com/angular/angular/blob/6.0.6/packages/compiler/src/schema/dom_security_schema.ts#L31-L58
|
||||
* Changing:
|
||||
* - SecurityContext.* => SCE_CONTEXTS/$sce.*
|
||||
* - STYLE => CSS
|
||||
* - various URL => MEDIA_URL
|
||||
* - *|formAction, form|action URL => RESOURCE_URL (like the attribute)
|
||||
*/
|
||||
(function registerNativePropertyContexts() {
|
||||
function registerContext(ctx, values) {
|
||||
forEach(values, function(v) { PROP_CONTEXTS[v.toLowerCase()] = ctx; });
|
||||
}
|
||||
|
||||
registerContext(SCE_CONTEXTS.HTML, [
|
||||
'iframe|srcdoc',
|
||||
'*|innerHTML',
|
||||
'*|outerHTML'
|
||||
]);
|
||||
registerContext(SCE_CONTEXTS.CSS, ['*|style']);
|
||||
registerContext(SCE_CONTEXTS.URL, [
|
||||
'area|href', 'area|ping',
|
||||
'a|href', 'a|ping',
|
||||
'blockquote|cite',
|
||||
'body|background',
|
||||
'del|cite',
|
||||
'input|src',
|
||||
'ins|cite',
|
||||
'q|cite'
|
||||
]);
|
||||
registerContext(SCE_CONTEXTS.MEDIA_URL, [
|
||||
'audio|src',
|
||||
'img|src', 'img|srcset',
|
||||
'source|src', 'source|srcset',
|
||||
'track|src',
|
||||
'video|src', 'video|poster'
|
||||
]);
|
||||
registerContext(SCE_CONTEXTS.RESOURCE_URL, [
|
||||
'*|formAction',
|
||||
'applet|code', 'applet|codebase',
|
||||
'base|href',
|
||||
'embed|src',
|
||||
'frame|src',
|
||||
'form|action',
|
||||
'head|profile',
|
||||
'html|manifest',
|
||||
'iframe|src',
|
||||
'link|href',
|
||||
'media|src',
|
||||
'object|codebase', 'object|data',
|
||||
'script|src'
|
||||
]);
|
||||
})();
|
||||
|
||||
|
||||
this.$get = [
|
||||
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
|
||||
'$controller', '$rootScope', '$sce', '$animate',
|
||||
@@ -1631,12 +1716,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
}
|
||||
|
||||
|
||||
function sanitizeSrcset(value) {
|
||||
function sanitizeSrcset(value, invokeType) {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
if (!isString(value)) {
|
||||
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `$set(\'srcset\', value)`: "{0}"', value.toString());
|
||||
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `{0}`: "{1}"', invokeType, value.toString());
|
||||
}
|
||||
|
||||
// Such values are a bit too complex to handle automatically inside $sce.
|
||||
@@ -1916,7 +2001,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
: function denormalizeTemplate(template) {
|
||||
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
|
||||
},
|
||||
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
|
||||
NG_PREFIX_BINDING = /^ng(Attr|Prop|On)([A-Z].*)$/;
|
||||
var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
|
||||
|
||||
compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
|
||||
@@ -2252,43 +2337,66 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective);
|
||||
|
||||
// iterate over the attributes
|
||||
for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes,
|
||||
for (var attr, name, nName, value, ngPrefixMatch, nAttrs = node.attributes,
|
||||
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
|
||||
var attrStartName = false;
|
||||
var attrEndName = false;
|
||||
|
||||
var isNgAttr = false, isNgProp = false, isNgEvent = false;
|
||||
var multiElementMatch;
|
||||
|
||||
attr = nAttrs[j];
|
||||
name = attr.name;
|
||||
value = attr.value;
|
||||
|
||||
// support ngAttr attribute binding
|
||||
ngAttrName = directiveNormalize(name);
|
||||
isNgAttr = NG_ATTR_BINDING.test(ngAttrName);
|
||||
if (isNgAttr) {
|
||||
nName = directiveNormalize(name.toLowerCase());
|
||||
|
||||
// Support ng-attr-*, ng-prop-* and ng-on-*
|
||||
if ((ngPrefixMatch = nName.match(NG_PREFIX_BINDING))) {
|
||||
isNgAttr = ngPrefixMatch[1] === 'Attr';
|
||||
isNgProp = ngPrefixMatch[1] === 'Prop';
|
||||
isNgEvent = ngPrefixMatch[1] === 'On';
|
||||
|
||||
// Normalize the non-prefixed name
|
||||
name = name.replace(PREFIX_REGEXP, '')
|
||||
.substr(8).replace(/_(.)/g, function(match, letter) {
|
||||
.toLowerCase()
|
||||
.substr(4 + ngPrefixMatch[1].length).replace(/_(.)/g, function(match, letter) {
|
||||
return letter.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
|
||||
if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
|
||||
// Support *-start / *-end multi element directives
|
||||
} else if ((multiElementMatch = nName.match(MULTI_ELEMENT_DIR_RE)) && directiveIsMultiElement(multiElementMatch[1])) {
|
||||
attrStartName = name;
|
||||
attrEndName = name.substr(0, name.length - 5) + 'end';
|
||||
name = name.substr(0, name.length - 6);
|
||||
}
|
||||
|
||||
nName = directiveNormalize(name.toLowerCase());
|
||||
attrsMap[nName] = name;
|
||||
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
|
||||
if (isNgProp || isNgEvent) {
|
||||
attrs[nName] = value;
|
||||
attrsMap[nName] = attr.name;
|
||||
|
||||
if (isNgProp) {
|
||||
addPropertyDirective(node, directives, nName, name);
|
||||
} else {
|
||||
addEventDirective(directives, nName, name);
|
||||
}
|
||||
} else {
|
||||
// Update nName for cases where a prefix was removed
|
||||
// NOTE: the .toLowerCase() is unnecessary and causes https://github.com/angular/angular.js/issues/16624 for ng-attr-*
|
||||
nName = directiveNormalize(name.toLowerCase());
|
||||
attrsMap[nName] = name;
|
||||
|
||||
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
|
||||
attrs[nName] = value;
|
||||
if (getBooleanAttrName(node, nName)) {
|
||||
attrs[nName] = true; // presence means true
|
||||
}
|
||||
}
|
||||
|
||||
addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
|
||||
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
|
||||
attrEndName);
|
||||
}
|
||||
addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
|
||||
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
|
||||
attrEndName);
|
||||
}
|
||||
|
||||
if (nodeName === 'input' && node.getAttribute('type') === 'hidden') {
|
||||
@@ -3332,42 +3440,95 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
}
|
||||
|
||||
|
||||
function getTrustedContext(node, attrNormalizedName) {
|
||||
function getTrustedAttrContext(nodeName, attrNormalizedName) {
|
||||
if (attrNormalizedName === 'srcdoc') {
|
||||
return $sce.HTML;
|
||||
}
|
||||
var tag = nodeName_(node);
|
||||
// All tags with src attributes require a RESOURCE_URL value, except for
|
||||
// img and various html5 media tags, which require the MEDIA_URL context.
|
||||
// All nodes with src attributes require a RESOURCE_URL value, except for
|
||||
// img and various html5 media nodes, which require the MEDIA_URL context.
|
||||
if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') {
|
||||
if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) {
|
||||
if (['img', 'video', 'audio', 'source', 'track'].indexOf(nodeName) === -1) {
|
||||
return $sce.RESOURCE_URL;
|
||||
}
|
||||
return $sce.MEDIA_URL;
|
||||
} else if (attrNormalizedName === 'xlinkHref') {
|
||||
// Some xlink:href are okay, most aren't
|
||||
if (tag === 'image') return $sce.MEDIA_URL;
|
||||
if (tag === 'a') return $sce.URL;
|
||||
if (nodeName === 'image') return $sce.MEDIA_URL;
|
||||
if (nodeName === 'a') return $sce.URL;
|
||||
return $sce.RESOURCE_URL;
|
||||
} else if (
|
||||
// Formaction
|
||||
(tag === 'form' && attrNormalizedName === 'action') ||
|
||||
(nodeName === 'form' && attrNormalizedName === 'action') ||
|
||||
// If relative URLs can go where they are not expected to, then
|
||||
// all sorts of trust issues can arise.
|
||||
(tag === 'base' && attrNormalizedName === 'href') ||
|
||||
(nodeName === 'base' && attrNormalizedName === 'href') ||
|
||||
// links can be stylesheets or imports, which can run script in the current origin
|
||||
(tag === 'link' && attrNormalizedName === 'href')
|
||||
(nodeName === 'link' && attrNormalizedName === 'href')
|
||||
) {
|
||||
return $sce.RESOURCE_URL;
|
||||
} else if (tag === 'a' && (attrNormalizedName === 'href' ||
|
||||
} else if (nodeName === 'a' && (attrNormalizedName === 'href' ||
|
||||
attrNormalizedName === 'ngHref')) {
|
||||
return $sce.URL;
|
||||
}
|
||||
}
|
||||
|
||||
function getTrustedPropContext(nodeName, propNormalizedName) {
|
||||
var prop = propNormalizedName.toLowerCase();
|
||||
return PROP_CONTEXTS[nodeName + '|' + prop] || PROP_CONTEXTS['*|' + prop];
|
||||
}
|
||||
|
||||
function sanitizeSrcsetPropertyValue(value) {
|
||||
return sanitizeSrcset($sce.valueOf(value), 'ng-prop-srcset');
|
||||
}
|
||||
function addPropertyDirective(node, directives, attrName, propName) {
|
||||
if (EVENT_HANDLER_ATTR_REGEXP.test(propName)) {
|
||||
throw $compileMinErr('nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
|
||||
}
|
||||
|
||||
var nodeName = nodeName_(node);
|
||||
var trustedContext = getTrustedPropContext(nodeName, propName);
|
||||
|
||||
var sanitizer = identity;
|
||||
// Sanitize img[srcset] + source[srcset] values.
|
||||
if (propName === 'srcset' && (nodeName === 'img' || nodeName === 'source')) {
|
||||
sanitizer = sanitizeSrcsetPropertyValue;
|
||||
} else if (trustedContext) {
|
||||
sanitizer = $sce.getTrusted.bind($sce, trustedContext);
|
||||
}
|
||||
|
||||
directives.push({
|
||||
priority: 100,
|
||||
compile: function ngPropCompileFn(_, attr) {
|
||||
var ngPropGetter = $parse(attr[attrName]);
|
||||
var ngPropWatch = $parse(attr[attrName], function sceValueOf(val) {
|
||||
// Unwrap the value to compare the actual inner safe value, not the wrapper object.
|
||||
return $sce.valueOf(val);
|
||||
});
|
||||
|
||||
return {
|
||||
pre: function ngPropPreLinkFn(scope, $element) {
|
||||
function applyPropValue() {
|
||||
var propValue = ngPropGetter(scope);
|
||||
$element.prop(propName, sanitizer(propValue));
|
||||
}
|
||||
|
||||
applyPropValue();
|
||||
scope.$watch(ngPropWatch, applyPropValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addEventDirective(directives, attrName, eventName) {
|
||||
directives.push(
|
||||
createEventDirective($parse, $rootScope, $exceptionHandler, attrName, eventName, /*forceAsync=*/false)
|
||||
);
|
||||
}
|
||||
|
||||
function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) {
|
||||
var trustedContext = getTrustedContext(node, name);
|
||||
var nodeName = nodeName_(node);
|
||||
var trustedContext = getTrustedAttrContext(nodeName, name);
|
||||
var mustHaveExpression = !isNgAttr;
|
||||
var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr;
|
||||
|
||||
@@ -3376,16 +3537,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
// no interpolation found -> ignore
|
||||
if (!interpolateFn) return;
|
||||
|
||||
if (name === 'multiple' && nodeName_(node) === 'select') {
|
||||
if (name === 'multiple' && nodeName === 'select') {
|
||||
throw $compileMinErr('selmulti',
|
||||
'Binding to the \'multiple\' attribute is not supported. Element: {0}',
|
||||
startingTag(node));
|
||||
}
|
||||
|
||||
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
|
||||
throw $compileMinErr('nodomevents',
|
||||
'Interpolations for HTML DOM event attributes are disallowed. Please use the ' +
|
||||
'ng- versions (such as ng-click instead of onclick) instead.');
|
||||
throw $compileMinErr('nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
}
|
||||
|
||||
directives.push({
|
||||
|
||||
@@ -51,39 +51,43 @@ forEach(
|
||||
function(eventName) {
|
||||
var directiveName = directiveNormalize('ng-' + eventName);
|
||||
ngEventDirectives[directiveName] = ['$parse', '$rootScope', '$exceptionHandler', function($parse, $rootScope, $exceptionHandler) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
compile: function($element, attr) {
|
||||
// NOTE:
|
||||
// We expose the powerful `$event` object on the scope that provides access to the Window,
|
||||
// etc. This is OK, because expressions are not sandboxed any more (and the expression
|
||||
// sandbox was never meant to be a security feature anyway).
|
||||
var fn = $parse(attr[directiveName]);
|
||||
return function ngEventHandler(scope, element) {
|
||||
element.on(eventName, function(event) {
|
||||
var callback = function() {
|
||||
fn(scope, {$event: event});
|
||||
};
|
||||
|
||||
if (!$rootScope.$$phase) {
|
||||
scope.$apply(callback);
|
||||
} else if (forceAsyncEvents[eventName]) {
|
||||
scope.$evalAsync(callback);
|
||||
} else {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
$exceptionHandler(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
return createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsyncEvents[eventName]);
|
||||
}];
|
||||
}
|
||||
);
|
||||
|
||||
function createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsync) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
compile: function($element, attr) {
|
||||
// NOTE:
|
||||
// We expose the powerful `$event` object on the scope that provides access to the Window,
|
||||
// etc. This is OK, because expressions are not sandboxed any more (and the expression
|
||||
// sandbox was never meant to be a security feature anyway).
|
||||
var fn = $parse(attr[directiveName]);
|
||||
return function ngEventHandler(scope, element) {
|
||||
element.on(eventName, function(event) {
|
||||
var callback = function() {
|
||||
fn(scope, {$event: event});
|
||||
};
|
||||
|
||||
if (!$rootScope.$$phase) {
|
||||
scope.$apply(callback);
|
||||
} else if (forceAsync) {
|
||||
scope.$evalAsync(callback);
|
||||
} else {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
$exceptionHandler(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngDblclick
|
||||
|
||||
+148
-8
@@ -11480,7 +11480,7 @@ describe('$compile', function() {
|
||||
expect(element.attr('srcset')).toEqual('http://example.com');
|
||||
}));
|
||||
|
||||
it('does not work with trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
it('should NOT work with trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
// A limitation of the approach used for srcset is that you cannot use `trustAsUrl`.
|
||||
// Use trustAsHtml and ng-bind-html to work around this.
|
||||
element = $compile('<img srcset="{{testUrl}}"></img>')($rootScope);
|
||||
@@ -11705,18 +11705,19 @@ describe('$compile', function() {
|
||||
expect(function() {
|
||||
$compile('<button onclick="{{onClickJs}}"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
|
||||
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ONCLICK="{{onClickJs}}"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
|
||||
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ng-attr-onclick="{{onClickJs}}"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
|
||||
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ng-attr-ONCLICK="{{onClickJs}}"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
}));
|
||||
|
||||
it('should pass through arbitrary values on onXYZ event attributes that contain a hyphen', inject(function($compile, $rootScope) {
|
||||
@@ -11833,7 +11834,7 @@ describe('$compile', function() {
|
||||
}));
|
||||
|
||||
|
||||
it('should pass through $sce.trustAs() values in action attribute', inject(function($compile, $rootScope, $sce) {
|
||||
it('should pass through $sce.trustAsResourceUrl() values in action attribute', inject(function($compile, $rootScope, $sce) {
|
||||
element = $compile('<form action="{{testUrl}}"></form>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
|
||||
$rootScope.$apply();
|
||||
@@ -12026,6 +12027,39 @@ describe('$compile', function() {
|
||||
expect(element.attr('test3')).toBe('Misko');
|
||||
}));
|
||||
|
||||
it('should use the non-prefixed name in $attr mappings', function() {
|
||||
var attrs;
|
||||
module(function() {
|
||||
directive('attrExposer', valueFn({
|
||||
link: function($scope, $element, $attrs) {
|
||||
attrs = $attrs;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<div attr-exposer ng-attr-title="12" ng-attr-super-title="34" ng-attr-my-camel_title="56">')($rootScope);
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(attrs.title).toBe('12');
|
||||
expect(attrs.$attr.title).toBe('title');
|
||||
expect(attrs.ngAttrTitle).toBeUndefined();
|
||||
expect(attrs.$attr.ngAttrTitle).toBeUndefined();
|
||||
|
||||
expect(attrs.superTitle).toBe('34');
|
||||
expect(attrs.$attr.superTitle).toBe('super-title');
|
||||
expect(attrs.ngAttrSuperTitle).toBeUndefined();
|
||||
expect(attrs.$attr.ngAttrSuperTitle).toBeUndefined();
|
||||
|
||||
// Note the casing is incorrect: https://github.com/angular/angular.js/issues/16624
|
||||
expect(attrs.myCameltitle).toBe('56');
|
||||
expect(attrs.$attr.myCameltitle).toBe('my-camelTitle');
|
||||
expect(attrs.ngAttrMyCameltitle).toBeUndefined();
|
||||
expect(attrs.ngAttrMyCamelTitle).toBeUndefined();
|
||||
expect(attrs.$attr.ngAttrMyCameltitle).toBeUndefined();
|
||||
expect(attrs.$attr.ngAttrMyCamelTitle).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with the "href" attribute', inject(function() {
|
||||
$rootScope.value = 'test';
|
||||
element = $compile('<a ng-attr-href="test/{{value}}"></a>')($rootScope);
|
||||
@@ -12112,6 +12146,112 @@ describe('$compile', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('addPropertySecurityContext', function() {
|
||||
function testProvider(provider) {
|
||||
module(provider);
|
||||
inject(function($compile) { /* done! */ });
|
||||
}
|
||||
|
||||
it('should allow adding new properties', function() {
|
||||
testProvider(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
|
||||
$compileProvider.addPropertySecurityContext('*', 'my-prop', 'resourceUrl');
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow different sce types of a property on different element types', function() {
|
||||
testProvider(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
|
||||
$compileProvider.addPropertySecurityContext('span', 'title', 'css');
|
||||
$compileProvider.addPropertySecurityContext('*', 'title', 'resourceUrl');
|
||||
$compileProvider.addPropertySecurityContext('article', 'title', 'html');
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw \'ctxoverride\' when changing an existing context', function() {
|
||||
testProvider(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
|
||||
|
||||
expect(function() {
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'resourceUrl');
|
||||
})
|
||||
.toThrowMinErr('$compile', 'ctxoverride', 'Property context \'div.title\' already set to \'mediaUrl\', cannot override to \'resourceUrl\'.');
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow setting the same property/element to the same value', function() {
|
||||
testProvider(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
|
||||
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
|
||||
});
|
||||
});
|
||||
|
||||
it('should enforce the specified sce type for properties added for specific elements', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('div', 'foo', 'mediaUrl');
|
||||
});
|
||||
inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
|
||||
|
||||
$rootScope.bar = 'untrusted:test1';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
|
||||
|
||||
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
|
||||
|
||||
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('untrusted:test3');
|
||||
});
|
||||
});
|
||||
|
||||
it('should enforce the specified sce type for properties added for all elements (*)', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('*', 'foo', 'mediaUrl');
|
||||
});
|
||||
inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
|
||||
|
||||
$rootScope.bar = 'untrusted:test1';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
|
||||
|
||||
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
|
||||
|
||||
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('untrusted:test3');
|
||||
});
|
||||
});
|
||||
|
||||
it('should enforce the specific sce type when both an element specific and generic exist', function() {
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.addPropertySecurityContext('*', 'foo', 'css');
|
||||
$compileProvider.addPropertySecurityContext('div', 'foo', 'mediaUrl');
|
||||
});
|
||||
inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
|
||||
|
||||
$rootScope.bar = 'untrusted:test1';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
|
||||
|
||||
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
|
||||
|
||||
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('foo')).toBe('untrusted:test3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('when an attribute has an underscore-separated name', function() {
|
||||
|
||||
it('should work with different prefixes', inject(function($compile, $rootScope) {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
'use strict';
|
||||
|
||||
describe('ngOn* event binding', function() {
|
||||
it('should add event listener of specified name', inject(function($compile, $rootScope) {
|
||||
$rootScope.name = 'Misko';
|
||||
var element = $compile('<span ng-on-foo="name = name + 3"></span>')($rootScope);
|
||||
element.triggerHandler('foo');
|
||||
expect($rootScope.name).toBe('Misko3');
|
||||
}));
|
||||
|
||||
it('should use angular.element(x).on() API to add listener', inject(function($compile, $rootScope) {
|
||||
spyOn(angular.element.prototype, 'on');
|
||||
|
||||
var element = $compile('<span ng-on-foo="name = name + 3"></span>')($rootScope);
|
||||
|
||||
expect(angular.element.prototype.on).toHaveBeenCalledWith('foo', jasmine.any(Function));
|
||||
}));
|
||||
|
||||
it('should allow access to the $event object', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-on-foo="e = $event"></span>')($rootScope);
|
||||
element.triggerHandler('foo');
|
||||
expect($rootScope.e.target).toBe(element[0]);
|
||||
}));
|
||||
|
||||
it('should call the listener synchronously', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<span ng-on-foo="fooEvent()"></span>')($rootScope);
|
||||
$rootScope.fooEvent = jasmine.createSpy('fooEvent');
|
||||
|
||||
element.triggerHandler('foo');
|
||||
|
||||
expect($rootScope.fooEvent).toHaveBeenCalledOnce();
|
||||
}));
|
||||
|
||||
it('should support multiple events on a single element', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<span ng-on-foo="fooEvent()" ng-on-bar="barEvent()"></span>')($rootScope);
|
||||
$rootScope.fooEvent = jasmine.createSpy('fooEvent');
|
||||
$rootScope.barEvent = jasmine.createSpy('barEvent');
|
||||
|
||||
element.triggerHandler('foo');
|
||||
expect($rootScope.fooEvent).toHaveBeenCalled();
|
||||
expect($rootScope.barEvent).not.toHaveBeenCalled();
|
||||
|
||||
$rootScope.fooEvent.calls.reset();
|
||||
$rootScope.barEvent.calls.reset();
|
||||
|
||||
element.triggerHandler('bar');
|
||||
expect($rootScope.fooEvent).not.toHaveBeenCalled();
|
||||
expect($rootScope.barEvent).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should work with different prefixes', inject(function($rootScope, $compile) {
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
var element = $compile('<span ng:on:test="cb(1)" ng-On-test2="cb(2)" ng_On_test3="cb(3)"></span>')($rootScope);
|
||||
|
||||
element.triggerHandler('test');
|
||||
expect(cb).toHaveBeenCalledWith(1);
|
||||
|
||||
element.triggerHandler('test2');
|
||||
expect(cb).toHaveBeenCalledWith(2);
|
||||
|
||||
element.triggerHandler('test3');
|
||||
expect(cb).toHaveBeenCalledWith(3);
|
||||
}));
|
||||
|
||||
it('should work if they are prefixed with x- or data- and different prefixes', inject(function($rootScope, $compile) {
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
var element = $compile('<span data-ng-on-test2="cb(2)" x-ng-on-test3="cb(3)" data-ng:on-test4="cb(4)" ' +
|
||||
'x_ng-on-test5="cb(5)" data:ng-on-test6="cb(6)"></span>')($rootScope);
|
||||
|
||||
element.triggerHandler('test2');
|
||||
expect(cb).toHaveBeenCalledWith(2);
|
||||
|
||||
element.triggerHandler('test3');
|
||||
expect(cb).toHaveBeenCalledWith(3);
|
||||
|
||||
element.triggerHandler('test4');
|
||||
expect(cb).toHaveBeenCalledWith(4);
|
||||
|
||||
element.triggerHandler('test5');
|
||||
expect(cb).toHaveBeenCalledWith(5);
|
||||
|
||||
element.triggerHandler('test6');
|
||||
expect(cb).toHaveBeenCalledWith(6);
|
||||
}));
|
||||
|
||||
it('should work independently of attributes with the same name', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-on-asdf="cb()" asdf="foo" />')($rootScope);
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
$rootScope.$digest();
|
||||
element.triggerHandler('asdf');
|
||||
expect(cb).toHaveBeenCalled();
|
||||
expect(element.attr('asdf')).toBe('foo');
|
||||
}));
|
||||
|
||||
it('should work independently of (ng-)attributes with the same name', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-on-asdf="cb()" ng-attr-asdf="foo" />')($rootScope);
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
$rootScope.$digest();
|
||||
element.triggerHandler('asdf');
|
||||
expect(cb).toHaveBeenCalled();
|
||||
expect(element.attr('asdf')).toBe('foo');
|
||||
}));
|
||||
|
||||
it('should work independently of properties with the same name', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-on-asdf="cb()" ng-prop-asdf="123" />')($rootScope);
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
$rootScope.$digest();
|
||||
element.triggerHandler('asdf');
|
||||
expect(cb).toHaveBeenCalled();
|
||||
expect(element.prop('asdf')).toBe(123);
|
||||
}));
|
||||
|
||||
it('should use the full ng-on-* attribute name in $attr mappings', function() {
|
||||
var attrs;
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('attrExposer', valueFn({
|
||||
link: function($scope, $element, $attrs) {
|
||||
attrs = $attrs;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<div attr-exposer ng-on-title="cb(1)" ng-on-super-title="cb(2)" ng-on-my-camel_title="cb(3)">')($rootScope);
|
||||
|
||||
expect(attrs.title).toBeUndefined();
|
||||
expect(attrs.$attr.title).toBeUndefined();
|
||||
expect(attrs.ngOnTitle).toBe('cb(1)');
|
||||
expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
|
||||
|
||||
expect(attrs.superTitle).toBeUndefined();
|
||||
expect(attrs.$attr.superTitle).toBeUndefined();
|
||||
expect(attrs.ngOnSuperTitle).toBe('cb(2)');
|
||||
expect(attrs.$attr.ngOnSuperTitle).toBe('ng-on-super-title');
|
||||
|
||||
expect(attrs.myCamelTitle).toBeUndefined();
|
||||
expect(attrs.$attr.myCamelTitle).toBeUndefined();
|
||||
expect(attrs.ngOnMyCamelTitle).toBe('cb(3)');
|
||||
expect(attrs.$attr.ngOnMyCamelTitle).toBe('ng-on-my-camel_title');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not conflict with (ng-attr-)attribute mappings of the same name', function() {
|
||||
var attrs;
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('attrExposer', valueFn({
|
||||
link: function($scope, $element, $attrs) {
|
||||
attrs = $attrs;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<div attr-exposer ng-on-title="42" ng-attr-title="foo" title="bar">')($rootScope);
|
||||
expect(attrs.title).toBe('foo');
|
||||
expect(attrs.$attr.title).toBe('title');
|
||||
expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,836 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint-disable no-script-url */
|
||||
|
||||
describe('ngProp*', function() {
|
||||
it('should bind boolean properties (input disabled)', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<button ng-prop-disabled="isDisabled">Button</button>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('disabled')).toBe(false);
|
||||
$rootScope.isDisabled = true;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('disabled')).toBe(true);
|
||||
$rootScope.isDisabled = false;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('disabled')).toBe(false);
|
||||
}));
|
||||
|
||||
it('should bind boolean properties (input checked)', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<input type="checkbox" ng-prop-checked="isChecked" />')($rootScope);
|
||||
expect(element.prop('checked')).toBe(false);
|
||||
$rootScope.isChecked = true;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('checked')).toBe(true);
|
||||
$rootScope.isChecked = false;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('checked')).toBe(false);
|
||||
}));
|
||||
|
||||
it('should bind string properties (title)', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-title="title" />')($rootScope);
|
||||
$rootScope.title = 123;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('title')).toBe('123');
|
||||
$rootScope.title = 'foobar';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('title')).toBe('foobar');
|
||||
}));
|
||||
|
||||
it('should bind variable type properties', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-asdf="asdf" />')($rootScope);
|
||||
$rootScope.asdf = 123;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('asdf')).toBe(123);
|
||||
$rootScope.asdf = 'foobar';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('asdf')).toBe('foobar');
|
||||
$rootScope.asdf = true;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('asdf')).toBe(true);
|
||||
}));
|
||||
|
||||
it('should support mixed case using underscore-separated names', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-a_bcd_e="value" />')($rootScope);
|
||||
$rootScope.value = 123;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('aBcdE')).toBe(123);
|
||||
}));
|
||||
|
||||
it('should work with different prefixes', inject(function($rootScope, $compile) {
|
||||
$rootScope.name = 'Misko';
|
||||
var element = $compile('<span ng:prop:test="name" ng-Prop-test2="name" ng_Prop_test3="name"></span>')($rootScope);
|
||||
expect(element.prop('test')).toBe('Misko');
|
||||
expect(element.prop('test2')).toBe('Misko');
|
||||
expect(element.prop('test3')).toBe('Misko');
|
||||
}));
|
||||
|
||||
it('should work with the "href" property', inject(function($rootScope, $compile) {
|
||||
$rootScope.value = 'test';
|
||||
var element = $compile('<a ng-prop-href="\'test/\' + value"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toMatch(/\/test\/test$/);
|
||||
}));
|
||||
|
||||
it('should work if they are prefixed with x- or data- and different prefixes', inject(function($rootScope, $compile) {
|
||||
$rootScope.name = 'Misko';
|
||||
var element = $compile('<span data-ng-prop-test2="name" x-ng-prop-test3="name" data-ng:prop-test4="name" ' +
|
||||
'x_ng-prop-test5="name" data:ng-prop-test6="name"></span>')($rootScope);
|
||||
expect(element.prop('test2')).toBe('Misko');
|
||||
expect(element.prop('test3')).toBe('Misko');
|
||||
expect(element.prop('test4')).toBe('Misko');
|
||||
expect(element.prop('test5')).toBe('Misko');
|
||||
expect(element.prop('test6')).toBe('Misko');
|
||||
}));
|
||||
|
||||
it('should work independently of attributes with the same name', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-asdf="asdf" asdf="foo" />')($rootScope);
|
||||
$rootScope.asdf = 123;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('asdf')).toBe(123);
|
||||
expect(element.attr('asdf')).toBe('foo');
|
||||
}));
|
||||
|
||||
it('should work independently of (ng-)attributes with the same name', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-asdf="asdf" ng-attr-asdf="foo" />')($rootScope);
|
||||
$rootScope.asdf = 123;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('asdf')).toBe(123);
|
||||
expect(element.attr('asdf')).toBe('foo');
|
||||
}));
|
||||
|
||||
it('should use the full ng-prop-* attribute name in $attr mappings', function() {
|
||||
var attrs;
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('attrExposer', valueFn({
|
||||
link: function($scope, $element, $attrs) {
|
||||
attrs = $attrs;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<div attr-exposer ng-prop-title="12" ng-prop-super-title="34" ng-prop-my-camel_title="56">')($rootScope);
|
||||
|
||||
expect(attrs.title).toBeUndefined();
|
||||
expect(attrs.$attr.title).toBeUndefined();
|
||||
expect(attrs.ngPropTitle).toBe('12');
|
||||
expect(attrs.$attr.ngPropTitle).toBe('ng-prop-title');
|
||||
|
||||
expect(attrs.superTitle).toBeUndefined();
|
||||
expect(attrs.$attr.superTitle).toBeUndefined();
|
||||
expect(attrs.ngPropSuperTitle).toBe('34');
|
||||
expect(attrs.$attr.ngPropSuperTitle).toBe('ng-prop-super-title');
|
||||
|
||||
expect(attrs.myCamelTitle).toBeUndefined();
|
||||
expect(attrs.$attr.myCamelTitle).toBeUndefined();
|
||||
expect(attrs.ngPropMyCamelTitle).toBe('56');
|
||||
expect(attrs.$attr.ngPropMyCamelTitle).toBe('ng-prop-my-camel_title');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not conflict with (ng-attr-)attribute mappings of the same name', function() {
|
||||
var attrs;
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('attrExposer', valueFn({
|
||||
link: function($scope, $element, $attrs) {
|
||||
attrs = $attrs;
|
||||
}
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
$compile('<div attr-exposer ng-prop-title="42" ng-attr-title="foo" title="bar">')($rootScope);
|
||||
expect(attrs.title).toBe('foo');
|
||||
expect(attrs.$attr.title).toBe('title');
|
||||
expect(attrs.$attr.ngPropTitle).toBe('ng-prop-title');
|
||||
});
|
||||
});
|
||||
|
||||
it('should disallow property binding to onclick', inject(function($compile, $rootScope) {
|
||||
// All event prop bindings are disallowed.
|
||||
expect(function() {
|
||||
$compile('<button ng-prop-onclick="onClickJs"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ng-prop-ONCLICK="onClickJs"></script>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
|
||||
}));
|
||||
|
||||
it('should process property bindings in pre-linking phase at priority 100', function() {
|
||||
module(provideLog);
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('propLog', function(log, $rootScope) {
|
||||
return {
|
||||
compile: function($element, $attrs) {
|
||||
log('compile=' + $element.prop('myName'));
|
||||
|
||||
return {
|
||||
pre: function($scope, $element, $attrs) {
|
||||
log('preLinkP0=' + $element.prop('myName'));
|
||||
$rootScope.name = 'pre0';
|
||||
},
|
||||
post: function($scope, $element, $attrs) {
|
||||
log('postLink=' + $element.prop('myName'));
|
||||
$rootScope.name = 'post0';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('propLogHighPriority', function(log, $rootScope) {
|
||||
return {
|
||||
priority: 101,
|
||||
compile: function() {
|
||||
return {
|
||||
pre: function($scope, $element, $attrs) {
|
||||
log('preLinkP101=' + $element.prop('myName'));
|
||||
$rootScope.name = 'pre101';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
inject(function($rootScope, $compile, log) {
|
||||
var element = $compile('<div prop-log-high-priority prop-log ng-prop-my_name="name"></div>')($rootScope);
|
||||
$rootScope.name = 'angular';
|
||||
$rootScope.$apply();
|
||||
log('digest=' + element.prop('myName'));
|
||||
expect(log).toEqual('compile=undefined; preLinkP101=undefined; preLinkP0=pre101; postLink=pre101; digest=angular');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
['img', 'audio', 'video'].forEach(function(tag) {
|
||||
// Support: IE 9 only
|
||||
// IE9 rejects the `video` / `audio` tags with "Error: Not implemented"
|
||||
if (msie !== 9 || tag === 'img') {
|
||||
describe(tag + '[src] context requirement', function() {
|
||||
it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
$rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('http://example.com/image.mp4');
|
||||
}));
|
||||
|
||||
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
// As a MEDIA_URL URL
|
||||
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
// Some browsers complain if you try to write `javascript:` into an `img[src]`
|
||||
// So for the test use something different
|
||||
$rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('untrusted:foo()');
|
||||
|
||||
// As a URL
|
||||
element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('untrusted:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('untrusted:foo()');
|
||||
|
||||
// As a RESOURCE URL
|
||||
element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('untrusted:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('untrusted:foo()');
|
||||
}));
|
||||
|
||||
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile, $sce) {
|
||||
// As a MEDIA_URL URL
|
||||
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
// Some browsers complain if you try to write `javascript:` into an `img[src]`
|
||||
// So for the test use something different
|
||||
$rootScope.testUrl = 'untrusted:foo()';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('unsafe:untrusted:foo()');
|
||||
}));
|
||||
|
||||
it('should sanitize wrongly typed values', inject(function($rootScope, $compile, $sce) {
|
||||
// As a MEDIA_URL URL
|
||||
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
|
||||
// Some browsers complain if you try to write `javascript:` into an `img[src]`
|
||||
// So for the test use something different
|
||||
$rootScope.testUrl = $sce.trustAsCss('untrusted:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('unsafe:untrusted:foo()');
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Support: IE 9 only
|
||||
// IE 9 rejects the `source` / `track` tags with
|
||||
// "Unable to get value of the property 'childNodes': object is null or undefined"
|
||||
if (msie !== 9) {
|
||||
['source', 'track'].forEach(function(tag) {
|
||||
describe(tag + '[src]', function() {
|
||||
it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('http://example.com/image.mp4');
|
||||
}));
|
||||
|
||||
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
// As a MEDIA_URL URL
|
||||
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsMediaUrl('javascript:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
|
||||
|
||||
// As a URL
|
||||
element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
|
||||
|
||||
// As a RESOURCE URL
|
||||
element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
|
||||
}));
|
||||
|
||||
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = 'untrusted:foo()';
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('unsafe:untrusted:foo()');
|
||||
}));
|
||||
|
||||
it('should sanitize wrongly typed values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsCss('untrusted:foo()');
|
||||
$rootScope.$digest();
|
||||
expect(element.find(tag).prop('src')).toEqual('unsafe:untrusted:foo()');
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('img[src] sanitization', function() {
|
||||
|
||||
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
|
||||
// Some browsers complain if you try to write `javascript:` into an `img[src]`
|
||||
// So for the test use something different
|
||||
$rootScope.testUrl = $sce.trustAsMediaUrl('someuntrustedthing:foo();');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('src')).toEqual('someuntrustedthing:foo();');
|
||||
}));
|
||||
|
||||
it('should use $$sanitizeUri', function() {
|
||||
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
|
||||
module(function($provide) {
|
||||
$provide.value('$$sanitizeUri', $$sanitizeUri);
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
|
||||
$rootScope.testUrl = 'someUrl';
|
||||
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('src')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
|
||||
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use $$sanitizeUri with trusted values', function() {
|
||||
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.throwError('Should not have been called');
|
||||
module(function($provide) {
|
||||
$provide.value('$$sanitizeUri', $$sanitizeUri);
|
||||
});
|
||||
inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
|
||||
// Assigning javascript:foo to src makes at least IE9-11 complain, so use another
|
||||
// protocol name.
|
||||
$rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo();');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('src')).toBe('untrusted:foo();');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
['img', 'source'].forEach(function(srcsetElement) {
|
||||
// Support: IE 9 only
|
||||
// IE9 ignores source[srcset] property assignments
|
||||
if (msie !== 9 || srcsetElement === 'img') {
|
||||
describe(srcsetElement + '[srcset] sanitization', function() {
|
||||
it('should not error if srcset is blank', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
// Set srcset to a value
|
||||
$rootScope.testUrl = 'http://example.com/';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toBe('http://example.com/');
|
||||
|
||||
// Now set it to blank
|
||||
$rootScope.testUrl = '';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toBe('');
|
||||
}));
|
||||
|
||||
it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toEqual('http://example.com/image.png');
|
||||
}));
|
||||
|
||||
it('should accept trusted values, if they are also whitelisted', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('http://example.com');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toEqual('http://example.com');
|
||||
}));
|
||||
|
||||
it('should NOT work with trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
// A limitation of the approach used for srcset is that you cannot use `trustAsUrl`.
|
||||
// Use trustAsHtml and ng-bind-html to work around this.
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:something');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toEqual('unsafe:javascript:something');
|
||||
|
||||
element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl + \',\' + testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:something');
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toEqual(
|
||||
'unsafe:javascript:something ,unsafe:javascript:something');
|
||||
}));
|
||||
|
||||
it('should use $$sanitizeUri', function() {
|
||||
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
|
||||
module(function($provide) {
|
||||
$provide.value('$$sanitizeUri', $$sanitizeUri);
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = 'someUrl';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('srcset')).toBe('someSanitizedUrl');
|
||||
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
|
||||
|
||||
element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl + \',\' + testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = 'javascript:yay';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl');
|
||||
|
||||
element = $compile('<' + srcsetElement + ' ng-prop-srcset="\'java\' + testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
$rootScope.testUrl = 'script:yay, javascript:nay';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl');
|
||||
});
|
||||
});
|
||||
|
||||
it('should sanitize all uris in srcset', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
|
||||
var testSet = {
|
||||
'http://example.com/image.png':'http://example.com/image.png',
|
||||
' http://example.com/image.png':'http://example.com/image.png',
|
||||
'http://example.com/image.png ':'http://example.com/image.png',
|
||||
'http://example.com/image.png 128w':'http://example.com/image.png 128w',
|
||||
'http://example.com/image.png 2x':'http://example.com/image.png 2x',
|
||||
'http://example.com/image.png 1.5x':'http://example.com/image.png 1.5x',
|
||||
'http://example.com/image1.png 1x,http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
|
||||
'http://example.com/image1.png 1x ,http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
|
||||
'http://example.com/image1.png 1x, http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
|
||||
'http://example.com/image1.png 1x , http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
|
||||
'http://example.com/image1.png 48w,http://example.com/image2.png 64w':'http://example.com/image1.png 48w,http://example.com/image2.png 64w',
|
||||
//Test regex to make sure doesn't mistake parts of url for width descriptors
|
||||
'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w':'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w',
|
||||
'http://example.com/image1.png 1x,http://example.com/image2.png 64w':'http://example.com/image1.png 1x,http://example.com/image2.png 64w',
|
||||
'http://example.com/image1.png,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
|
||||
'http://example.com/image1.png ,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
|
||||
'http://example.com/image1.png, http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
|
||||
'http://example.com/image1.png , http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
|
||||
'http://example.com/image1.png 1x, http://example.com/image2.png 2x, http://example.com/image3.png 3x':
|
||||
'http://example.com/image1.png 1x,http://example.com/image2.png 2x,http://example.com/image3.png 3x',
|
||||
'javascript:doEvilStuff() 2x': 'unsafe:javascript:doEvilStuff() 2x',
|
||||
'http://example.com/image1.png 1x,javascript:doEvilStuff() 2x':'http://example.com/image1.png 1x,unsafe:javascript:doEvilStuff() 2x',
|
||||
'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x',
|
||||
//Test regex to make sure doesn't mistake parts of url for pixel density descriptors
|
||||
'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x'
|
||||
};
|
||||
|
||||
forEach(testSet, function(ref, url) {
|
||||
$rootScope.testUrl = url;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('srcset')).toEqual(ref);
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('a[href] sanitization', function() {
|
||||
it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile) {
|
||||
$rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted
|
||||
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('http://example.com/image.png');
|
||||
|
||||
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('http://example.com/image.png');
|
||||
}));
|
||||
|
||||
it('should accept trusted values for non-whitelisted values', inject(function($rootScope, $compile, $sce) {
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo()'); // `javascript` is not whitelisted
|
||||
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('javascript:foo()');
|
||||
|
||||
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('javascript:foo()');
|
||||
}));
|
||||
|
||||
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile) {
|
||||
$rootScope.testUrl = 'javascript:foo()'; // `javascript` is not whitelisted
|
||||
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('unsafe:javascript:foo()');
|
||||
|
||||
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('href')).toEqual('unsafe:javascript:foo()');
|
||||
}));
|
||||
|
||||
it('should not sanitize href on elements other than anchor', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<div ng-prop-href="testUrl"></div>')($rootScope);
|
||||
$rootScope.testUrl = 'javascript:doEvilStuff()';
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(element.prop('href')).toBe('javascript:doEvilStuff()');
|
||||
}));
|
||||
|
||||
it('should not sanitize properties other then those configured', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<a ng-prop-title="testUrl"></a>')($rootScope);
|
||||
$rootScope.testUrl = 'javascript:doEvilStuff()';
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(element.prop('title')).toBe('javascript:doEvilStuff()');
|
||||
}));
|
||||
|
||||
it('should use $$sanitizeUri', function() {
|
||||
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
|
||||
module(function($provide) {
|
||||
$provide.value('$$sanitizeUri', $$sanitizeUri);
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.testUrl = 'someUrl';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('href')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
|
||||
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
|
||||
|
||||
$$sanitizeUri.calls.reset();
|
||||
|
||||
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('href')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
|
||||
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not have endless digests when given arrays in concatenable context', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<foo ng-prop-href="testUrl"></foo><foo ng-prop-href="::testUrl"></foo>' +
|
||||
'<foo ng-prop-href="\'http://example.com/\' + testUrl"></foo><foo ng-prop-href="::\'http://example.com/\' + testUrl"></foo>')($rootScope);
|
||||
$rootScope.testUrl = [1];
|
||||
$rootScope.$digest();
|
||||
|
||||
$rootScope.testUrl = [];
|
||||
$rootScope.$digest();
|
||||
|
||||
$rootScope.testUrl = {a:'b'};
|
||||
$rootScope.$digest();
|
||||
|
||||
$rootScope.testUrl = {};
|
||||
$rootScope.$digest();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('iframe[src]', function() {
|
||||
it('should pass through src properties for the same domain', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
|
||||
$rootScope.testUrl = 'different_page';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('src')).toMatch(/\/different_page$/);
|
||||
}));
|
||||
|
||||
it('should clear out src properties for a different domain', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
|
||||
$rootScope.testUrl = 'http://a.different.domain.example.com';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: http://a.different.domain.example.com');
|
||||
}));
|
||||
|
||||
it('should clear out JS src properties', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
|
||||
$rootScope.testUrl = 'javascript:alert(1);';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: javascript:alert(1);');
|
||||
}));
|
||||
|
||||
it('should clear out non-resource_url src properties', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:doTrustedStuff()');
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: javascript:doTrustedStuff()');
|
||||
}));
|
||||
|
||||
it('should pass through $sce.trustAs() values in src properties', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(element.prop('src')).toEqual('javascript:doTrustedStuff()');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('base[href]', function() {
|
||||
it('should be a RESOURCE_URL context', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<base ng-prop-href="testUrl"/>')($rootScope);
|
||||
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('https://example.com/');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('href')).toContain('https://example.com/');
|
||||
|
||||
$rootScope.testUrl = 'https://not.example.com/';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: https://not.example.com/');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('form[action]', function() {
|
||||
it('should pass through action property for the same domain', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
|
||||
$rootScope.testUrl = 'different_page';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('action')).toMatch(/\/different_page$/);
|
||||
}));
|
||||
|
||||
it('should clear out action property for a different domain', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
|
||||
$rootScope.testUrl = 'http://a.different.domain.example.com';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: http://a.different.domain.example.com');
|
||||
}));
|
||||
|
||||
it('should clear out JS action property', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
|
||||
$rootScope.testUrl = 'javascript:alert(1);';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: javascript:alert(1);');
|
||||
}));
|
||||
|
||||
it('should clear out non-resource_url action property', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsUrl('javascript:doTrustedStuff()');
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: javascript:doTrustedStuff()');
|
||||
}));
|
||||
|
||||
|
||||
it('should pass through $sce.trustAsResourceUrl() values in action property', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(element.prop('action')).toEqual('javascript:doTrustedStuff()');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('link[href]', function() {
|
||||
it('should reject invalid RESOURCE_URLs', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<link ng-prop-href="testUrl" rel="stylesheet" />')($rootScope);
|
||||
$rootScope.testUrl = 'https://evil.example.org/css.css';
|
||||
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
|
||||
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
|
||||
' URL: https://evil.example.org/css.css');
|
||||
}));
|
||||
|
||||
it('should accept valid RESOURCE_URLs', inject(function($compile, $rootScope, $sce) {
|
||||
var element = $compile('<link ng-prop-href="testUrl" rel="stylesheet" />')($rootScope);
|
||||
|
||||
$rootScope.testUrl = './css1.css';
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('href')).toContain('css1.css');
|
||||
|
||||
$rootScope.testUrl = $sce.trustAsResourceUrl('https://elsewhere.example.org/css2.css');
|
||||
$rootScope.$apply();
|
||||
expect(element.prop('href')).toContain('https://elsewhere.example.org/css2.css');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('*[innerHTML]', function() {
|
||||
describe('SCE disabled', function() {
|
||||
beforeEach(function() {
|
||||
module(function($sceProvider) { $sceProvider.enabled(false); });
|
||||
});
|
||||
|
||||
it('should set html', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
|
||||
}));
|
||||
|
||||
it('should update html', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = 'hello';
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('hello');
|
||||
$rootScope.html = 'goodbye';
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('goodbye');
|
||||
}));
|
||||
|
||||
it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="::html"></div>')($rootScope);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
expect($rootScope.$$watchers.length).toEqual(1);
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('hello');
|
||||
expect($rootScope.$$watchers.length).toEqual(0);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toEqual('hello');
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
describe('SCE enabled', function() {
|
||||
it('should NOT set html for untrusted values', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
|
||||
}));
|
||||
|
||||
it('should NOT set html for wrongly typed values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = $sce.trustAsCss('<div onclick="">hello</div>');
|
||||
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
|
||||
}));
|
||||
|
||||
it('should set html for trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = $sce.trustAsHtml('<div onclick="">hello</div>');
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
|
||||
}));
|
||||
|
||||
it('should update html', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = $sce.trustAsHtml('hello');
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('hello');
|
||||
$rootScope.html = $sce.trustAsHtml('goodbye');
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('goodbye');
|
||||
}));
|
||||
|
||||
it('should not cause infinite recursion for trustAsHtml object watches',
|
||||
inject(function($rootScope, $compile, $sce) {
|
||||
// Ref: https://github.com/angular/angular.js/issues/3932
|
||||
// If the binding is a function that creates a new value on every call via trustAs, we'll
|
||||
// trigger an infinite digest if we don't take care of it.
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="getHtml()"></div>')($rootScope);
|
||||
$rootScope.getHtml = function() {
|
||||
return $sce.trustAsHtml('<div onclick="">hello</div>');
|
||||
};
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
|
||||
}));
|
||||
|
||||
it('should handle custom $sce objects', function() {
|
||||
function MySafeHtml(val) { this.val = val; }
|
||||
|
||||
module(function($provide) {
|
||||
$provide.decorator('$sce', function($delegate) {
|
||||
$delegate.trustAsHtml = function(html) { return new MySafeHtml(html); };
|
||||
$delegate.getTrusted = function(type, mySafeHtml) { return mySafeHtml && mySafeHtml.val; };
|
||||
$delegate.valueOf = function(v) { return v instanceof MySafeHtml ? v.val : v; };
|
||||
return $delegate;
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($rootScope, $compile, $sce) {
|
||||
// Ref: https://github.com/angular/angular.js/issues/14526
|
||||
// Previous code used toString for change detection, which fails for custom objects
|
||||
// that don't override toString.
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="getHtml()"></div>')($rootScope);
|
||||
var html = 'hello';
|
||||
$rootScope.getHtml = function() { return $sce.trustAsHtml(html); };
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('hello');
|
||||
html = 'goodbye';
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('goodbye');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when $sanitize is available', function() {
|
||||
beforeEach(function() { module('ngSanitize'); });
|
||||
|
||||
it('should sanitize untrusted html', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
$rootScope.$digest();
|
||||
expect(lowercase(element.html())).toEqual('<div>hello</div>');
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('*[style]', function() {
|
||||
// Support: IE9
|
||||
// Some browsers throw when assignging to HTMLElement.style
|
||||
function canAssignStyleProp() {
|
||||
try {
|
||||
window.document.createElement('div').style = 'margin-left: 10px';
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
it('should NOT set style for untrusted values', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
|
||||
$rootScope.style = 'margin-left: 10px';
|
||||
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
|
||||
}));
|
||||
|
||||
it('should NOT set style for wrongly typed values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
|
||||
$rootScope.style = $sce.trustAsHtml('margin-left: 10px');
|
||||
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
|
||||
}));
|
||||
|
||||
if (canAssignStyleProp()) {
|
||||
it('should set style for trusted values', inject(function($rootScope, $compile, $sce) {
|
||||
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
|
||||
$rootScope.style = $sce.trustAsCss('margin-left: 10px');
|
||||
$rootScope.$digest();
|
||||
|
||||
// Support: IE
|
||||
// IE allows assignments but does not register the styles
|
||||
// Sometimes the value is '0px', sometimes ''
|
||||
if (msie) {
|
||||
expect(parseInt(element.css('margin-left'), 10) || 0).toBe(0);
|
||||
} else {
|
||||
expect(element.css('margin-left')).toEqual('10px');
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user