From 51c516e7d4f2d10b0aaa4487bd0b52772022207a Mon Sep 17 00:00:00 2001
From: Austin O'Neil
Date: Fri, 9 Sep 2016 15:34:29 -0600
Subject: [PATCH 0001/1014] docs(ngOptions): correct links
remove redundant link to ngOptions and add link to ngRepeat
PR (#15117)
---
src/ng/directive/ngOptions.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js
index 8168b4110fdd..9b58f9b985fd 100644
--- a/src/ng/directive/ngOptions.js
+++ b/src/ng/directive/ngOptions.js
@@ -17,8 +17,8 @@ var ngOptionsMinErr = minErr('ngOptions');
* elements for the `` element using the array or object obtained by evaluating the
* `ngOptions` comprehension expression.
*
- * In many cases, `ngRepeat` can be used on `` elements instead of {@link ng.directive:ngOptions
- * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits:
+ * In many cases, {@link ng.directive:ngRepeat ngRepeat} can be used on ` ` elements instead of
+ * `ngOptions` to achieve a similar result. However, `ngOptions` provides some benefits:
* - more flexibility in how the ``'s model is assigned via the `select` **`as`** part of the
* comprehension expression
* - reduced memory consumption by not creating a new scope for each repeated instance
From 78e6a58368470cef3454b33acd8ee788f2eb88e2 Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Sun, 5 Jun 2016 18:30:13 -0700
Subject: [PATCH 0002/1014] refactor($q): remove unnecessary
checks/helpers/wrappers
- Remove internal `makePromise()` helper.
- Remove unnecessary wrapper functions.
- Remove unnecessary check for promises resolving multiple times.
(By following the Promises/A+ spec, we know this will never happen.)
- Switch from function expressions to (named) function declarations.
Closes #15065
---
src/ng/q.js | 60 +++++++++++++++++------------------------------------
1 file changed, 19 insertions(+), 41 deletions(-)
diff --git a/src/ng/q.js b/src/ng/q.js
index 8664c992a43c..7b9de29b0d52 100644
--- a/src/ng/q.js
+++ b/src/ng/q.js
@@ -298,14 +298,14 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
*
* @returns {Deferred} Returns a new instance of deferred.
*/
- var defer = function() {
+ function defer() {
var d = new Deferred();
//Necessary to support unbound execution :/
d.resolve = simpleBind(d, d.resolve);
d.reject = simpleBind(d, d.reject);
d.notify = simpleBind(d, d.notify);
return d;
- };
+ }
function Promise() {
this.$$state = { status: 0 };
@@ -331,9 +331,9 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
'finally': function(callback, progressBack) {
return this.then(function(value) {
- return handleCallback(value, true, callback);
+ return handleCallback(value, resolve, callback);
}, function(error) {
- return handleCallback(error, false, callback);
+ return handleCallback(error, reject, callback);
}, progressBack);
}
});
@@ -372,7 +372,7 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
} finally {
--queueSize;
if (errorOnUnhandledRejections && queueSize === 0) {
- nextTick(processChecksFn());
+ nextTick(processChecks);
}
}
}
@@ -389,25 +389,17 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
}
}
- function processChecksFn() {
- return function() { processChecks(); };
- }
-
- function processQueueFn(state) {
- return function() { processQueue(state); };
- }
-
function scheduleProcessQueue(state) {
if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) {
if (queueSize === 0 && checkQueue.length === 0) {
- nextTick(processChecksFn());
+ nextTick(processChecks);
}
checkQueue.push(state);
}
if (state.processScheduled || !state.pending) return;
state.processScheduled = true;
++queueSize;
- nextTick(processQueueFn(state));
+ nextTick(function() { processQueue(state); });
}
function Deferred() {
@@ -526,39 +518,27 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
* @param {*} reason Constant, message, exception or an object representing the rejection reason.
* @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`.
*/
- var reject = function(reason) {
+ function reject(reason) {
var result = new Deferred();
result.reject(reason);
return result.promise;
- };
-
- var makePromise = function makePromise(value, resolved) {
- var result = new Deferred();
- if (resolved) {
- result.resolve(value);
- } else {
- result.reject(value);
- }
- return result.promise;
- };
+ }
- var handleCallback = function handleCallback(value, isResolved, callback) {
+ function handleCallback(value, resolver, callback) {
var callbackOutput = null;
try {
if (isFunction(callback)) callbackOutput = callback();
} catch (e) {
- return makePromise(e, false);
+ return reject(e);
}
if (isPromiseLike(callbackOutput)) {
return callbackOutput.then(function() {
- return makePromise(value, isResolved);
- }, function(error) {
- return makePromise(error, false);
- });
+ return resolver(value);
+ }, reject);
} else {
- return makePromise(value, isResolved);
+ return resolver(value);
}
- };
+ }
/**
* @ngdoc method
@@ -578,11 +558,11 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
*/
- var when = function(value, callback, errback, progressBack) {
+ function when(value, callback, errback, progressBack) {
var result = new Deferred();
result.resolve(value);
return result.promise.then(callback, errback, progressBack);
- };
+ }
/**
* @ngdoc method
@@ -624,11 +604,9 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
forEach(promises, function(promise, key) {
counter++;
when(promise).then(function(value) {
- if (results.hasOwnProperty(key)) return;
results[key] = value;
if (!(--counter)) deferred.resolve(results);
}, function(reason) {
- if (results.hasOwnProperty(key)) return;
deferred.reject(reason);
});
});
@@ -664,7 +642,7 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
return deferred.promise;
}
- var $Q = function Q(resolver) {
+ function $Q(resolver) {
if (!isFunction(resolver)) {
throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver);
}
@@ -682,7 +660,7 @@ function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
resolver(resolveFn, rejectFn);
return deferred.promise;
- };
+ }
// Let's make the instanceof operator work for promises, so that
// `new $q(fn) instanceof $q` would evaluate to true.
From d14c7f3c31deb098bf8f1c50ea6d00af758dbdcb Mon Sep 17 00:00:00 2001
From: Georgii Dolzhykov
Date: Sat, 10 Sep 2016 02:15:00 +0300
Subject: [PATCH 0003/1014] docs($compile): remove obsolete sentence
Fixes #15109
Closes #15119
---
src/ng/compile.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/ng/compile.js b/src/ng/compile.js
index 4dcba455e5e5..8741dc4d074a 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -267,14 +267,14 @@
* and other directives used in the directive's template will also be excluded from execution.
*
* #### `scope`
- * The scope property can be `true`, an object or a falsy value:
+ * The scope property can be `false`, `true`, or an object:
*
- * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope.
+ * * **`false` (default):** No scope will be created for the directive. The directive will use its
+ * parent's scope.
*
* * **`true`:** A new child scope that prototypically inherits from its parent will be created for
* the directive's element. If multiple directives on the same element request a new scope,
- * only one new scope is created. The new scope rule does not apply for the root of the template
- * since the root of the template always gets a new scope.
+ * only one new scope is created.
*
* * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The
* 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent
From 912d5b9ad36efe3c4b8e0da4b20e2f3540472af8 Mon Sep 17 00:00:00 2001
From: Georgios Kalpakas
Date: Mon, 12 Sep 2016 17:27:37 +0300
Subject: [PATCH 0004/1014] docs(ngView): remove obsolete known issue
---
src/ngRoute/directive/ngView.js | 7 -------
1 file changed, 7 deletions(-)
diff --git a/src/ngRoute/directive/ngView.js b/src/ngRoute/directive/ngView.js
index dba842f1eafa..ead89554f10f 100644
--- a/src/ngRoute/directive/ngView.js
+++ b/src/ngRoute/directive/ngView.js
@@ -26,13 +26,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory);
*
* The enter and leave animation occur concurrently.
*
- * @knownIssue If `ngView` is contained in an asynchronously loaded template (e.g. in another
- * directive's templateUrl or in a template loaded using `ngInclude`), then you need to
- * make sure that `$route` is instantiated in time to capture the initial
- * `$locationChangeStart` event and load the appropriate view. One way to achieve this
- * is to have it as a dependency in a `.run` block:
- * `myModule.run(['$route', function() {}]);`
- *
* @scope
* @priority 400
* @param {string=} onload Expression to evaluate whenever the view updates.
From 07849779ba365f371a8caa3b58e23f677cfdc5ad Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Mon, 12 Sep 2016 17:59:54 +0200
Subject: [PATCH 0005/1014] chore(benchmarks): fix order-by benchmark
---
benchmarks/orderby-bp/app.js | 2 +-
benchmarks/orderby-bp/jquery-noop.js | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 benchmarks/orderby-bp/jquery-noop.js
diff --git a/benchmarks/orderby-bp/app.js b/benchmarks/orderby-bp/app.js
index 849042b87980..32bcfaeb0e12 100644
--- a/benchmarks/orderby-bp/app.js
+++ b/benchmarks/orderby-bp/app.js
@@ -7,7 +7,7 @@ app.controller('DataController', function DataController($rootScope, $scope) {
this.rows = [];
var self = this;
- $scope.benchmarkType = 'basic';
+ $scope.benchmarkType = 'baseline';
$scope.rawProperty = function(key) {
return function(item) {
diff --git a/benchmarks/orderby-bp/jquery-noop.js b/benchmarks/orderby-bp/jquery-noop.js
new file mode 100644
index 000000000000..8cac7fe4a149
--- /dev/null
+++ b/benchmarks/orderby-bp/jquery-noop.js
@@ -0,0 +1 @@
+// Override me with ?jquery=/bower_components/jquery/dist/jquery.js
From 21e4db9e0783d1f22b043cdb053e3b8c155a2786 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Freyre?=
Date: Wed, 14 Sep 2016 13:56:45 +0200
Subject: [PATCH 0006/1014] docs(input[range]): fix erroneous examples
PR (#15135)
---
src/ng/directive/input.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index 9b109793b90c..7e26ca16f6d1 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -1116,7 +1116,7 @@ var inputType = {
Model as number:
Min:
- Max:
+ Max:
value = {{value}}
myForm.range.$valid = {{myForm.range.$valid}}
myForm.range.$error = {{myForm.range.$error}}
@@ -1142,7 +1142,7 @@ var inputType = {
Model as number:
Min:
- Max:
+ Max:
value = {{value}}
myForm.range.$valid = {{myForm.range.$valid}}
myForm.range.$error = {{myForm.range.$error}}
From 9e24e774a558143b3478536911a3a4c1714564ba Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Wed, 14 Sep 2016 13:21:09 -0700
Subject: [PATCH 0007/1014] perf(form, ngModel): change controllers to use
prototype methods
This makes the largetable-bp ng-model benchmarks 10-15% faster (down 90-100ms for me). The actual controller instantiation doesn't change too much but the overall numbers seem consistently faster, I assume all due to reducing memory usage / gc. Specifically on creation there is ~40% less memory GCed, on destruction there is about ~25% less.
PR (#13286)
BREAKING CHANGE:
The use of prototype methods instead of new methods per instance removes the ability to pass
NgModelController and FormController methods without context.
For example
`$scope.$watch('something', myNgModelCtrl.$render)`
will no longer work because the `$render` method is passed without any context.
This must now be replaced with
```
$scope.$watch('something', function() {
myNgModelCtrl.$render();
})
```
or possibly by using `Function.prototype.bind` or `angular.bind`.
---
src/ng/directive/form.js | 322 +++++++++++++++--------
src/ng/directive/ngModel.js | 502 +++++++++++++++---------------------
2 files changed, 425 insertions(+), 399 deletions(-)
diff --git a/src/ng/directive/form.js b/src/ng/directive/form.js
index dac2ebd05e5d..4790080854a8 100644
--- a/src/ng/directive/form.js
+++ b/src/ng/directive/form.js
@@ -1,6 +1,6 @@
'use strict';
-/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true
+/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS
*/
var nullFormCtrl = {
$addControl: noop,
@@ -11,6 +11,7 @@ var nullFormCtrl = {
$setPristine: noop,
$setSubmitted: noop
},
+PENDING_CLASS = 'ng-pending',
SUBMITTED_CLASS = 'ng-submitted';
function nullFormRenameControl(control, name) {
@@ -61,22 +62,28 @@ function nullFormRenameControl(control, name) {
*/
//asks for $scope to fool the BC controller module
FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
-function FormController(element, attrs, $scope, $animate, $interpolate) {
- var form = this,
- controls = [];
+function FormController($element, $attrs, $scope, $animate, $interpolate) {
+ this.$$controls = [];
// init state
- form.$error = {};
- form.$$success = {};
- form.$pending = undefined;
- form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope);
- form.$dirty = false;
- form.$pristine = true;
- form.$valid = true;
- form.$invalid = false;
- form.$submitted = false;
- form.$$parentForm = nullFormCtrl;
+ this.$error = {};
+ this.$$success = {};
+ this.$pending = undefined;
+ this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$valid = true;
+ this.$invalid = false;
+ this.$submitted = false;
+ this.$$parentForm = nullFormCtrl;
+
+ this.$$element = $element;
+ this.$$animate = $animate;
+
+ setupValidity(this);
+}
+FormController.prototype = {
/**
* @ngdoc method
* @name form.FormController#$rollbackViewValue
@@ -88,11 +95,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* event defined in `ng-model-options`. This method is typically needed by the reset button of
* a form that uses `ng-model-options` to pend updates.
*/
- form.$rollbackViewValue = function() {
- forEach(controls, function(control) {
+ $rollbackViewValue: function() {
+ forEach(this.$$controls, function(control) {
control.$rollbackViewValue();
});
- };
+ },
/**
* @ngdoc method
@@ -105,11 +112,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* event defined in `ng-model-options`. This method is rarely needed as `NgModelController`
* usually handles calling this in response to input events.
*/
- form.$commitViewValue = function() {
- forEach(controls, function(control) {
+ $commitViewValue: function() {
+ forEach(this.$$controls, function(control) {
control.$commitViewValue();
});
- };
+ },
/**
* @ngdoc method
@@ -132,29 +139,29 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* For example, if an input control is added that is already `$dirty` and has `$error` properties,
* calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form.
*/
- form.$addControl = function(control) {
+ $addControl: function(control) {
// Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
// and not added to the scope. Now we throw an error.
assertNotHasOwnProperty(control.$name, 'input');
- controls.push(control);
+ this.$$controls.push(control);
if (control.$name) {
- form[control.$name] = control;
+ this[control.$name] = control;
}
- control.$$parentForm = form;
- };
+ control.$$parentForm = this;
+ },
// Private API: rename a form control
- form.$$renameControl = function(control, newName) {
+ $$renameControl: function(control, newName) {
var oldName = control.$name;
- if (form[oldName] === control) {
- delete form[oldName];
+ if (this[oldName] === control) {
+ delete this[oldName];
}
- form[newName] = control;
+ this[newName] = control;
control.$name = newName;
- };
+ },
/**
* @ngdoc method
@@ -172,60 +179,26 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* different from case to case. For example, removing the only `$dirty` control from a form may or
* may not mean that the form is still `$dirty`.
*/
- form.$removeControl = function(control) {
- if (control.$name && form[control.$name] === control) {
- delete form[control.$name];
+ $removeControl: function(control) {
+ if (control.$name && this[control.$name] === control) {
+ delete this[control.$name];
}
- forEach(form.$pending, function(value, name) {
- form.$setValidity(name, null, control);
- });
- forEach(form.$error, function(value, name) {
- form.$setValidity(name, null, control);
- });
- forEach(form.$$success, function(value, name) {
- form.$setValidity(name, null, control);
- });
-
- arrayRemove(controls, control);
+ forEach(this.$pending, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$error, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$$success, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+
+ arrayRemove(this.$$controls, control);
control.$$parentForm = nullFormCtrl;
- };
-
-
- /**
- * @ngdoc method
- * @name form.FormController#$setValidity
- *
- * @description
- * Sets the validity of a form control.
- *
- * This method will also propagate to parent forms.
- */
- addSetValidityMethod({
- ctrl: this,
- $element: element,
- set: function(object, property, controller) {
- var list = object[property];
- if (!list) {
- object[property] = [controller];
- } else {
- var index = list.indexOf(controller);
- if (index === -1) {
- list.push(controller);
- }
- }
- },
- unset: function(object, property, controller) {
- var list = object[property];
- if (!list) {
- return;
- }
- arrayRemove(list, controller);
- if (list.length === 0) {
- delete object[property];
- }
- },
- $animate: $animate
- });
+ },
/**
* @ngdoc method
@@ -237,13 +210,13 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* This method can be called to add the 'ng-dirty' class and set the form to a dirty
* state (ng-dirty class). This method will also propagate to parent forms.
*/
- form.$setDirty = function() {
- $animate.removeClass(element, PRISTINE_CLASS);
- $animate.addClass(element, DIRTY_CLASS);
- form.$dirty = true;
- form.$pristine = false;
- form.$$parentForm.$setDirty();
- };
+ $setDirty: function() {
+ this.$$animate.removeClass(this.$$element, PRISTINE_CLASS);
+ this.$$animate.addClass(this.$$element, DIRTY_CLASS);
+ this.$dirty = true;
+ this.$pristine = false;
+ this.$$parentForm.$setDirty();
+ },
/**
* @ngdoc method
@@ -261,15 +234,15 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* Setting a form back to a pristine state is often useful when we want to 'reuse' a form after
* saving or resetting it.
*/
- form.$setPristine = function() {
- $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
- form.$dirty = false;
- form.$pristine = true;
- form.$submitted = false;
- forEach(controls, function(control) {
+ $setPristine: function() {
+ this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$submitted = false;
+ forEach(this.$$controls, function(control) {
control.$setPristine();
});
- };
+ },
/**
* @ngdoc method
@@ -284,11 +257,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* Setting a form controls back to their untouched state is often useful when setting the form
* back to its pristine state.
*/
- form.$setUntouched = function() {
- forEach(controls, function(control) {
+ $setUntouched: function() {
+ forEach(this.$$controls, function(control) {
control.$setUntouched();
});
- };
+ },
/**
* @ngdoc method
@@ -297,12 +270,46 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* @description
* Sets the form to its submitted state.
*/
- form.$setSubmitted = function() {
- $animate.addClass(element, SUBMITTED_CLASS);
- form.$submitted = true;
- form.$$parentForm.$setSubmitted();
- };
-}
+ $setSubmitted: function() {
+ this.$$animate.addClass(this.$$element, SUBMITTED_CLASS);
+ this.$submitted = true;
+ this.$$parentForm.$setSubmitted();
+ }
+};
+
+/**
+ * @ngdoc method
+ * @name form.FormController#$setValidity
+ *
+ * @description
+ * Sets the validity of a form control.
+ *
+ * This method will also propagate to parent forms.
+ */
+addSetValidityMethod({
+ clazz: FormController,
+ set: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ object[property] = [controller];
+ } else {
+ var index = list.indexOf(controller);
+ if (index === -1) {
+ list.push(controller);
+ }
+ }
+ },
+ unset: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ return;
+ }
+ arrayRemove(list, controller);
+ if (list.length === 0) {
+ delete object[property];
+ }
+ }
+});
/**
* @ngdoc directive
@@ -549,3 +556,108 @@ var formDirectiveFactory = function(isNgForm) {
var formDirective = formDirectiveFactory();
var ngFormDirective = formDirectiveFactory(true);
+
+
+
+// helper methods
+function setupValidity(instance) {
+ instance.$$classCache = {};
+ instance.$$classCache[INVALID_CLASS] = !(instance.$$classCache[VALID_CLASS] = instance.$$element.hasClass(VALID_CLASS));
+}
+function addSetValidityMethod(context) {
+ var clazz = context.clazz,
+ set = context.set,
+ unset = context.unset;
+
+ clazz.prototype.$setValidity = function(validationErrorKey, state, controller) {
+ if (isUndefined(state)) {
+ createAndSet(this, '$pending', validationErrorKey, controller);
+ } else {
+ unsetAndCleanup(this, '$pending', validationErrorKey, controller);
+ }
+ if (!isBoolean(state)) {
+ unset(this.$error, validationErrorKey, controller);
+ unset(this.$$success, validationErrorKey, controller);
+ } else {
+ if (state) {
+ unset(this.$error, validationErrorKey, controller);
+ set(this.$$success, validationErrorKey, controller);
+ } else {
+ set(this.$error, validationErrorKey, controller);
+ unset(this.$$success, validationErrorKey, controller);
+ }
+ }
+ if (this.$pending) {
+ cachedToggleClass(this, PENDING_CLASS, true);
+ this.$valid = this.$invalid = undefined;
+ toggleValidationCss(this, '', null);
+ } else {
+ cachedToggleClass(this, PENDING_CLASS, false);
+ this.$valid = isObjectEmpty(this.$error);
+ this.$invalid = !this.$valid;
+ toggleValidationCss(this, '', this.$valid);
+ }
+
+ // re-read the state as the set/unset methods could have
+ // combined state in this.$error[validationError] (used for forms),
+ // where setting/unsetting only increments/decrements the value,
+ // and does not replace it.
+ var combinedState;
+ if (this.$pending && this.$pending[validationErrorKey]) {
+ combinedState = undefined;
+ } else if (this.$error[validationErrorKey]) {
+ combinedState = false;
+ } else if (this.$$success[validationErrorKey]) {
+ combinedState = true;
+ } else {
+ combinedState = null;
+ }
+
+ toggleValidationCss(this, validationErrorKey, combinedState);
+ this.$$parentForm.$setValidity(validationErrorKey, combinedState, this);
+ };
+
+ function createAndSet(ctrl, name, value, controller) {
+ if (!ctrl[name]) {
+ ctrl[name] = {};
+ }
+ set(ctrl[name], value, controller);
+ }
+
+ function unsetAndCleanup(ctrl, name, value, controller) {
+ if (ctrl[name]) {
+ unset(ctrl[name], value, controller);
+ }
+ if (isObjectEmpty(ctrl[name])) {
+ ctrl[name] = undefined;
+ }
+ }
+
+ function cachedToggleClass(ctrl, className, switchValue) {
+ if (switchValue && !ctrl.$$classCache[className]) {
+ ctrl.$$animate.addClass(ctrl.$$element, className);
+ ctrl.$$classCache[className] = true;
+ } else if (!switchValue && ctrl.$$classCache[className]) {
+ ctrl.$$animate.removeClass(ctrl.$$element, className);
+ ctrl.$$classCache[className] = false;
+ }
+ }
+
+ function toggleValidationCss(ctrl, validationErrorKey, isValid) {
+ validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
+
+ cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true);
+ cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false);
+ }
+}
+
+function isObjectEmpty(obj) {
+ if (obj) {
+ for (var prop in obj) {
+ if (obj.hasOwnProperty(prop)) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js
index 006cab47aa6a..55310adceddd 100644
--- a/src/ng/directive/ngModel.js
+++ b/src/ng/directive/ngModel.js
@@ -6,7 +6,10 @@
DIRTY_CLASS: true,
UNTOUCHED_CLASS: true,
TOUCHED_CLASS: true,
- $ModelOptionsProvider: true
+ PENDING_CLASS: true,
+ $ModelOptionsProvider: true,
+ addSetValidityMethod: true,
+ setupValidity: true
*/
var VALID_CLASS = 'ng-valid',
@@ -15,7 +18,6 @@ var VALID_CLASS = 'ng-valid',
DIRTY_CLASS = 'ng-dirty',
UNTOUCHED_CLASS = 'ng-untouched',
TOUCHED_CLASS = 'ng-touched',
- PENDING_CLASS = 'ng-pending',
EMPTY_CLASS = 'ng-empty',
NOT_EMPTY_CLASS = 'ng-not-empty';
@@ -221,8 +223,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
*
*
*/
-var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', '$modelOptions',
- /** @this */ function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate, $modelOptions) {
+NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate', '$modelOptions'];
+function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate, $modelOptions) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
@@ -244,40 +246,53 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
this.$$parentForm = nullFormCtrl;
this.$options = $modelOptions;
- var parsedNgModel = $parse($attr.ngModel),
- parsedNgModelAssign = parsedNgModel.assign,
- ngModelGet = parsedNgModel,
- ngModelSet = parsedNgModelAssign,
- pendingDebounce = null,
- parserValid,
- ctrl = this;
-
-
- this.$$initGetterSetters = function() {
+ this.$$parsedNgModel = $parse($attr.ngModel);
+ this.$$parsedNgModelAssign = this.$$parsedNgModel.assign;
+ this.$$ngModelGet = this.$$parsedNgModel;
+ this.$$ngModelSet = this.$$parsedNgModelAssign;
+ this.$$pendingDebounce = null;
+ this.$$parserValid = undefined;
+
+ this.$$currentValidationRunId = 0;
+
+ this.$$scope = $scope;
+ this.$$attr = $attr;
+ this.$$element = $element;
+ this.$$animate = $animate;
+ this.$$timeout = $timeout;
+ this.$$parse = $parse;
+ this.$$q = $q;
+ this.$$exceptionHandler = $exceptionHandler;
+
+ setupValidity(this);
+ setupModelWatcher(this);
+}
- if (ctrl.$options.getOption('getterSetter')) {
- var invokeModelGetter = $parse($attr.ngModel + '()'),
- invokeModelSetter = $parse($attr.ngModel + '($$$p)');
+NgModelController.prototype = {
+ $$initGetterSetters: function() {
+ if (this.$options.getOption('getterSetter')) {
+ var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'),
+ invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)');
- ngModelGet = function($scope) {
- var modelValue = parsedNgModel($scope);
+ this.$$ngModelGet = function($scope) {
+ var modelValue = this.$$parsedNgModel($scope);
if (isFunction(modelValue)) {
modelValue = invokeModelGetter($scope);
}
return modelValue;
};
- ngModelSet = function($scope, newValue) {
- if (isFunction(parsedNgModel($scope))) {
+ this.$$ngModelSet = function($scope, newValue) {
+ if (isFunction(this.$$parsedNgModel($scope))) {
invokeModelSetter($scope, {$$$p: newValue});
} else {
- parsedNgModelAssign($scope, newValue);
+ this.$$parsedNgModelAssign($scope, newValue);
}
};
- } else if (!parsedNgModel.assign) {
+ } else if (!this.$$parsedNgModel.assign) {
throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}',
- $attr.ngModel, startingTag($element));
+ this.$$attr.ngModel, startingTag(this.$$element));
}
- };
+ },
/**
@@ -300,7 +315,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* or `$viewValue` are objects (rather than a string or number) then `$render()` will not be
* invoked if you only change a property on the objects.
*/
- this.$render = noop;
+ $render: noop,
/**
* @ngdoc method
@@ -320,57 +335,20 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* @param {*} value The value of the input to check for emptiness.
* @returns {boolean} True if `value` is "empty".
*/
- this.$isEmpty = function(value) {
+ $isEmpty: function(value) {
// eslint-disable-next-line no-self-compare
return isUndefined(value) || value === '' || value === null || value !== value;
- };
+ },
- this.$$updateEmptyClasses = function(value) {
- if (ctrl.$isEmpty(value)) {
- $animate.removeClass($element, NOT_EMPTY_CLASS);
- $animate.addClass($element, EMPTY_CLASS);
+ $$updateEmptyClasses: function(value) {
+ if (this.$isEmpty(value)) {
+ this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS);
+ this.$$animate.addClass(this.$$element, EMPTY_CLASS);
} else {
- $animate.removeClass($element, EMPTY_CLASS);
- $animate.addClass($element, NOT_EMPTY_CLASS);
+ this.$$animate.removeClass(this.$$element, EMPTY_CLASS);
+ this.$$animate.addClass(this.$$element, NOT_EMPTY_CLASS);
}
- };
-
-
- var currentValidationRunId = 0;
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setValidity
- *
- * @description
- * Change the validity state, and notify the form.
- *
- * This method can be called within $parsers/$formatters or a custom validation implementation.
- * However, in most cases it should be sufficient to use the `ngModel.$validators` and
- * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
- *
- * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
- * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
- * (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
- * The `validationErrorKey` should be in camelCase and will get converted into dash-case
- * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
- * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
- * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
- * or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
- * Skipped is used by Angular when validators do not run because of parse errors and
- * when `$asyncValidators` do not run because any of the `$validators` failed.
- */
- addSetValidityMethod({
- ctrl: this,
- $element: $element,
- set: function(object, property) {
- object[property] = true;
- },
- unset: function(object, property) {
- delete object[property];
- },
- $animate: $animate
- });
+ },
/**
* @ngdoc method
@@ -383,12 +361,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* state (`ng-pristine` class). A model is considered to be pristine when the control
* has not been changed from when first compiled.
*/
- this.$setPristine = function() {
- ctrl.$dirty = false;
- ctrl.$pristine = true;
- $animate.removeClass($element, DIRTY_CLASS);
- $animate.addClass($element, PRISTINE_CLASS);
- };
+ $setPristine: function() {
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$$animate.removeClass(this.$$element, DIRTY_CLASS);
+ this.$$animate.addClass(this.$$element, PRISTINE_CLASS);
+ },
/**
* @ngdoc method
@@ -401,13 +379,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* state (`ng-dirty` class). A model is considered to be dirty when the control has been changed
* from when first compiled.
*/
- this.$setDirty = function() {
- ctrl.$dirty = true;
- ctrl.$pristine = false;
- $animate.removeClass($element, PRISTINE_CLASS);
- $animate.addClass($element, DIRTY_CLASS);
- ctrl.$$parentForm.$setDirty();
- };
+ $setDirty: function() {
+ this.$dirty = true;
+ this.$pristine = false;
+ this.$$animate.removeClass(this.$$element, PRISTINE_CLASS);
+ this.$$animate.addClass(this.$$element, DIRTY_CLASS);
+ this.$$parentForm.$setDirty();
+ },
/**
* @ngdoc method
@@ -421,11 +399,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* by default, however this function can be used to restore that state if the model has
* already been touched by the user.
*/
- this.$setUntouched = function() {
- ctrl.$touched = false;
- ctrl.$untouched = true;
- $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS);
- };
+ $setUntouched: function() {
+ this.$touched = false;
+ this.$untouched = true;
+ this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS);
+ },
/**
* @ngdoc method
@@ -438,11 +416,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* touched state (`ng-touched` class). A model is considered to be touched when the user has
* first focused the control element and then shifted focus away from the control (blur event).
*/
- this.$setTouched = function() {
- ctrl.$touched = true;
- ctrl.$untouched = false;
- $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS);
- };
+ $setTouched: function() {
+ this.$touched = true;
+ this.$untouched = false;
+ this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS);
+ },
/**
* @ngdoc method
@@ -532,11 +510,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
*
*/
- this.$rollbackViewValue = function() {
- $timeout.cancel(pendingDebounce);
- ctrl.$viewValue = ctrl.$$lastCommittedViewValue;
- ctrl.$render();
- };
+ $rollbackViewValue: function() {
+ this.$$timeout.cancel(this.$$pendingDebounce);
+ this.$viewValue = this.$$lastCommittedViewValue;
+ this.$render();
+ },
/**
* @ngdoc method
@@ -550,45 +528,46 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* If the validity changes to valid, it will set the model to the last available valid
* `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
*/
- this.$validate = function() {
+ $validate: function() {
// ignore $validate before model is initialized
- if (isNumberNaN(ctrl.$modelValue)) {
+ if (isNumberNaN(this.$modelValue)) {
return;
}
- var viewValue = ctrl.$$lastCommittedViewValue;
+ var viewValue = this.$$lastCommittedViewValue;
// Note: we use the $$rawModelValue as $modelValue might have been
// set to undefined during a view -> model update that found validation
// errors. We can't parse the view here, since that could change
// the model although neither viewValue nor the model on the scope changed
- var modelValue = ctrl.$$rawModelValue;
+ var modelValue = this.$$rawModelValue;
- var prevValid = ctrl.$valid;
- var prevModelValue = ctrl.$modelValue;
+ var prevValid = this.$valid;
+ var prevModelValue = this.$modelValue;
- var allowInvalid = ctrl.$options.getOption('allowInvalid');
+ var allowInvalid = this.$options.getOption('allowInvalid');
- ctrl.$$runValidators(modelValue, viewValue, function(allValid) {
+ var that = this;
+ this.$$runValidators(modelValue, viewValue, function(allValid) {
// If there was no change in validity, don't update the model
// This prevents changing an invalid modelValue to undefined
if (!allowInvalid && prevValid !== allValid) {
- // Note: Don't check ctrl.$valid here, as we could have
+ // Note: Don't check this.$valid here, as we could have
// external validators (e.g. calculated on the server),
// that just call $setValidity and need the model value
// to calculate their validity.
- ctrl.$modelValue = allValid ? modelValue : undefined;
+ that.$modelValue = allValid ? modelValue : undefined;
- if (ctrl.$modelValue !== prevModelValue) {
- ctrl.$$writeModelToScope();
+ if (that.$modelValue !== prevModelValue) {
+ that.$$writeModelToScope();
}
}
});
+ },
- };
-
- this.$$runValidators = function(modelValue, viewValue, doneCallback) {
- currentValidationRunId++;
- var localValidationRunId = currentValidationRunId;
+ $$runValidators: function(modelValue, viewValue, doneCallback) {
+ this.$$currentValidationRunId++;
+ var localValidationRunId = this.$$currentValidationRunId;
+ var that = this;
// check parser error
if (!processParseErrors()) {
@@ -602,34 +581,34 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
processAsyncValidators();
function processParseErrors() {
- var errorKey = ctrl.$$parserName || 'parse';
- if (isUndefined(parserValid)) {
+ var errorKey = that.$$parserName || 'parse';
+ if (isUndefined(that.$$parserValid)) {
setValidity(errorKey, null);
} else {
- if (!parserValid) {
- forEach(ctrl.$validators, function(v, name) {
+ if (!that.$$parserValid) {
+ forEach(that.$validators, function(v, name) {
setValidity(name, null);
});
- forEach(ctrl.$asyncValidators, function(v, name) {
+ forEach(that.$asyncValidators, function(v, name) {
setValidity(name, null);
});
}
// Set the parse error last, to prevent unsetting it, should a $validators key == parserName
- setValidity(errorKey, parserValid);
- return parserValid;
+ setValidity(errorKey, that.$$parserValid);
+ return that.$$parserValid;
}
return true;
}
function processSyncValidators() {
var syncValidatorsValid = true;
- forEach(ctrl.$validators, function(validator, name) {
+ forEach(that.$validators, function(validator, name) {
var result = validator(modelValue, viewValue);
syncValidatorsValid = syncValidatorsValid && result;
setValidity(name, result);
});
if (!syncValidatorsValid) {
- forEach(ctrl.$asyncValidators, function(v, name) {
+ forEach(that.$asyncValidators, function(v, name) {
setValidity(name, null);
});
return false;
@@ -640,7 +619,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
function processAsyncValidators() {
var validatorPromises = [];
var allValid = true;
- forEach(ctrl.$asyncValidators, function(validator, name) {
+ forEach(that.$asyncValidators, function(validator, name) {
var promise = validator(modelValue, viewValue);
if (!isPromiseLike(promise)) {
throw ngModelMinErr('nopromise',
@@ -657,25 +636,25 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
if (!validatorPromises.length) {
validationDone(true);
} else {
- $q.all(validatorPromises).then(function() {
+ that.$$q.all(validatorPromises).then(function() {
validationDone(allValid);
}, noop);
}
}
function setValidity(name, isValid) {
- if (localValidationRunId === currentValidationRunId) {
- ctrl.$setValidity(name, isValid);
+ if (localValidationRunId === that.$$currentValidationRunId) {
+ that.$setValidity(name, isValid);
}
}
function validationDone(allValid) {
- if (localValidationRunId === currentValidationRunId) {
+ if (localValidationRunId === that.$$currentValidationRunId) {
doneCallback(allValid);
}
}
- };
+ },
/**
* @ngdoc method
@@ -688,84 +667,87 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* event defined in `ng-model-options`. this method is rarely needed as `NgModelController`
* usually handles calling this in response to input events.
*/
- this.$commitViewValue = function() {
- var viewValue = ctrl.$viewValue;
+ $commitViewValue: function() {
+ var viewValue = this.$viewValue;
- $timeout.cancel(pendingDebounce);
+ this.$$timeout.cancel(this.$$pendingDebounce);
// If the view value has not changed then we should just exit, except in the case where there is
// a native validator on the element. In this case the validation state may have changed even though
// the viewValue has stayed empty.
- if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
+ if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) {
return;
}
- ctrl.$$updateEmptyClasses(viewValue);
- ctrl.$$lastCommittedViewValue = viewValue;
+ this.$$updateEmptyClasses(viewValue);
+ this.$$lastCommittedViewValue = viewValue;
// change to dirty
- if (ctrl.$pristine) {
+ if (this.$pristine) {
this.$setDirty();
}
this.$$parseAndValidate();
- };
+ },
- this.$$parseAndValidate = function() {
- var viewValue = ctrl.$$lastCommittedViewValue;
+ $$parseAndValidate: function() {
+ var viewValue = this.$$lastCommittedViewValue;
var modelValue = viewValue;
- parserValid = isUndefined(modelValue) ? undefined : true;
+ var that = this;
+
+ this.$$parserValid = isUndefined(modelValue) ? undefined : true;
- if (parserValid) {
- for (var i = 0; i < ctrl.$parsers.length; i++) {
- modelValue = ctrl.$parsers[i](modelValue);
+ if (this.$$parserValid) {
+ for (var i = 0; i < this.$parsers.length; i++) {
+ modelValue = this.$parsers[i](modelValue);
if (isUndefined(modelValue)) {
- parserValid = false;
+ this.$$parserValid = false;
break;
}
}
}
- if (isNumberNaN(ctrl.$modelValue)) {
- // ctrl.$modelValue has not been touched yet...
- ctrl.$modelValue = ngModelGet($scope);
+ if (isNumberNaN(this.$modelValue)) {
+ // this.$modelValue has not been touched yet...
+ this.$modelValue = this.$$ngModelGet(this.$$scope);
}
- var prevModelValue = ctrl.$modelValue;
- var allowInvalid = ctrl.$options.getOption('allowInvalid');
- ctrl.$$rawModelValue = modelValue;
+ var prevModelValue = this.$modelValue;
+ var allowInvalid = this.$options.getOption('allowInvalid');
+ this.$$rawModelValue = modelValue;
if (allowInvalid) {
- ctrl.$modelValue = modelValue;
+ this.$modelValue = modelValue;
writeToModelIfNeeded();
}
// Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date.
// This can happen if e.g. $setViewValue is called from inside a parser
- ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) {
+ this.$$runValidators(modelValue, this.$$lastCommittedViewValue, function(allValid) {
if (!allowInvalid) {
- // Note: Don't check ctrl.$valid here, as we could have
+ // Note: Don't check this.$valid here, as we could have
// external validators (e.g. calculated on the server),
// that just call $setValidity and need the model value
// to calculate their validity.
- ctrl.$modelValue = allValid ? modelValue : undefined;
+ that.$modelValue = allValid ? modelValue : undefined;
writeToModelIfNeeded();
}
});
function writeToModelIfNeeded() {
- if (ctrl.$modelValue !== prevModelValue) {
- ctrl.$$writeModelToScope();
+ if (that.$modelValue !== prevModelValue) {
+ that.$$writeModelToScope();
}
}
- };
+ },
- this.$$writeModelToScope = function() {
- ngModelSet($scope, ctrl.$modelValue);
- forEach(ctrl.$viewChangeListeners, function(listener) {
+ $$writeModelToScope: function() {
+ this.$$ngModelSet(this.$$scope, this.$modelValue);
+ forEach(this.$viewChangeListeners, function(listener) {
try {
listener();
} catch (e) {
- $exceptionHandler(e);
+ // eslint-disable-next-line no-invalid-this
+ this.$$exceptionHandler(e);
}
- });
- };
+ }, this);
+ },
/**
* @ngdoc method
@@ -817,16 +799,15 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* @param {*} value value from the view.
* @param {string} trigger Event that triggered the update.
*/
- this.$setViewValue = function(value, trigger) {
- ctrl.$viewValue = value;
- if (ctrl.$options.getOption('updateOnDefault')) {
- ctrl.$$debounceViewValueCommit(trigger);
+ $setViewValue: function(value, trigger) {
+ this.$viewValue = value;
+ if (this.$options.getOption('updateOnDefault')) {
+ this.$$debounceViewValueCommit(trigger);
}
- };
+ },
- this.$$debounceViewValueCommit = function(trigger) {
- var options = ctrl.$options,
- debounceDelay = options.getOption('debounce');
+ $$debounceViewValueCommit: function(trigger) {
+ var debounceDelay = this.$options.getOption('debounce');
if (isNumber(debounceDelay[trigger])) {
debounceDelay = debounceDelay[trigger];
@@ -834,20 +815,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
debounceDelay = debounceDelay['default'];
}
- $timeout.cancel(pendingDebounce);
+ this.$$timeout.cancel(this.$$pendingDebounce);
+ var that = this;
if (debounceDelay) {
- pendingDebounce = $timeout(function() {
- ctrl.$commitViewValue();
+ this.$$pendingDebounce = this.$$timeout(function() {
+ that.$commitViewValue();
}, debounceDelay);
- } else if ($rootScope.$$phase) {
- ctrl.$commitViewValue();
+ } else if (this.$$scope.$root.$$phase) {
+ this.$commitViewValue();
} else {
- $scope.$apply(function() {
- ctrl.$commitViewValue();
+ this.$$scope.$apply(function() {
+ that.$commitViewValue();
});
}
- };
+ }
+};
+function setupModelWatcher(ctrl) {
// model -> value
// Note: we cannot use a normal scope.$watch as we want to detect the following:
// 1. scope value is 'a'
@@ -856,8 +840,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
// -> scope value did not change since the last digest as
// ng-change executes in apply phase
// 4. view should be changed back to 'a'
- $scope.$watch(function ngModelWatch() {
- var modelValue = ngModelGet($scope);
+ ctrl.$$scope.$watch(function ngModelWatch() {
+ var modelValue = ctrl.$$ngModelGet(ctrl.$$scope);
// if scope model value and ngModel value are out of sync
// TODO(perf): why not move this to the action fn?
@@ -867,7 +851,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
(ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
) {
ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
- parserValid = undefined;
+ ctrl.$$parserValid = undefined;
var formatters = ctrl.$formatters,
idx = formatters.length;
@@ -888,7 +872,39 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
return modelValue;
});
-}];
+}
+
+/**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setValidity
+ *
+ * @description
+ * Change the validity state, and notify the form.
+ *
+ * This method can be called within $parsers/$formatters or a custom validation implementation.
+ * However, in most cases it should be sufficient to use the `ngModel.$validators` and
+ * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
+ *
+ * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
+ * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
+ * (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
+ * The `validationErrorKey` should be in camelCase and will get converted into dash-case
+ * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
+ * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
+ * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
+ * or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
+ * Skipped is used by Angular when validators do not run because of parse errors and
+ * when `$asyncValidators` do not run because any of the `$validators` failed.
+ */
+addSetValidityMethod({
+ clazz: NgModelController,
+ set: function(object, property) {
+ object[property] = true;
+ },
+ unset: function(object, property) {
+ delete object[property];
+ }
+});
/**
@@ -1124,13 +1140,17 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
});
}
+ function setTouched() {
+ modelCtrl.$setTouched();
+ }
+
element.on('blur', function() {
if (modelCtrl.$touched) return;
if ($rootScope.$$phase) {
- scope.$evalAsync(modelCtrl.$setTouched);
+ scope.$evalAsync(setTouched);
} else {
- scope.$apply(modelCtrl.$setTouched);
+ scope.$apply(setTouched);
}
});
}
@@ -1465,109 +1485,3 @@ function ModelOptions(options, parentOptions) {
return new ModelOptions(options, _options);
};
}
-
-// helper methods
-function addSetValidityMethod(context) {
- var ctrl = context.ctrl,
- $element = context.$element,
- classCache = {},
- set = context.set,
- unset = context.unset,
- $animate = context.$animate;
-
- classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
-
- ctrl.$setValidity = setValidity;
-
- function setValidity(validationErrorKey, state, controller) {
- if (isUndefined(state)) {
- createAndSet('$pending', validationErrorKey, controller);
- } else {
- unsetAndCleanup('$pending', validationErrorKey, controller);
- }
- if (!isBoolean(state)) {
- unset(ctrl.$error, validationErrorKey, controller);
- unset(ctrl.$$success, validationErrorKey, controller);
- } else {
- if (state) {
- unset(ctrl.$error, validationErrorKey, controller);
- set(ctrl.$$success, validationErrorKey, controller);
- } else {
- set(ctrl.$error, validationErrorKey, controller);
- unset(ctrl.$$success, validationErrorKey, controller);
- }
- }
- if (ctrl.$pending) {
- cachedToggleClass(PENDING_CLASS, true);
- ctrl.$valid = ctrl.$invalid = undefined;
- toggleValidationCss('', null);
- } else {
- cachedToggleClass(PENDING_CLASS, false);
- ctrl.$valid = isObjectEmpty(ctrl.$error);
- ctrl.$invalid = !ctrl.$valid;
- toggleValidationCss('', ctrl.$valid);
- }
-
- // re-read the state as the set/unset methods could have
- // combined state in ctrl.$error[validationError] (used for forms),
- // where setting/unsetting only increments/decrements the value,
- // and does not replace it.
- var combinedState;
- if (ctrl.$pending && ctrl.$pending[validationErrorKey]) {
- combinedState = undefined;
- } else if (ctrl.$error[validationErrorKey]) {
- combinedState = false;
- } else if (ctrl.$$success[validationErrorKey]) {
- combinedState = true;
- } else {
- combinedState = null;
- }
-
- toggleValidationCss(validationErrorKey, combinedState);
- ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
- }
-
- function createAndSet(name, value, controller) {
- if (!ctrl[name]) {
- ctrl[name] = {};
- }
- set(ctrl[name], value, controller);
- }
-
- function unsetAndCleanup(name, value, controller) {
- if (ctrl[name]) {
- unset(ctrl[name], value, controller);
- }
- if (isObjectEmpty(ctrl[name])) {
- ctrl[name] = undefined;
- }
- }
-
- function cachedToggleClass(className, switchValue) {
- if (switchValue && !classCache[className]) {
- $animate.addClass($element, className);
- classCache[className] = true;
- } else if (!switchValue && classCache[className]) {
- $animate.removeClass($element, className);
- classCache[className] = false;
- }
- }
-
- function toggleValidationCss(validationErrorKey, isValid) {
- validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
-
- cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true);
- cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false);
- }
-}
-
-function isObjectEmpty(obj) {
- if (obj) {
- for (var prop in obj) {
- if (obj.hasOwnProperty(prop)) {
- return false;
- }
- }
- }
- return true;
-}
From 76d3dafdeaf2f343d094b5a34ffb74adf64bb284 Mon Sep 17 00:00:00 2001
From: Prashant Singh Pawar
Date: Thu, 15 Sep 2016 06:10:27 -0400
Subject: [PATCH 0008/1014] fix($compile): don't throw tplrt error when there
is a whitespace around a top-level comment
Added new conditional for NODE_TYPE_TEXT inside removeComments method of $compile
Added corresponding unit tests.
Closes #15108
PR (#15132)
---
src/ng/compile.js | 5 +++--
test/ng/compileSpec.js | 48 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 51 insertions(+), 2 deletions(-)
diff --git a/src/ng/compile.js b/src/ng/compile.js
index 8741dc4d074a..631ddc67cedd 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -3679,8 +3679,9 @@ function removeComments(jqNodes) {
while (i--) {
var node = jqNodes[i];
- if (node.nodeType === NODE_TYPE_COMMENT) {
- splice.call(jqNodes, i, 1);
+ if (node.nodeType === NODE_TYPE_COMMENT ||
+ (node.nodeType === NODE_TYPE_TEXT && node.nodeValue.trim() === '')) {
+ splice.call(jqNodes, i, 1);
}
}
return jqNodes;
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index e4a8ddf4e960..70112d14a82f 100755
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -1364,6 +1364,22 @@ describe('$compile', function() {
});
});
+ it('should ignore whitespace betwee comment and root node when replacing with a template', function() {
+ module(function() {
+ directive('replaceWithWhitespace', valueFn({
+ replace: true,
+ template: ' Hello, world!
'
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ expect(function() {
+ element = $compile('')($rootScope);
+ }).not.toThrow();
+ expect(element.find('p').length).toBe(1);
+ expect(element.find('p').text()).toBe('Hello, world!');
+ });
+ });
+
it('should keep prototype properties on directive', function() {
module(function() {
function DirectiveClass() {
@@ -2092,6 +2108,18 @@ describe('$compile', function() {
$compile('
');
$rootScope.$apply();
expect($exceptionHandler.errors).toEqual([]);
+
+ // comments are ok
+ $templateCache.put('template.html', '
\n');
+ $compile('
');
+ $rootScope.$apply();
+ expect($exceptionHandler.errors).toEqual([]);
+
+ // white space around comments is ok
+ $templateCache.put('template.html', '
\n');
+ $compile('
');
+ $rootScope.$apply();
+ expect($exceptionHandler.errors).toEqual([]);
});
});
@@ -2303,6 +2331,26 @@ describe('$compile', function() {
});
});
+ it('should ignore whitespace between comment and root node when replacing with a templateUrl', function() {
+ module(function() {
+ directive('replaceWithWhitespace', valueFn({
+ replace: true,
+ templateUrl: 'templateWithWhitespace.html'
+ }));
+ });
+ inject(function($compile, $rootScope, $httpBackend) {
+ $httpBackend.whenGET('templateWithWhitespace.html').
+ respond(' Hello, world!
');
+ expect(function() {
+ element = $compile('')($rootScope);
+ }).not.toThrow();
+ $httpBackend.flush();
+ expect(element.find('p').length).toBe(1);
+ expect(element.find('p').text()).toBe('Hello, world!');
+ });
+ });
+
+
it('should keep prototype properties on sync version of async directive', function() {
module(function() {
function DirectiveClass() {
From 1547c751aa48efe7dbefef701c3df5983b04aa2e Mon Sep 17 00:00:00 2001
From: Peter Bacon Darwin
Date: Tue, 6 Sep 2016 14:33:23 +0100
Subject: [PATCH 0009/1014] refactor($parse): remove Angular expression sandbox
The angular expression parser (`$parse`) attempts to sandbox expressions
to prevent unrestricted access to the global context.
While the sandbox was not on the frontline of the security defense,
developers kept relying upon it as a security feature even though it was
always possible to access arbitrary JavaScript code if a malicious user
could control the content of Angular templates in applications.
This commit removes this sandbox, which has the following benefits:
* it sends a clear message to developers that they should not rely on
the sandbox to prevent XSS attacks; that they must prevent control of
expression and templates instead.
* it allows performance and size improvements in the core Angular 1
library.
* it simplifies maintenance and provides opportunities to make the
parser more capable.
Please see the [Sandbox Removal Blog Post](http://angularjs.blogspot.com/2016/09/angular-16-expression-sandbox-removal.html)
for more detail on what you should do to ensure that your application is
secure.
Closes #15094
---
docs/content/error/$parse/isecaf.ngdoc | 12 -
docs/content/error/$parse/isecdom.ngdoc | 47 --
docs/content/error/$parse/isecff.ngdoc | 17 -
docs/content/error/$parse/isecfld.ngdoc | 27 -
docs/content/error/$parse/isecfn.ngdoc | 10 -
docs/content/error/$parse/isecobj.ngdoc | 11 -
docs/content/error/$parse/isecwindow.ngdoc | 45 --
docs/content/guide/expression.ngdoc | 6 +-
docs/content/guide/security.ngdoc | 73 +-
src/ng/parse.js | 264 +------
test/ng/directive/ngEventDirsSpec.js | 12 +-
test/ng/parseSpec.js | 764 ---------------------
12 files changed, 66 insertions(+), 1222 deletions(-)
delete mode 100644 docs/content/error/$parse/isecaf.ngdoc
delete mode 100644 docs/content/error/$parse/isecdom.ngdoc
delete mode 100644 docs/content/error/$parse/isecff.ngdoc
delete mode 100644 docs/content/error/$parse/isecfld.ngdoc
delete mode 100644 docs/content/error/$parse/isecfn.ngdoc
delete mode 100644 docs/content/error/$parse/isecobj.ngdoc
delete mode 100644 docs/content/error/$parse/isecwindow.ngdoc
diff --git a/docs/content/error/$parse/isecaf.ngdoc b/docs/content/error/$parse/isecaf.ngdoc
deleted file mode 100644
index 115e1b26d754..000000000000
--- a/docs/content/error/$parse/isecaf.ngdoc
+++ /dev/null
@@ -1,12 +0,0 @@
-@ngdoc error
-@name $parse:isecaf
-@fullName Assigning to Fields of Disallowed Context
-@description
-
-Occurs when an expression attempts to assign a value on a field of any of the `Boolean`, `Number`,
-`String`, `Array`, `Object`, or `Function` constructors or the corresponding prototypes.
-
-Angular bans the modification of these constructors or their prototypes from within expressions,
-since it is a known way to modify the behaviour of existing functions/operations.
-
-To resolve this error, avoid assigning to fields of constructors or their prototypes in expressions.
diff --git a/docs/content/error/$parse/isecdom.ngdoc b/docs/content/error/$parse/isecdom.ngdoc
deleted file mode 100644
index 9f60e189ee92..000000000000
--- a/docs/content/error/$parse/isecdom.ngdoc
+++ /dev/null
@@ -1,47 +0,0 @@
-@ngdoc error
-@name $parse:isecdom
-@fullName Referencing a DOM node in Expression
-@description
-
-Occurs when an expression attempts to access a DOM node.
-
-AngularJS restricts access to DOM nodes from within expressions since it's a known way to
-execute arbitrary Javascript code.
-
-This check is only performed on object index and function calls in Angular expressions. These are
-places that are harder for the developer to guard. Dotted member access (such as a.b.c) does not
-perform this check - it's up to the developer to not expose such sensitive and powerful objects
-directly on the scope chain.
-
-To resolve this error, avoid access to DOM nodes.
-
-
-# Event Handlers and Return Values
-
-The `$parse:isecdom` error also occurs when an event handler invokes a function that returns a DOM
-node.
-
-```html
- click me
-```
-
-```js
- $scope.iWillReturnDOM = function() {
- return someDomNode;
- }
-```
-
-To fix this issue, avoid returning DOM nodes from event handlers.
-
-*Note: This error often means that you are accessing DOM from your controllers, which is usually
-a sign of poor coding style that violates separation of concerns.*
-
-
-# Implicit Returns in CoffeeScript
-
-This error can occur more frequently when using CoffeeScript, which has a feature called implicit
-returns. This language feature returns the last dereferenced object in the function when the
-function has no explicit return statement.
-
-The solution in this scenario is to add an explicit return statement. For example `return false` to
-the function.
diff --git a/docs/content/error/$parse/isecff.ngdoc b/docs/content/error/$parse/isecff.ngdoc
deleted file mode 100644
index a1e72775d254..000000000000
--- a/docs/content/error/$parse/isecff.ngdoc
+++ /dev/null
@@ -1,17 +0,0 @@
-@ngdoc error
-@name $parse:isecff
-@fullName Referencing 'call', 'apply' and 'bind' Disallowed
-@description
-
-Occurs when an expression attempts to invoke Function's 'call', 'apply' or 'bind'.
-
-Angular bans the invocation of 'call', 'apply' and 'bind' from within expressions
-since access is a known way to modify the behaviour of existing functions.
-
-To resolve this error, avoid using these methods in expressions.
-
-Example expression that would result in this error:
-
-```
-{{user.sendInfo.call({}, true)}}
-```
diff --git a/docs/content/error/$parse/isecfld.ngdoc b/docs/content/error/$parse/isecfld.ngdoc
deleted file mode 100644
index a19c5fa51e97..000000000000
--- a/docs/content/error/$parse/isecfld.ngdoc
+++ /dev/null
@@ -1,27 +0,0 @@
-@ngdoc error
-@name $parse:isecfld
-@fullName Referencing Disallowed Field in Expression
-@description
-
-Occurs when an expression attempts to access one of the following fields:
-
-* __proto__
-* __defineGetter__
-* __defineSetter__
-* __lookupGetter__
-* __lookupSetter__
-
-AngularJS bans access to these fields from within expressions since
-access is a known way to mess with native objects or
-to execute arbitrary Javascript code.
-
-To resolve this error, avoid using these fields in expressions. As a last resort,
-alias their value and access them through the alias instead.
-
-Example expressions that would result in this error:
-
-```
-{{user.__proto__.hasOwnProperty = $emit}}
-
-{{user.__defineGetter__('name', noop)}}
-```
diff --git a/docs/content/error/$parse/isecfn.ngdoc b/docs/content/error/$parse/isecfn.ngdoc
deleted file mode 100644
index 417551cb3606..000000000000
--- a/docs/content/error/$parse/isecfn.ngdoc
+++ /dev/null
@@ -1,10 +0,0 @@
-@ngdoc error
-@name $parse:isecfn
-@fullName Referencing Function Disallowed
-@description
-
-Occurs when an expression attempts to access the 'Function' object (constructor for all functions in JavaScript).
-
-Angular bans access to Function from within expressions since constructor access is a known way to execute arbitrary Javascript code.
-
-To resolve this error, avoid Function access.
diff --git a/docs/content/error/$parse/isecobj.ngdoc b/docs/content/error/$parse/isecobj.ngdoc
deleted file mode 100644
index 8da6e27a3505..000000000000
--- a/docs/content/error/$parse/isecobj.ngdoc
+++ /dev/null
@@ -1,11 +0,0 @@
-@ngdoc error
-@name $parse:isecobj
-@fullName Referencing Object Disallowed
-@description
-
-Occurs when an expression attempts to access the 'Object' object (Root object in JavaScript).
-
-Angular bans access to Object from within expressions since access is a known way to modify
-the behaviour of existing objects.
-
-To resolve this error, avoid Object access.
diff --git a/docs/content/error/$parse/isecwindow.ngdoc b/docs/content/error/$parse/isecwindow.ngdoc
deleted file mode 100644
index e7f4ceeaddd5..000000000000
--- a/docs/content/error/$parse/isecwindow.ngdoc
+++ /dev/null
@@ -1,45 +0,0 @@
-@ngdoc error
-@name $parse:isecwindow
-@fullName Referencing Window object in Expression
-@description
-
-Occurs when an expression attempts to access a Window object.
-
-AngularJS restricts access to the Window object from within expressions since it's a known way to
-execute arbitrary Javascript code.
-
-This check is only performed on object index and function calls in Angular expressions. These are
-places that are harder for the developer to guard. Dotted member access (such as a.b.c) does not
-perform this check - it's up to the developer to not expose such sensitive and powerful objects
-directly on the scope chain.
-
-To resolve this error, avoid Window access.
-
-### Common CoffeeScript Issue
-
-Be aware that if you are using CoffeeScript, it automatically returns the value of the last statement in a
-function. So for instance
-
-```coffeescript
- scope.foo = ->
- window.open 'https://example.com'
-```
-
-compiles to something like
-
-```js
- scope.foo = function() {
- return window.open('https://example.com');
- };
-```
-
-You can see that this function will return the result of calling `window.open`, which is a `Window`
-object.
-
-You can avoid this by explicitly returning something else from the function:
-
-```coffeescript
- scope.foo = ->
- window.open 'https://example.com'
- return true;
-```
diff --git a/docs/content/guide/expression.ngdoc b/docs/content/guide/expression.ngdoc
index 824facd9dfb3..24326b6abae1 100644
--- a/docs/content/guide/expression.ngdoc
+++ b/docs/content/guide/expression.ngdoc
@@ -113,11 +113,11 @@ You can try evaluating different expressions here:
Angular does not use JavaScript's `eval()` to evaluate expressions. Instead Angular's
{@link ng.$parse $parse} service processes these expressions.
-Angular expressions do not have access to global variables like `window`, `document` or `location`.
+Angular expressions do not have direct access to global variables like `window`, `document` or `location`.
This restriction is intentional. It prevents accidental access to the global state – a common source of subtle bugs.
-Instead use services like `$window` and `$location` in functions called from expressions. Such services
-provide mockable access to globals.
+Instead use services like `$window` and `$location` in functions on controllers, which are then called from expressions.
+Such services provide mockable access to globals.
It is possible to access the context object using the identifier `this` and the locals object using the
identifier `$locals`.
diff --git a/docs/content/guide/security.ngdoc b/docs/content/guide/security.ngdoc
index 175c49edf9c0..9fcf0e7c6078 100644
--- a/docs/content/guide/security.ngdoc
+++ b/docs/content/guide/security.ngdoc
@@ -30,42 +30,55 @@ so keeping to AngularJS standards is not just a functionality issue, it's also c
facilitate rapid security updates.
-## Expression Sandboxing
-
-AngularJS's expressions are sandboxed not for security reasons, but instead to maintain a proper
-separation of application responsibilities. For example, access to `window` is disallowed
-because it makes it easy to introduce brittle global state into your application.
-
-However, this sandbox is not intended to stop attackers who can edit the template before it's
-processed by Angular. It may be possible to run arbitrary JavaScript inside double-curly bindings
-if an attacker can modify them.
-
-But if an attacker can change arbitrary HTML templates, there's nothing stopping them from doing:
-
-```html
-
-```
-
-**It's better to design your application in such a way that users cannot change client-side templates.**
-
-For instance:
+## Angular Templates and Expressions
+
+**If an attacker has access to control Angular templates or expressions, they can exploit an Angular application
+via an XSS attack, regardless of the version.**
+
+There are a number of ways that templates and expressions can be controlled:
+
+* **Generating Angular templates on the server containing user-provided content**. This is the most common pitfall
+ where you are generating HTML via some server-side engine such as PHP, Java or ASP.NET.
+* **Passing an expression generated from user-provided content in calls to the following methods on a {@link scope scope}**:
+ * `$watch(userContent, ...)`
+ * `$watchGroup(userContent, ...)`
+ * `$watchCollection(userContent, ...)`
+ * `$eval(userContent)`
+ * `$evalAsync(userContent)`
+ * `$apply(userContent)`
+ * `$applyAsync(userContent)`
+* **Passing an expression generated from user-provided content in calls to services that parse expressions**:
+ * `$compile(userContent)`
+ * `$parse(userContent)`
+ * `$interpolate(userContent)`
+* **Passing an expression generated from user provided content as a predicate to `orderBy` pipe**:
+ `{{ value | orderBy : userContent }}`
+
+### Sandbox removal
+Each version of Angular 1 up to, but not including 1.6, contained an expression sandbox, which reduced the surface area of
+the vulnerability but never removed it. **In Angular 1.6 we removed this sandbox as developers kept relying upon it as a security
+feature even though it was always possible to access arbitrary JavaScript code if one could control the Angular templates
+or expressions of applications.**
+
+Control of the Angular templates makes applications vulnerable even if there was a completely secure sandbox:
+* https://ryhanson.com/stealing-session-tokens-on-plunker-with-an-angular-expression-injection/ in this blog post the author shows
+ a (now closed) vulnerability in the Plunker application due to server-side rendering inside an Angular template.
+* https://ryhanson.com/angular-expression-injection-walkthrough/ in this blog post the author describes an attack, which does not
+ rely upon an expression sandbox bypass, that can be made because the sample application is rendering a template on the server that
+ contains user entered content.
+
+**It's best to design your application in such a way that users cannot change client-side templates.**
* Do not mix client and server templates
* Do not use user input to generate templates dynamically
-* Do not run user input through `$scope.$eval`
+* Do not run user input through `$scope.$eval` (or any of the other expression parsing functions listed above)
* Consider using {@link ng.directive:ngCsp CSP} (but don't rely only on CSP)
+**You can use suitably sanitized server-side templating to dynamically generate CSS, URLs, etc, but not for generating templates that are
+bootstrapped/compiled by Angular.**
-### Mixing client-side and server-side templates
-
-In general, we recommend against this because it can create unintended XSS vectors.
-
-However, it's ok to mix server-side templating in the bootstrap template (`index.html`) as long
-as user input cannot be used on the server to output html that would then be processed by Angular
-in a way that would allow for arbitrary code execution.
-
-**For instance, you can use server-side templating to dynamically generate CSS, URLs, etc, but not
-for generating templates that are bootstrapped/compiled by Angular.**
+**If you must continue to allow user-provided content in an Angular template then the safest option is to ensure that it is only
+present in the part of the template that is made inert via the {@link ngNonBindable} directive.**
## HTTP Requests
diff --git a/src/ng/parse.js b/src/ng/parse.js
index a008dd1e4fea..ed0c4ca43afc 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -13,60 +13,23 @@
var $parseMinErr = minErr('$parse');
-var ARRAY_CTOR = [].constructor;
-var BOOLEAN_CTOR = (false).constructor;
-var FUNCTION_CTOR = Function.constructor;
-var NUMBER_CTOR = (0).constructor;
-var OBJECT_CTOR = {}.constructor;
-var STRING_CTOR = ''.constructor;
-var ARRAY_CTOR_PROTO = ARRAY_CTOR.prototype;
-var BOOLEAN_CTOR_PROTO = BOOLEAN_CTOR.prototype;
-var FUNCTION_CTOR_PROTO = FUNCTION_CTOR.prototype;
-var NUMBER_CTOR_PROTO = NUMBER_CTOR.prototype;
-var OBJECT_CTOR_PROTO = OBJECT_CTOR.prototype;
-var STRING_CTOR_PROTO = STRING_CTOR.prototype;
-
-var CALL = FUNCTION_CTOR_PROTO.call;
-var APPLY = FUNCTION_CTOR_PROTO.apply;
-var BIND = FUNCTION_CTOR_PROTO.bind;
-
-var objectValueOf = OBJECT_CTOR_PROTO.valueOf;
+var objectValueOf = {}.constructor.prototype.valueOf;
// Sandboxing Angular Expressions
// ------------------------------
-// Angular expressions are generally considered safe because these expressions only have direct
-// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by
-// obtaining a reference to native JS functions such as the Function constructor.
+// Angular expressions are no longer sandboxed. So it is now even easier to access arbitary JS code by
+// various means such as obtaining a reference to native JS functions like the Function constructor.
//
// As an example, consider the following Angular expression:
//
// {}.toString.constructor('alert("evil JS code")')
//
-// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits
-// against the expression language, but not to prevent exploits that were enabled by exposing
-// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good
-// practice and therefore we are not even trying to protect against interaction with an object
-// explicitly exposed in this way.
-//
-// In general, it is not possible to access a Window object from an angular expression unless a
-// window or some DOM object that has a reference to window is published onto a Scope.
-// Similarly we prevent invocations of function known to be dangerous, as well as assignments to
-// native objects.
+// It is important to realise that if you create an expression from a string that contains user provided
+// content then it is possible that your application contains a security vulnerability to an XSS style attack.
//
// See https://docs.angularjs.org/guide/security
-function ensureSafeMemberName(name, fullExpression) {
- if (name === '__defineGetter__' || name === '__defineSetter__'
- || name === '__lookupGetter__' || name === '__lookupSetter__'
- || name === '__proto__') {
- throw $parseMinErr('isecfld',
- 'Attempting to access a disallowed field in Angular expressions! '
- + 'Expression: {0}', fullExpression);
- }
- return name;
-}
-
function getStringValue(name) {
// Property names must be strings. This means that non-string objects cannot be used
// as keys in an object. Any non-string object, including a number, is typecasted
@@ -85,67 +48,6 @@ function getStringValue(name) {
return name + '';
}
-function ensureSafeObject(obj, fullExpression) {
- // nifty check if obj is Function that is fast and works across iframes and other contexts
- if (obj) {
- if (obj.constructor === obj) {
- throw $parseMinErr('isecfn',
- 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// isWindow(obj)
- obj.window === obj) {
- throw $parseMinErr('isecwindow',
- 'Referencing the Window in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// isElement(obj)
- obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) {
- throw $parseMinErr('isecdom',
- 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// block Object so that we can't get hold of dangerous Object.* methods
- obj === Object) {
- throw $parseMinErr('isecobj',
- 'Referencing Object in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- }
- }
- return obj;
-}
-
-function ensureSafeFunction(obj, fullExpression) {
- if (obj) {
- if (obj.constructor === obj) {
- throw $parseMinErr('isecfn',
- 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (obj === CALL || obj === APPLY || obj === BIND) {
- throw $parseMinErr('isecff',
- 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- }
- }
-}
-
-function ensureSafeAssignContext(obj, fullExpression) {
- if (obj) {
- if (obj === ARRAY_CTOR ||
- obj === BOOLEAN_CTOR ||
- obj === FUNCTION_CTOR ||
- obj === NUMBER_CTOR ||
- obj === OBJECT_CTOR ||
- obj === STRING_CTOR ||
- obj === ARRAY_CTOR_PROTO ||
- obj === BOOLEAN_CTOR_PROTO ||
- obj === FUNCTION_CTOR_PROTO ||
- obj === NUMBER_CTOR_PROTO ||
- obj === OBJECT_CTOR_PROTO ||
- obj === STRING_CTOR_PROTO) {
- throw $parseMinErr('isecaf',
- 'Assigning to a constructor or its prototype is disallowed! Expression: {0}',
- fullExpression);
- }
- }
-}
var OPERATORS = createMap();
forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; });
@@ -862,13 +764,12 @@ function ASTCompiler(astBuilder, $filter) {
}
ASTCompiler.prototype = {
- compile: function(expression, expensiveChecks) {
+ compile: function(expression) {
var self = this;
var ast = this.astBuilder.ast(expression);
this.state = {
nextId: 0,
filters: {},
- expensiveChecks: expensiveChecks,
fn: {vars: [], body: [], own: {}},
assign: {vars: [], body: [], own: {}},
inputs: []
@@ -911,21 +812,13 @@ ASTCompiler.prototype = {
// eslint-disable-next-line no-new-func
var fn = (new Function('$filter',
- 'ensureSafeMemberName',
- 'ensureSafeObject',
- 'ensureSafeFunction',
'getStringValue',
- 'ensureSafeAssignContext',
'ifDefined',
'plus',
'text',
fnString))(
this.$filter,
- ensureSafeMemberName,
- ensureSafeObject,
- ensureSafeFunction,
getStringValue,
- ensureSafeAssignContext,
ifDefined,
plusFn,
expression);
@@ -1042,7 +935,6 @@ ASTCompiler.prototype = {
nameId.computed = false;
nameId.name = ast.name;
}
- ensureSafeMemberName(ast.name);
self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)),
function() {
self.if_(self.stage === 'inputs' || 's', function() {
@@ -1055,9 +947,6 @@ ASTCompiler.prototype = {
});
}, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name))
);
- if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) {
- self.addEnsureSafeObject(intoId);
- }
recursionFn(intoId);
break;
case AST.MemberExpression:
@@ -1065,32 +954,24 @@ ASTCompiler.prototype = {
intoId = intoId || this.nextId();
self.recurse(ast.object, left, undefined, function() {
self.if_(self.notNull(left), function() {
- if (create && create !== 1) {
- self.addEnsureSafeAssignContext(left);
- }
if (ast.computed) {
right = self.nextId();
self.recurse(ast.property, right);
self.getStringValue(right);
- self.addEnsureSafeMemberName(right);
if (create && create !== 1) {
self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}'));
}
- expression = self.ensureSafeObject(self.computedMember(left, right));
+ expression = self.computedMember(left, right);
self.assign(intoId, expression);
if (nameId) {
nameId.computed = true;
nameId.name = right;
}
} else {
- ensureSafeMemberName(ast.property.name);
if (create && create !== 1) {
self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}'));
}
expression = self.nonComputedMember(left, ast.property.name);
- if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) {
- expression = self.ensureSafeObject(expression);
- }
self.assign(intoId, expression);
if (nameId) {
nameId.computed = false;
@@ -1122,21 +1003,16 @@ ASTCompiler.prototype = {
args = [];
self.recurse(ast.callee, right, left, function() {
self.if_(self.notNull(right), function() {
- self.addEnsureSafeFunction(right);
forEach(ast.arguments, function(expr) {
self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) {
- args.push(self.ensureSafeObject(argument));
+ args.push(argument);
});
});
if (left.name) {
- if (!self.state.expensiveChecks) {
- self.addEnsureSafeObject(left.context);
- }
expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')';
} else {
expression = right + '(' + args.join(',') + ')';
}
- expression = self.ensureSafeObject(expression);
self.assign(intoId, expression);
}, function() {
self.assign(intoId, 'undefined');
@@ -1154,8 +1030,6 @@ ASTCompiler.prototype = {
this.recurse(ast.left, undefined, left, function() {
self.if_(self.notNull(left.context), function() {
self.recurse(ast.right, right);
- self.addEnsureSafeObject(self.member(left.context, left.name, left.computed));
- self.addEnsureSafeAssignContext(left.context);
expression = self.member(left.context, left.name, left.computed) + ast.operator + right;
self.assign(intoId, expression);
recursionFn(intoId || expression);
@@ -1303,42 +1177,10 @@ ASTCompiler.prototype = {
return this.nonComputedMember(left, right);
},
- addEnsureSafeObject: function(item) {
- this.current().body.push(this.ensureSafeObject(item), ';');
- },
-
- addEnsureSafeMemberName: function(item) {
- this.current().body.push(this.ensureSafeMemberName(item), ';');
- },
-
- addEnsureSafeFunction: function(item) {
- this.current().body.push(this.ensureSafeFunction(item), ';');
- },
-
- addEnsureSafeAssignContext: function(item) {
- this.current().body.push(this.ensureSafeAssignContext(item), ';');
- },
-
- ensureSafeObject: function(item) {
- return 'ensureSafeObject(' + item + ',text)';
- },
-
- ensureSafeMemberName: function(item) {
- return 'ensureSafeMemberName(' + item + ',text)';
- },
-
- ensureSafeFunction: function(item) {
- return 'ensureSafeFunction(' + item + ',text)';
- },
-
getStringValue: function(item) {
this.assign(item, 'getStringValue(' + item + ')');
},
- ensureSafeAssignContext: function(item) {
- return 'ensureSafeAssignContext(' + item + ',text)';
- },
-
lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
var self = this;
return function() {
@@ -1390,11 +1232,10 @@ function ASTInterpreter(astBuilder, $filter) {
}
ASTInterpreter.prototype = {
- compile: function(expression, expensiveChecks) {
+ compile: function(expression) {
var self = this;
var ast = this.astBuilder.ast(expression);
this.expression = expression;
- this.expensiveChecks = expensiveChecks;
findConstantAndWatchExpressions(ast, self.$filter);
var assignable;
var assign;
@@ -1465,20 +1306,17 @@ ASTInterpreter.prototype = {
context
);
case AST.Identifier:
- ensureSafeMemberName(ast.name, self.expression);
return self.identifier(ast.name,
- self.expensiveChecks || isPossiblyDangerousMemberName(ast.name),
context, create, self.expression);
case AST.MemberExpression:
left = this.recurse(ast.object, false, !!create);
if (!ast.computed) {
- ensureSafeMemberName(ast.property.name, self.expression);
right = ast.property.name;
}
if (ast.computed) right = this.recurse(ast.property);
return ast.computed ?
this.computedMember(left, right, context, create, self.expression) :
- this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression);
+ this.nonComputedMember(left, right, context, create, self.expression);
case AST.CallExpression:
args = [];
forEach(ast.arguments, function(expr) {
@@ -1499,13 +1337,11 @@ ASTInterpreter.prototype = {
var rhs = right(scope, locals, assign, inputs);
var value;
if (rhs.value != null) {
- ensureSafeObject(rhs.context, self.expression);
- ensureSafeFunction(rhs.value, self.expression);
var values = [];
for (var i = 0; i < args.length; ++i) {
- values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression));
+ values.push(args[i](scope, locals, assign, inputs));
}
- value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression);
+ value = rhs.value.apply(rhs.context, values);
}
return context ? {value: value} : value;
};
@@ -1515,8 +1351,6 @@ ASTInterpreter.prototype = {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs = right(scope, locals, assign, inputs);
- ensureSafeObject(lhs.value, self.expression);
- ensureSafeAssignContext(lhs.context);
lhs.context[lhs.name] = rhs;
return context ? {value: rhs} : rhs;
};
@@ -1708,16 +1542,13 @@ ASTInterpreter.prototype = {
value: function(value, context) {
return function() { return context ? {context: undefined, name: undefined, value: value} : value; };
},
- identifier: function(name, expensiveChecks, context, create, expression) {
+ identifier: function(name, context, create, expression) {
return function(scope, locals, assign, inputs) {
var base = locals && (name in locals) ? locals : scope;
if (create && create !== 1 && base && !(base[name])) {
base[name] = {};
}
var value = base ? base[name] : undefined;
- if (expensiveChecks) {
- ensureSafeObject(value, expression);
- }
if (context) {
return {context: base, name: name, value: value};
} else {
@@ -1733,15 +1564,12 @@ ASTInterpreter.prototype = {
if (lhs != null) {
rhs = right(scope, locals, assign, inputs);
rhs = getStringValue(rhs);
- ensureSafeMemberName(rhs, expression);
if (create && create !== 1) {
- ensureSafeAssignContext(lhs);
if (lhs && !(lhs[rhs])) {
lhs[rhs] = {};
}
}
value = lhs[rhs];
- ensureSafeObject(value, expression);
}
if (context) {
return {context: lhs, name: rhs, value: value};
@@ -1750,19 +1578,15 @@ ASTInterpreter.prototype = {
}
};
},
- nonComputedMember: function(left, right, expensiveChecks, context, create, expression) {
+ nonComputedMember: function(left, right, context, create, expression) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
if (create && create !== 1) {
- ensureSafeAssignContext(lhs);
if (lhs && !(lhs[right])) {
lhs[right] = {};
}
}
var value = lhs != null ? lhs[right] : undefined;
- if (expensiveChecks || isPossiblyDangerousMemberName(right)) {
- ensureSafeObject(value, expression);
- }
if (context) {
return {context: lhs, name: right, value: value};
} else {
@@ -1794,14 +1618,10 @@ Parser.prototype = {
constructor: Parser,
parse: function(text) {
- return this.astCompiler.compile(text, this.options.expensiveChecks);
+ return this.astCompiler.compile(text);
}
};
-function isPossiblyDangerousMemberName(name) {
- return name === 'constructor';
-}
-
function getValueOf(value) {
return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value);
}
@@ -1859,8 +1679,7 @@ function getValueOf(value) {
* service.
*/
function $ParseProvider() {
- var cacheDefault = createMap();
- var cacheExpensive = createMap();
+ var cache = createMap();
var literals = {
'true': true,
'false': false,
@@ -1918,37 +1737,20 @@ function $ParseProvider() {
var noUnsafeEval = csp().noUnsafeEval;
var $parseOptions = {
csp: noUnsafeEval,
- expensiveChecks: false,
- literals: copy(literals),
- isIdentifierStart: isFunction(identStart) && identStart,
- isIdentifierContinue: isFunction(identContinue) && identContinue
- },
- $parseOptionsExpensive = {
- csp: noUnsafeEval,
- expensiveChecks: true,
literals: copy(literals),
isIdentifierStart: isFunction(identStart) && identStart,
isIdentifierContinue: isFunction(identContinue) && identContinue
};
- var runningChecksEnabled = false;
-
- $parse.$$runningExpensiveChecks = function() {
- return runningChecksEnabled;
- };
-
return $parse;
- function $parse(exp, interceptorFn, expensiveChecks) {
+ function $parse(exp, interceptorFn) {
var parsedExpression, oneTime, cacheKey;
- expensiveChecks = expensiveChecks || runningChecksEnabled;
-
switch (typeof exp) {
case 'string':
exp = exp.trim();
cacheKey = exp;
- var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
parsedExpression = cache[cacheKey];
if (!parsedExpression) {
@@ -1956,9 +1758,8 @@ function $ParseProvider() {
oneTime = true;
exp = exp.substring(2);
}
- var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
- var lexer = new Lexer(parseOptions);
- var parser = new Parser(lexer, $filter, parseOptions);
+ var lexer = new Lexer($parseOptions);
+ var parser = new Parser(lexer, $filter, $parseOptions);
parsedExpression = parser.parse(exp);
if (parsedExpression.constant) {
parsedExpression.$$watchDelegate = constantWatchDelegate;
@@ -1968,9 +1769,6 @@ function $ParseProvider() {
} else if (parsedExpression.inputs) {
parsedExpression.$$watchDelegate = inputsWatchDelegate;
}
- if (expensiveChecks) {
- parsedExpression = expensiveChecksInterceptor(parsedExpression);
- }
cache[cacheKey] = parsedExpression;
}
return addInterceptor(parsedExpression, interceptorFn);
@@ -1983,30 +1781,6 @@ function $ParseProvider() {
}
}
- function expensiveChecksInterceptor(fn) {
- if (!fn) return fn;
- expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate;
- expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign);
- expensiveCheckFn.constant = fn.constant;
- expensiveCheckFn.literal = fn.literal;
- for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) {
- fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]);
- }
- expensiveCheckFn.inputs = fn.inputs;
-
- return expensiveCheckFn;
-
- function expensiveCheckFn(scope, locals, assign, inputs) {
- var expensiveCheckOldValue = runningChecksEnabled;
- runningChecksEnabled = true;
- try {
- return fn(scope, locals, assign, inputs);
- } finally {
- runningChecksEnabled = expensiveCheckOldValue;
- }
- }
- }
-
function expressionInputDirtyCheck(newValue, oldValueOfValue) {
if (newValue == null || oldValueOfValue == null) { // null/undefined
diff --git a/test/ng/directive/ngEventDirsSpec.js b/test/ng/directive/ngEventDirsSpec.js
index e2c2745f840e..d8288b828bbd 100644
--- a/test/ng/directive/ngEventDirsSpec.js
+++ b/test/ng/directive/ngEventDirsSpec.js
@@ -90,23 +90,13 @@ describe('event directives', function() {
});
- describe('security', function() {
+ describe('DOM event object', function() {
it('should allow access to the $event object', inject(function($rootScope, $compile) {
var scope = $rootScope.$new();
element = $compile('BTN ')(scope);
element.triggerHandler('click');
expect(scope.e.target).toBe(element[0]);
}));
-
- it('should block access to DOM nodes (e.g. exposed via $event)', inject(function($rootScope, $compile) {
- var scope = $rootScope.$new();
- element = $compile('BTN ')(scope);
- expect(function() {
- element.triggerHandler('click');
- }).toThrowMinErr(
- '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is disallowed! ' +
- 'Expression: e = $event.target');
- }));
});
describe('blur', function() {
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
index ea216a644c75..79d49340da46 100644
--- a/test/ng/parseSpec.js
+++ b/test/ng/parseSpec.js
@@ -2422,770 +2422,6 @@ describe('parser', function() {
}));
- describe('sandboxing', function() {
- describe('Function constructor', function() {
- it('should not tranverse the Function constructor in the getter', function() {
- expect(function() {
- scope.$eval('{}.toString.constructor');
- }).toThrowMinErr(
- '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: {}.toString.constructor');
- });
-
- it('should not allow access to the Function prototype in the getter', function() {
- expect(function() {
- scope.$eval('toString.constructor.prototype');
- }).toThrowMinErr(
- '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: toString.constructor.prototype');
- });
-
- it('should NOT allow access to Function constructor in getter', function() {
- expect(function() {
- scope.$eval('{}.toString.constructor("alert(1)")');
- }).toThrowMinErr(
- '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: {}.toString.constructor("alert(1)")');
- });
-
- it('should NOT allow access to Function constructor in setter', function() {
-
- expect(function() {
- scope.$eval('{}.toString.constructor.a = 1');
- }).toThrowMinErr(
- '$parse', 'isecfn','Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: {}.toString.constructor.a = 1');
-
- expect(function() {
- scope.$eval('{}.toString["constructor"]["constructor"] = 1');
- }).toThrowMinErr(
- '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: {}.toString["constructor"]["constructor"] = 1');
-
- scope.key1 = 'const';
- scope.key2 = 'ructor';
- expect(function() {
- scope.$eval('{}.toString[key1 + key2].foo = 1');
- }).toThrowMinErr(
- '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: {}.toString[key1 + key2].foo = 1');
-
- expect(function() {
- scope.$eval('{}.toString["constructor"]["a"] = 1');
- }).toThrowMinErr(
- '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: {}.toString["constructor"]["a"] = 1');
-
- scope.a = [];
- expect(function() {
- scope.$eval('a.toString.constructor = 1', scope);
- }).toThrowMinErr(
- '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: a.toString.constructor');
- });
-
- it('should disallow traversing the Function object in a setter: E02', function() {
- expect(function() {
- // This expression by itself isn't dangerous. However, one can use this to
- // automatically call an object (e.g. a Function object) when it is automatically
- // toString'd/valueOf'd by setting the RHS to Function.prototype.call.
- scope.$eval('hasOwnProperty.constructor.prototype.valueOf = 1');
- }).toThrowMinErr(
- '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: hasOwnProperty.constructor.prototype.valueOf');
- });
-
- it('should disallow passing the Function object as a parameter: E03', function() {
- expect(function() {
- // This expression constructs a function but does not execute it. It does lead the
- // way to execute it if one can get the toString/valueOf of it to call the function.
- scope.$eval('["a", "alert(1)"].sort(hasOwnProperty.constructor)');
- }).toThrow();
- });
-
- it('should prevent exploit E01', function() {
- // This is a tracking exploit. The two individual tests, it('should … : E02') and
- // it('should … : E03') test for two parts to block this exploit. This exploit works
- // as follows:
- //
- // • Array.sort takes a comparison function and passes it 2 parameters to compare. If
- // the result is non-primitive, sort then invokes valueOf() on the result.
- // • The Function object conveniently accepts two string arguments so we can use this
- // to construct a function. However, this doesn't do much unless we can execute it.
- // • We set the valueOf property on Function.prototype to Function.prototype.call.
- // This causes the function that we constructed to be executed when sort calls
- // .valueOf() on the result of the comparison.
- expect(function() {
- scope.$eval('' +
- 'hasOwnProperty.constructor.prototype.valueOf=valueOf.call;' +
- '["a","alert(1)"].sort(hasOwnProperty.constructor)');
- }).toThrow();
- });
-
- it('should NOT allow access to Function constructor that has been aliased in getters', function() {
- scope.foo = { 'bar': Function };
- expect(function() {
- scope.$eval('foo["bar"]');
- }).toThrowMinErr(
- '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: foo["bar"]');
- });
-
- it('should NOT allow access to Function constructor that has been aliased in setters', function() {
- scope.foo = { 'bar': Function };
- expect(function() {
- scope.$eval('foo["bar"] = 1');
- }).toThrowMinErr(
- '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
- 'Expression: foo["bar"] = 1');
- });
-
- describe('expensiveChecks', function() {
- it('should block access to window object even when aliased in getters', inject(function($parse, $window) {
- scope.foo = {w: $window};
- // This isn't blocked for performance.
- expect(scope.$eval($parse('foo.w'))).toBe($window);
- // Event handlers use the more expensive path for better protection since they expose
- // the $event object on the scope.
- expect(function() {
- scope.$eval($parse('foo.w', null, true));
- }).toThrowMinErr(
- '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' +
- 'Expression: foo.w');
- }));
-
- it('should block access to window object even when aliased in setters', inject(function($parse, $window) {
- scope.foo = {w: $window};
- // This is blocked as it points to `window`.
- expect(function() {
- expect(scope.$eval($parse('foo.w = 1'))).toBe($window);
- }).toThrowMinErr(
- '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' +
- 'Expression: foo.w = 1');
- // Event handlers use the more expensive path for better protection since they expose
- // the $event object on the scope.
- expect(function() {
- scope.$eval($parse('foo.w = 1', null, true));
- }).toThrowMinErr(
- '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' +
- 'Expression: foo.w = 1');
- }));
-
- they('should propagate expensive checks when calling $prop',
- ['foo.w && true',
- '$eval("foo.w && true")',
- 'this["$eval"]("foo.w && true")',
- 'bar;$eval("foo.w && true")',
- '$eval("foo.w && true");bar',
- '$eval("foo.w && true", null, false)',
- '$eval("foo");$eval("foo.w && true")',
- '$eval("$eval(\\"foo.w && true\\")")',
- '$eval("foo.e()")',
- '$evalAsync("foo.w && true")',
- 'this["$evalAsync"]("foo.w && true")',
- 'bar;$evalAsync("foo.w && true")',
- '$evalAsync("foo.w && true");bar',
- '$evalAsync("foo.w && true", null, false)',
- '$evalAsync("foo");$evalAsync("foo.w && true")',
- '$evalAsync("$evalAsync(\\"foo.w && true\\")")',
- '$evalAsync("foo.e()")',
- '$evalAsync("$eval(\\"foo.w && true\\")")',
- '$eval("$evalAsync(\\"foo.w && true\\")")',
- '$watch("foo.w && true")',
- '$watchCollection("foo.w && true", foo.f)',
- '$watchGroup(["foo.w && true"])',
- '$applyAsync("foo.w && true")'], function(expression) {
- inject(function($parse, $window) {
- scope.foo = {
- w: $window,
- bar: 'bar',
- e: function() { scope.$eval('foo.w && true'); },
- f: function() {}
- };
- expect($parse.$$runningExpensiveChecks()).toEqual(false);
- expect(function() {
- scope.$eval($parse(expression, null, true));
- scope.$digest();
- }).toThrowMinErr(
- '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' +
- 'Expression: foo.w && true');
- expect($parse.$$runningExpensiveChecks()).toEqual(false);
- });
- });
-
- they('should restore the state of $$runningExpensiveChecks when the expression $prop throws',
- ['$eval("foo.t()")',
- '$evalAsync("foo.t()", {foo: foo})'], function(expression) {
- inject(function($parse, $window) {
- scope.foo = {
- t: function() { throw new Error(); }
- };
- expect($parse.$$runningExpensiveChecks()).toEqual(false);
- expect(function() {
- scope.$eval($parse(expression, null, true));
- scope.$digest();
- }).toThrow();
- expect($parse.$$runningExpensiveChecks()).toEqual(false);
- });
- });
-
- it('should handle `inputs` when running with expensive checks', inject(function($parse) {
- expect(function() {
- scope.$watch($parse('a + b', null, true), noop);
- scope.$digest();
- }).not.toThrow();
- }));
- });
- });
-
- describe('Function prototype functions', function() {
- it('should NOT allow invocation to Function.call', function() {
- scope.fn = Function.prototype.call;
-
- expect(function() {
- scope.$eval('$eval.call()');
- }).toThrowMinErr(
- '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' +
- 'Expression: $eval.call()');
-
- expect(function() {
- scope.$eval('fn()');
- }).toThrowMinErr(
- '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' +
- 'Expression: fn()');
- });
-
- it('should NOT allow invocation to Function.apply', function() {
- scope.apply = Function.prototype.apply;
-
- expect(function() {
- scope.$eval('$eval.apply()');
- }).toThrowMinErr(
- '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' +
- 'Expression: $eval.apply()');
-
- expect(function() {
- scope.$eval('apply()');
- }).toThrowMinErr(
- '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' +
- 'Expression: apply()');
- });
-
- it('should NOT allow invocation to Function.bind', function() {
- scope.bind = Function.prototype.bind;
-
- expect(function() {
- scope.$eval('$eval.bind()');
- }).toThrowMinErr(
- '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' +
- 'Expression: $eval.bind()');
-
- expect(function() {
- scope.$eval('bind()');
- }).toThrowMinErr(
- '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' +
- 'Expression: bind()');
- });
- });
-
- describe('Object constructor', function() {
-
- it('should NOT allow access to Object constructor that has been aliased in getters', function() {
- scope.foo = { 'bar': Object };
-
- expect(function() {
- scope.$eval('foo.bar.keys(foo)');
- }).toThrowMinErr(
- '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' +
- 'Expression: foo.bar.keys(foo)');
-
- expect(function() {
- scope.$eval('foo["bar"]["keys"](foo)');
- }).toThrowMinErr(
- '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' +
- 'Expression: foo["bar"]["keys"](foo)');
- });
-
- it('should NOT allow access to Object constructor that has been aliased in setters', function() {
- scope.foo = { 'bar': Object };
-
- expect(function() {
- scope.$eval('foo.bar.keys(foo).bar = 1');
- }).toThrowMinErr(
- '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' +
- 'Expression: foo.bar.keys(foo).bar = 1');
-
- expect(function() {
- scope.$eval('foo["bar"]["keys"](foo).bar = 1');
- }).toThrowMinErr(
- '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' +
- 'Expression: foo["bar"]["keys"](foo).bar = 1');
- });
- });
-
- describe('Window and $element/node', function() {
- it('should NOT allow access to the Window or DOM when indexing', inject(function($window, $document) {
- scope.wrap = {w: $window, d: $document};
-
- expect(function() {
- scope.$eval('wrap["w"]', scope);
- }).toThrowMinErr(
- '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' +
- 'disallowed! Expression: wrap["w"]');
- expect(function() {
- scope.$eval('wrap["d"]', scope);
- }).toThrowMinErr(
- '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' +
- 'disallowed! Expression: wrap["d"]');
- expect(function() {
- scope.$eval('wrap["w"] = 1', scope);
- }).toThrowMinErr(
- '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' +
- 'disallowed! Expression: wrap["w"] = 1');
- expect(function() {
- scope.$eval('wrap["d"] = 1', scope);
- }).toThrowMinErr(
- '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' +
- 'disallowed! Expression: wrap["d"] = 1');
- }));
-
- it('should NOT allow access to the Window or DOM returned from a function', inject(function($window, $document) {
- scope.getWin = valueFn($window);
- scope.getDoc = valueFn($document);
-
- expect(function() {
- scope.$eval('getWin()', scope);
- }).toThrowMinErr(
- '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' +
- 'disallowed! Expression: getWin()');
- expect(function() {
- scope.$eval('getDoc()', scope);
- }).toThrowMinErr(
- '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' +
- 'disallowed! Expression: getDoc()');
- }));
-
- it('should NOT allow calling functions on Window or DOM', inject(function($window, $document) {
- scope.a = {b: { win: $window, doc: $document }};
- expect(function() {
- scope.$eval('a.b.win.alert(1)', scope);
- }).toThrowMinErr(
- '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' +
- 'disallowed! Expression: a.b.win.alert(1)');
- expect(function() {
- scope.$eval('a.b.doc.on("click")', scope);
- }).toThrowMinErr(
- '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' +
- 'disallowed! Expression: a.b.doc.on("click")');
- }));
-
- // Issue #4805
- it('should NOT throw isecdom when referencing a Backbone Collection', function() {
- // Backbone stuff is sort of hard to mock, if you have a better way of doing this,
- // please fix this.
- var fakeBackboneCollection = {
- children: [{}, {}, {}],
- find: function() {},
- on: function() {},
- off: function() {},
- bind: function() {}
- };
- scope.backbone = fakeBackboneCollection;
- expect(function() { scope.$eval('backbone'); }).not.toThrow();
- });
-
- it('should NOT throw isecdom when referencing an array with node properties', function() {
- var array = [1,2,3];
- array.on = array.attr = array.prop = array.bind = true;
- scope.array = array;
- expect(function() { scope.$eval('array'); }).not.toThrow();
- });
- });
-
- describe('Disallowed fields', function() {
- it('should NOT allow access or invocation of __defineGetter__', function() {
- expect(function() {
- scope.$eval('{}.__defineGetter__');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}.__defineGetter__("a", "".charAt)');
- }).toThrowMinErr('$parse', 'isecfld');
-
- expect(function() {
- scope.$eval('{}["__defineGetter__"]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}["__defineGetter__"]("a", "".charAt)');
- }).toThrowMinErr('$parse', 'isecfld');
-
- scope.a = '__define';
- scope.b = 'Getter__';
- expect(function() {
- scope.$eval('{}[a + b]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}[a + b]("a", "".charAt)');
- }).toThrowMinErr('$parse', 'isecfld');
- });
-
- it('should NOT allow access or invocation of __defineSetter__', function() {
- expect(function() {
- scope.$eval('{}.__defineSetter__');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}.__defineSetter__("a", "".charAt)');
- }).toThrowMinErr('$parse', 'isecfld');
-
- expect(function() {
- scope.$eval('{}["__defineSetter__"]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}["__defineSetter__"]("a", "".charAt)');
- }).toThrowMinErr('$parse', 'isecfld');
-
- scope.a = '__define';
- scope.b = 'Setter__';
- expect(function() {
- scope.$eval('{}[a + b]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}[a + b]("a", "".charAt)');
- }).toThrowMinErr('$parse', 'isecfld');
- });
-
- it('should NOT allow access or invocation of __lookupGetter__', function() {
- expect(function() {
- scope.$eval('{}.__lookupGetter__');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}.__lookupGetter__("a")');
- }).toThrowMinErr('$parse', 'isecfld');
-
- expect(function() {
- scope.$eval('{}["__lookupGetter__"]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}["__lookupGetter__"]("a")');
- }).toThrowMinErr('$parse', 'isecfld');
-
- scope.a = '__lookup';
- scope.b = 'Getter__';
- expect(function() {
- scope.$eval('{}[a + b]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}[a + b]("a")');
- }).toThrowMinErr('$parse', 'isecfld');
- });
-
- it('should NOT allow access or invocation of __lookupSetter__', function() {
- expect(function() {
- scope.$eval('{}.__lookupSetter__');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}.__lookupSetter__("a")');
- }).toThrowMinErr('$parse', 'isecfld');
-
- expect(function() {
- scope.$eval('{}["__lookupSetter__"]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}["__lookupSetter__"]("a")');
- }).toThrowMinErr('$parse', 'isecfld');
-
- scope.a = '__lookup';
- scope.b = 'Setter__';
- expect(function() {
- scope.$eval('{}[a + b]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}[a + b]("a")');
- }).toThrowMinErr('$parse', 'isecfld');
- });
-
- it('should NOT allow access to __proto__', function() {
- expect(function() {
- scope.$eval('__proto__');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}.__proto__');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}.__proto__.foo = 1');
- }).toThrowMinErr('$parse', 'isecfld');
-
- expect(function() {
- scope.$eval('{}["__proto__"]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}["__proto__"].foo = 1');
- }).toThrowMinErr('$parse', 'isecfld');
-
- expect(function() {
- scope.$eval('{}[["__proto__"]]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}[["__proto__"]].foo = 1');
- }).toThrowMinErr('$parse', 'isecfld');
-
- expect(function() {
- scope.$eval('0[["__proto__"]]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('0[["__proto__"]].foo = 1');
- }).toThrowMinErr('$parse', 'isecfld');
-
- scope.a = '__pro';
- scope.b = 'to__';
- expect(function() {
- scope.$eval('{}[a + b]');
- }).toThrowMinErr('$parse', 'isecfld');
- expect(function() {
- scope.$eval('{}[a + b].foo = 1');
- }).toThrowMinErr('$parse', 'isecfld');
- });
- });
-
- it('should prevent the exploit', function() {
- expect(function() {
- scope.$eval('(1)[{0: "__proto__", 1: "__proto__", 2: "__proto__", 3: "safe", length: 4, toString: [].pop}].foo = 1');
- }).toThrow();
- if (!msie || msie > 10) {
- // eslint-disable-next-line no-proto
- expect((1)['__proto__'].foo).toBeUndefined();
- }
- });
-
- it('should prevent the exploit', function() {
- expect(function() {
- scope.$eval('' +
- ' "".sub.call.call(' +
- '({})["constructor"].getOwnPropertyDescriptor("".sub.__proto__, "constructor").value,' +
- 'null,' +
- '"alert(1)"' +
- ')()' +
- '');
- }).toThrow();
- });
-
- they('should prevent assigning in the context of the $prop constructor', {
- Array: [[], '[]'],
- Boolean: [true, '(true)'],
- Number: [1, '(1)'],
- String: ['string', '"string"']
- }, function(values) {
- var thing = values[0];
- var expr = values[1];
- var constructorExpr = expr + '.constructor';
-
- expect(function() {
- scope.$eval(constructorExpr + '.join');
- }).not.toThrow();
- expect(function() {
- delete scope.foo;
- scope.$eval('foo = ' + constructorExpr + '.join');
- }).not.toThrow();
- expect(function() {
- scope.$eval(constructorExpr + '.join = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- scope.$eval(constructorExpr + '[0] = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo = ' + constructorExpr + '; foo.join = ""');
- }).toThrowMinErr('$parse', 'isecaf');
-
- expect(function() {
- scope.foo = thing;
- scope.$eval('foo.constructor[0] = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo.constructor[0] = ""', {foo: thing});
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- scope.foo = thing.constructor;
- scope.$eval('foo[0] = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo[0] = ""', {foo: thing.constructor});
- }).toThrowMinErr('$parse', 'isecaf');
- });
-
- they('should prevent assigning in the context of the $prop constructor', {
- // These might throw different error (e.g. isecobj, isecfn),
- // but still having them here for good measure
- Function: [noop, '$eval'],
- Object: [{}, '{}']
- }, function(values) {
- var thing = values[0];
- var expr = values[1];
- var constructorExpr = expr + '.constructor';
-
- expect(function() {
- scope.$eval(constructorExpr + '.join');
- }).not.toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo = ' + constructorExpr + '.join');
- }).not.toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- scope.$eval(constructorExpr + '.join = ""');
- }).toThrow();
- expect(function() {
- scope.$eval(constructorExpr + '[0] = ""');
- }).toThrow();
- expect(function() {
- delete scope.foo;
- scope.$eval('foo = ' + constructorExpr + '; foo.join = ""');
- }).toThrow();
-
- expect(function() {
- scope.foo = thing;
- scope.$eval('foo.constructor[0] = ""');
- }).toThrow();
- expect(function() {
- delete scope.foo;
- scope.$eval('foo.constructor[0] = ""', {foo: thing});
- }).toThrow();
- expect(function() {
- scope.foo = thing.constructor;
- scope.$eval('foo[0] = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo[0] = ""', {foo: thing.constructor});
- }).toThrowMinErr('$parse', 'isecaf');
- });
-
- it('should prevent assigning only in the context of an actual constructor', function() {
- // foo.constructor is not a constructor.
- expect(function() {
- delete scope.foo;
- scope.$eval('foo.constructor[0] = ""', {foo: {constructor: ''}});
- }).not.toThrow();
-
- expect(function() {
- scope.$eval('"a".constructor.prototype.charAt = [].join');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- scope.$eval('"a".constructor.prototype.charCodeAt = [].concat');
- }).toThrowMinErr('$parse', 'isecaf');
- });
-
- they('should prevent assigning in the context of the $prop constructor prototype', {
- Array: [[], '[]'],
- Boolean: [true, '(true)'],
- Number: [1, '(1)'],
- String: ['string', '"string"']
- }, function(values) {
- var thing = values[0];
- var expr = values[1];
- var constructorExpr = expr + '.constructor';
- var prototypeExpr = constructorExpr + '.prototype';
-
- expect(function() {
- scope.$eval(prototypeExpr + '.boin');
- }).not.toThrow();
- expect(function() {
- delete scope.foo;
- scope.$eval('foo = ' + prototypeExpr + '.boin');
- }).not.toThrow();
- expect(function() {
- scope.$eval(prototypeExpr + '.boin = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- scope.$eval(prototypeExpr + '[0] = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo = ' + constructorExpr + '; foo.prototype.boin = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo = ' + prototypeExpr + '; foo.boin = ""');
- }).toThrowMinErr('$parse', 'isecaf');
-
- expect(function() {
- scope.foo = thing.constructor;
- scope.$eval('foo.prototype[0] = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo.prototype[0] = ""', {foo: thing.constructor});
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- scope.foo = thing.constructor.prototype;
- scope.$eval('foo[0] = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo[0] = ""', {foo: thing.constructor.prototype});
- }).toThrowMinErr('$parse', 'isecaf');
- });
-
- they('should prevent assigning in the context of a constructor prototype', {
- // These might throw different error (e.g. isecobj, isecfn),
- // but still having them here for good measure
- Function: [noop, '$eval'],
- Object: [{}, '{}']
- }, function(values) {
- var thing = values[0];
- var expr = values[1];
- var constructorExpr = expr + '.constructor';
- var prototypeExpr = constructorExpr + '.prototype';
-
- expect(function() {
- scope.$eval(prototypeExpr + '.boin');
- }).not.toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo = ' + prototypeExpr + '.boin');
- }).not.toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- scope.$eval(prototypeExpr + '.boin = ""');
- }).toThrow();
- expect(function() {
- scope.$eval(prototypeExpr + '[0] = ""');
- }).toThrow();
- expect(function() {
- delete scope.foo;
- scope.$eval('foo = ' + constructorExpr + '; foo.prototype.boin = ""');
- }).toThrow();
- expect(function() {
- delete scope.foo;
- scope.$eval('foo = ' + prototypeExpr + '; foo.boin = ""');
- }).toThrow();
-
- expect(function() {
- scope.foo = thing.constructor;
- scope.$eval('foo.prototype[0] = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo.prototype[0] = ""', {foo: thing.constructor});
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- scope.foo = thing.constructor.prototype;
- scope.$eval('foo[0] = ""');
- }).toThrowMinErr('$parse', 'isecaf');
- expect(function() {
- delete scope.foo;
- scope.$eval('foo[0] = ""', {foo: thing.constructor.prototype});
- }).toThrowMinErr('$parse', 'isecaf');
- });
-
- it('should prevent assigning only in the context of an actual prototype', function() {
- // foo.constructor.prototype is not a constructor prototype.
- expect(function() {
- delete scope.foo;
- scope.$eval('foo.constructor.prototype[0] = ""', {foo: {constructor: {prototype: ''}}});
- }).not.toThrow();
- });
- });
-
it('should call the function from the received instance and not from a new one', function() {
var n = 0;
scope.fn = function() {
From 32aa7e7395527624119e3917c54ee43b4d219301 Mon Sep 17 00:00:00 2001
From: Peter Bacon Darwin
Date: Thu, 15 Sep 2016 11:59:42 +0100
Subject: [PATCH 0010/1014] fix(ngTransclude): use fallback content if only
whitespace is provided
If the transcluded content is only whitespace then we should use the
fallback content instead. This allows more flexibility in formatting
your HTML.
Closes #15077
Closes #15140
BREAKING CHANGE:
Previously whitespace only transclusion would be treated as the transclusion
being "not empty", which meant that fallback content was not used in that
case.
Now if you only provide whitespace as the transclusion content, it will be
assumed to be empty and the fallback content will be used instead.
If you really do want whitespace then you can force it to be used by adding
a comment to the whitespace.
---
src/ng/directive/ngTransclude.js | 15 +++++++--
test/ng/compileSpec.js | 54 ++++++++++++++++++++++++++++++++
2 files changed, 66 insertions(+), 3 deletions(-)
diff --git a/src/ng/directive/ngTransclude.js b/src/ng/directive/ngTransclude.js
index f19c251fc3b6..4f48f5ee0bbf 100644
--- a/src/ng/directive/ngTransclude.js
+++ b/src/ng/directive/ngTransclude.js
@@ -13,8 +13,8 @@
*
* If the transcluded content is not empty (i.e. contains one or more DOM nodes, including whitespace text nodes), any existing
* content of this element will be removed before the transcluded content is inserted.
- * If the transcluded content is empty, the existing content is left intact. This lets you provide fallback content in the case
- * that no transcluded content is provided.
+ * If the transcluded content is empty (or only whitespace), the existing content is left intact. This lets you provide fallback
+ * content in the case that no transcluded content is provided.
*
* @element ANY
*
@@ -195,7 +195,7 @@ var ngTranscludeDirective = ['$compile', function($compile) {
}
function ngTranscludeCloneAttachFn(clone, transcludedScope) {
- if (clone.length) {
+ if (clone.length && notWhitespace(clone)) {
$element.append(clone);
} else {
useFallbackContent();
@@ -212,6 +212,15 @@ var ngTranscludeDirective = ['$compile', function($compile) {
$element.append(clone);
});
}
+
+ function notWhitespace(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ var node = nodes[i];
+ if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) {
+ return true;
+ }
+ }
+ }
};
}
};
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index 70112d14a82f..a9659524fd21 100755
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -8728,6 +8728,60 @@ describe('$compile', function() {
});
});
+ it('should compile and link the fallback content if only whitespace transcluded content is provided', function() {
+ var linkSpy = jasmine.createSpy('postlink');
+
+ module(function() {
+ directive('inner', function() {
+ return {
+ restrict: 'E',
+ template: 'old stuff! ',
+ link: linkSpy
+ };
+ });
+
+ directive('trans', function() {
+ return {
+ transclude: true,
+ template: '
'
+ };
+ });
+ });
+ inject(function(log, $rootScope, $compile) {
+ element = $compile('\n \n
')($rootScope);
+ $rootScope.$apply();
+ expect(sortedHtml(element.html())).toEqual('old stuff!
');
+ expect(linkSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('should not link the fallback content if only whitespace and comments are provided as transclude content', function() {
+ var linkSpy = jasmine.createSpy('postlink');
+
+ module(function() {
+ directive('inner', function() {
+ return {
+ restrict: 'E',
+ template: 'old stuff! ',
+ link: linkSpy
+ };
+ });
+
+ directive('trans', function() {
+ return {
+ transclude: true,
+ template: '
'
+ };
+ });
+ });
+ inject(function(log, $rootScope, $compile) {
+ element = $compile('\n \n
')($rootScope);
+ $rootScope.$apply();
+ expect(sortedHtml(element.html())).toEqual('\n \n
');
+ expect(linkSpy).not.toHaveBeenCalled();
+ });
+ });
+
it('should compile and link the fallback content if an optional transclusion slot is not provided', function() {
var linkSpy = jasmine.createSpy('postlink');
From 52bf2bd11e9070807762bad1a7ff048a67f24b75 Mon Sep 17 00:00:00 2001
From: Georgios Kalpakas
Date: Mon, 12 Sep 2016 17:30:38 +0300
Subject: [PATCH 0011/1014] refactor(ngOptions): access `copy()` directly
(`angular.copy` --> `copy`)
---
src/ng/directive/ngOptions.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js
index 9b58f9b985fd..db00e4cee7bc 100644
--- a/src/ng/directive/ngOptions.js
+++ b/src/ng/directive/ngOptions.js
@@ -397,7 +397,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
getViewValueFromOption: function(option) {
// If the viewValue could be an object that may be mutated by the application,
// we need to make a copy and not return the reference to the value on the option.
- return trackBy ? angular.copy(option.viewValue) : option.viewValue;
+ return trackBy ? copy(option.viewValue) : option.viewValue;
}
};
}
From a1bdffa12f82e838dee5492956b380df7e54cdf9 Mon Sep 17 00:00:00 2001
From: Georgios Kalpakas
Date: Mon, 12 Sep 2016 16:45:32 +0300
Subject: [PATCH 0012/1014] fix($compile): do not overwrite values set in
`$onInit()` for `<`-bound literals
See #15118 for more details.
Fixes #15118
Closes #15123
---
src/ng/compile.js | 7 +++++--
test/ng/compileSpec.js | 33 ++++++++++++++++++++++++++++++++-
2 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/src/ng/compile.js b/src/ng/compile.js
index 631ddc67cedd..1d2ae28e1b07 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -3506,18 +3506,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (optional && !attrs[attrName]) break;
parentGet = $parse(attrs[attrName]);
+ var deepWatch = parentGet.literal;
var initialValue = destination[scopeName] = parentGet(scope);
initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
if (oldValue === newValue) {
- if (oldValue === initialValue) return;
+ if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) {
+ return;
+ }
oldValue = initialValue;
}
recordChanges(scopeName, newValue, oldValue);
destination[scopeName] = newValue;
- }, parentGet.literal);
+ }, deepWatch);
removeWatchCollection.push(removeWatch);
break;
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index a9659524fd21..2784d16ccf7c 100755
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -5524,7 +5524,7 @@ describe('$compile', function() {
expect($rootScope.name).toEqual('outer');
expect(component.input).toEqual('$onInit');
- $rootScope.$apply();
+ $rootScope.$digest();
expect($rootScope.name).toEqual('outer');
expect(component.input).toEqual('$onInit');
@@ -5537,6 +5537,37 @@ describe('$compile', function() {
});
});
+ it('should not update isolate again after $onInit if outer is a literal', function() {
+ module('owComponentTest');
+ inject(function() {
+ $rootScope.name = 'outer';
+ compile(' ');
+
+ expect(component.input).toEqual('$onInit');
+
+ // No outer change
+ $rootScope.$apply('name = "outer"');
+ expect(component.input).toEqual('$onInit');
+
+ // Outer change
+ $rootScope.$apply('name = "re-outer"');
+ expect(component.input).toEqual(['re-outer']);
+
+ expect(log).toEqual([
+ 'constructor',
+ [
+ '$onChanges',
+ jasmine.objectContaining({currentValue: ['outer']})
+ ],
+ '$onInit',
+ [
+ '$onChanges',
+ jasmine.objectContaining({previousValue: ['outer'], currentValue: ['re-outer']})
+ ]
+ ]);
+ });
+ });
+
it('should update isolate again after $onInit if outer has changed (before initial watchAction call)', function() {
module('owComponentTest');
inject(function() {
From 16dccea8873b06285d4ec6eb3bb8e96ccbd3b64e Mon Sep 17 00:00:00 2001
From: thorn0
Date: Thu, 8 Sep 2016 17:54:13 +0300
Subject: [PATCH 0013/1014] fix($compile): `bindToController` should work
without `controllerAs`
Fixes #15088
Closes #15110
---
docs/content/error/$compile/noident.ngdoc | 71 -------
src/ng/compile.js | 28 +--
test/ng/compileSpec.js | 237 +++++++++-------------
3 files changed, 106 insertions(+), 230 deletions(-)
delete mode 100644 docs/content/error/$compile/noident.ngdoc
diff --git a/docs/content/error/$compile/noident.ngdoc b/docs/content/error/$compile/noident.ngdoc
deleted file mode 100644
index 9770a94585e1..000000000000
--- a/docs/content/error/$compile/noident.ngdoc
+++ /dev/null
@@ -1,71 +0,0 @@
-@ngdoc error
-@name $compile:noident
-@fullName Controller identifier is required.
-@description
-
-When using the `bindToController` feature of AngularJS, a directive is required
-to have a Controller identifier, which is initialized in scope with the value of
-the controller instance. This can be supplied using the "controllerAs" property
-of the directive object, or alternatively by adding " as IDENTIFIER" to the controller
-name.
-
-For example, the following directives are valid:
-
-```js
-// OKAY, because controller is a string with an identifier component.
-directive("okay", function() {
- return {
- bindToController: true,
- controller: "myCtrl as $ctrl",
- scope: {
- text: "@text"
- }
- };
-});
-
-
-// OKAY, because the directive uses the controllerAs property to override
-// the controller identifier.
-directive("okay2", function() {
- return {
- bindToController: true,
- controllerAs: "$ctrl",
- controller: function() {
-
- },
- scope: {
- text: "@text"
- }
- };
-});
-```
-
-While the following are invalid:
-
-```js
-// BAD, because the controller property is a string with no identifier.
-directive("bad", function() {
- return {
- bindToController: true,
- controller: "noIdentCtrl",
- scope: {
- text: "@text"
- }
- };
-});
-
-
-// BAD because the controller is not a string (therefore has no identifier),
-// and there is no controllerAs property.
-directive("bad2", function() {
- return {
- bindToController: true,
- controller: function noControllerAs() {
-
- },
- scope: {
- text: "@text"
- }
- };
-});
-```
diff --git a/src/ng/compile.js b/src/ng/compile.js
index 1d2ae28e1b07..df611d315c3d 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -361,9 +361,7 @@
*
* #### `bindToController`
* This property is used to bind scope properties directly to the controller. It can be either
- * `true` or an object hash with the same format as the `scope` property. Additionally, a controller
- * alias must be set, either by using `controllerAs: 'myAlias'` or by specifying the alias in the controller
- * definition: `controller: 'myCtrl as myAlias'`.
+ * `true` or an object hash with the same format as the `scope` property.
*
* When an isolate scope is used for a directive (see above), `bindToController: true` will
* allow a component to have its properties bound to the controller, rather than to scope.
@@ -1028,20 +1026,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
bindings.bindToController =
parseIsolateBindings(directive.bindToController, directiveName, true);
}
- if (isObject(bindings.bindToController)) {
- var controller = directive.controller;
- var controllerAs = directive.controllerAs;
- if (!controller) {
- // There is no controller, there may or may not be a controllerAs property
- throw $compileMinErr('noctrl',
- 'Cannot bind to controller without directive \'{0}\'s controller.',
- directiveName);
- } else if (!identifierForController(controller, controllerAs)) {
- // There is a controller, but no identifier or controllerAs property
- throw $compileMinErr('noident',
- 'Cannot bind to controller without identifier for directive \'{0}\'.',
- directiveName);
- }
+ if (bindings.bindToController && !directive.controller) {
+ // There is no controller
+ throw $compileMinErr('noctrl',
+ 'Cannot bind to controller without directive \'{0}\'s controller.',
+ directiveName);
}
return bindings;
}
@@ -2710,7 +2699,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var bindings = controllerDirective.$$bindings.bindToController;
if (preAssignBindingsEnabled) {
- if (controller.identifier && bindings) {
+ if (bindings) {
controller.bindingInfo =
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
} else {
@@ -3413,8 +3402,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
- // Set up $watches for isolate scope and controller bindings. This process
- // only occurs for isolate scopes and new scopes with controllerAs.
+ // Set up $watches for isolate scope and controller bindings.
function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
var removeWatchCollection = [];
var initialChanges = {};
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index 2784d16ccf7c..492741a4496a 100755
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -6205,154 +6205,113 @@ describe('$compile', function() {
});
- it('should throw noident when missing controllerAs directive property', function() {
- module(function($compileProvider) {
- $compileProvider.directive('noIdent', valueFn({
- templateUrl: 'test.html',
- scope: {
- 'data': '=dirData',
- 'oneway': '')($rootScope);
- }).toThrowMinErr('$compile', 'noident',
- 'Cannot bind to controller without identifier for directive \'noIdent\'.');
- });
- });
-
-
- it('should throw noident when missing controller identifier', function() {
- module(function($compileProvider, $controllerProvider) {
- $controllerProvider.register('myCtrl', function() {});
- $compileProvider.directive('noIdent', valueFn({
- templateUrl: 'test.html',
- scope: {
- 'data': '=dirData',
- 'oneway': '')($rootScope);
- }).toThrowMinErr('$compile', 'noident',
- 'Cannot bind to controller without identifier for directive \'noIdent\'.');
- });
- });
+ controllerAs: 'myCtrl'
+ }],
+ scopeOptions = [{
+ description: 'isolate scope',
+ scope: {}
+ }, {
+ description: 'new scope',
+ scope: true
+ }, {
+ description: 'no scope',
+ scope: false
+ }],
- it('should bind to controller via object notation (isolate scope)', function() {
- var controllerCalled = false;
- module(function($compileProvider, $controllerProvider) {
- $controllerProvider.register('myCtrl', function() {
- this.check = function() {
- expect(this.data).toEqualData({
- 'foo': 'bar',
- 'baz': 'biz'
- });
- expect(this.oneway).toEqualData({
- 'foo': 'bar',
- 'baz': 'biz'
- });
- expect(this.str).toBe('Hello, world!');
- expect(this.fn()).toBe('called!');
- };
- controllerCalled = true;
- if (preAssignBindingsEnabled) {
- this.check();
- } else {
- this.$onInit = this.check;
- }
- });
- $compileProvider.directive('fooDir', valueFn({
- templateUrl: 'test.html',
- bindToController: {
- 'data': '=dirData',
- 'oneway': 'isolate
');
- $rootScope.fn = valueFn('called!');
- $rootScope.whom = 'world';
- $rootScope.remoteData = {
- 'foo': 'bar',
- 'baz': 'biz'
- };
- element = $compile('
')($rootScope);
- $rootScope.$digest();
- expect(controllerCalled).toBe(true);
- });
- });
+ templateOptions = [{
+ description: 'inline template',
+ template: 'template
'
+ }, {
+ description: 'templateUrl setting',
+ templateUrl: 'test.html'
+ }, {
+ description: 'no template'
+ }];
+ forEach(controllerOptions, function(controllerOption) {
+ forEach(scopeOptions, function(scopeOption) {
+ forEach(templateOptions, function(templateOption) {
+
+ var description = [],
+ ddo = {
+ bindToController: {
+ 'data': '=dirData',
+ 'oneway': 'template');
+ $rootScope.fn = valueFn('called!');
+ $rootScope.whom = 'world';
+ $rootScope.remoteData = {
+ 'foo': 'bar',
+ 'baz': 'biz'
+ };
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+ expect(controllerCalled).toBe(true);
+ if (ddo.controllerAs || ddo.controller.indexOf(' as ') !== -1) {
+ if (ddo.scope) {
+ expect($rootScope.myCtrl).toBeUndefined();
+ } else {
+ // The controller identifier was added to the containing scope.
+ expect($rootScope.myCtrl).toBeDefined();
+ }
+ }
+ });
});
- expect(this.str).toBe('Hello, world!');
- expect(this.fn()).toBe('called!');
- };
- controllerCalled = true;
- if (preAssignBindingsEnabled) {
- this.check();
- } else {
- this.$onInit = this.check;
- }
+
+ });
});
- $compileProvider.directive('fooDir', valueFn({
- templateUrl: 'test.html',
- bindToController: {
- 'data': '=dirData',
- 'oneway': 'isolate');
- $rootScope.fn = valueFn('called!');
- $rootScope.whom = 'world';
- $rootScope.remoteData = {
- 'foo': 'bar',
- 'baz': 'biz'
- };
- element = $compile('
')($rootScope);
- $rootScope.$digest();
- expect(controllerCalled).toBe(true);
});
+
});
From bb8e955a02093b90567677820aefebb8b48f1c4f Mon Sep 17 00:00:00 2001
From: Stepan Suvorov
Date: Sun, 18 Sep 2016 18:57:51 +0200
Subject: [PATCH 0014/1014] docs(cacheFactory): remove `ng-include` practice
from docs
Generally we don't use `ngInclude` any more, so this commit updates the
example snippet to use component instead.
Closes #15153
---
src/ng/cacheFactory.js | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/ng/cacheFactory.js b/src/ng/cacheFactory.js
index 84e91c9883bd..3a8f5f5c7e67 100644
--- a/src/ng/cacheFactory.js
+++ b/src/ng/cacheFactory.js
@@ -385,12 +385,14 @@ function $CacheFactoryProvider() {
* });
* ```
*
- * To retrieve the template later, simply use it in your HTML:
- * ```html
- *
+ * To retrieve the template later, simply use it in your component:
+ * ```js
+ * myApp.component('myComponent', {
+ * template: 'templateId.html'
+ * });
* ```
*
- * or get it via Javascript:
+ * or get it via $templateCache service:
* ```js
* $templateCache.get('templateId.html')
* ```
From e1e2fe1c089d6cfac5b8c8517d29e6b7c431ea78 Mon Sep 17 00:00:00 2001
From: Georgios Kalpakas
Date: Mon, 19 Sep 2016 17:57:31 +0300
Subject: [PATCH 0015/1014] docs($templateCache): fix typo (template -->
templateCache)
---
src/ng/cacheFactory.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/ng/cacheFactory.js b/src/ng/cacheFactory.js
index 3a8f5f5c7e67..c4735d2738b9 100644
--- a/src/ng/cacheFactory.js
+++ b/src/ng/cacheFactory.js
@@ -388,11 +388,11 @@ function $CacheFactoryProvider() {
* To retrieve the template later, simply use it in your component:
* ```js
* myApp.component('myComponent', {
- * template: 'templateId.html'
+ * templateUrl: 'templateId.html'
* });
* ```
*
- * or get it via $templateCache service:
+ * or get it via the `$templateCache` service:
* ```js
* $templateCache.get('templateId.html')
* ```
From f1cc58c7d282d48a9b23b90f1cc5e5b2bc9f296e Mon Sep 17 00:00:00 2001
From: Matt Gilson
Date: Mon, 19 Sep 2016 08:36:25 -0700
Subject: [PATCH 0016/1014] docs(angular.toJson): add missing param type
Reference:
[JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify).
Closes #15156
---
src/Angular.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Angular.js b/src/Angular.js
index ad68d3e128d5..dced06cc28b2 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -1223,7 +1223,7 @@ function toJsonReplacer(key, value) {
* Serializes input into a JSON-formatted string. Properties with leading $$ characters will be
* stripped since angular uses this notation internally.
*
- * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON.
+ * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON.
* @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace.
* If set to an integer, the JSON output will contain that many spaces per indentation.
* @returns {string|undefined} JSON-ified string representing `obj`.
From b59bc0b01ddddbe42310d113dafe127d4e71511c Mon Sep 17 00:00:00 2001
From: davidcigital
Date: Thu, 15 Sep 2016 13:59:55 +0100
Subject: [PATCH 0017/1014] docs(ngCsp): update explanation of CSP rules and
how they affect Angular
Update the description of CSP, mainly regarding `unsafe-eval` and `unsafe-inline`. The way it was
presented previously was slightly misleading as it indicated that these were rules forbidding
certain things, when in fact it's a keyword in the CSP that disables the very rule that was
described. The updated text clarifies this better.
Closes #15142
---
src/ng/directive/ngCsp.js | 44 ++++++++++++++++++++++-----------------
1 file changed, 25 insertions(+), 19 deletions(-)
diff --git a/src/ng/directive/ngCsp.js b/src/ng/directive/ngCsp.js
index f0303ef8a844..38a6e67b2b1e 100644
--- a/src/ng/directive/ngCsp.js
+++ b/src/ng/directive/ngCsp.js
@@ -7,28 +7,34 @@
* @element html
* @description
*
- * Angular has some features that can break certain
+ * Angular has some features that can conflict with certain restrictions that are applied when using
* [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules.
*
- * If you intend to implement these rules then you must tell Angular not to use these features.
+ * If you intend to implement CSP with these rules then you must tell Angular not to use these
+ * features.
*
* This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
*
*
- * The following rules affect Angular:
+ * The following default rules in CSP affect Angular:
*
- * * `unsafe-eval`: this rule forbids apps to use `eval` or `Function(string)` generated functions
- * (among other things). Angular makes use of this in the {@link $parse} service to provide a 30%
- * increase in the speed of evaluating Angular expressions.
+ * * The use of `eval()`, `Function(string)` and similar functions to dynamically create and execute
+ * code from strings is forbidden. Angular makes use of this in the {@link $parse} service to
+ * provide a 30% increase in the speed of evaluating Angular expressions. (This CSP rule can be
+ * disabled with the CSP keyword `unsafe-eval`, but it is generally not recommended as it would
+ * weaken the protections offered by CSP.)
*
- * * `unsafe-inline`: this rule forbids apps from inject custom styles into the document. Angular
- * makes use of this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}).
- * To make these directives work when a CSP rule is blocking inline styles, you must link to the
- * `angular-csp.css` in your HTML manually.
+ * * The use of inline resources, such as inline `
+