Compare commits

...

43 Commits

Author SHA1 Message Date
Michał Gołębiowski b586bfdfab fix(testabilityPatch): fix invocations of angular.mock.dump 2014-06-30 16:58:15 -07:00
Igor Minar 1d69015e3d test($parse): skip Function.prototype.bind test on IE8 2014-06-30 11:22:41 -07:00
Igor Minar f13c33bf10 fix($parse): don't check Function.prototype.bind when it doesn't exist
e.g. IE8 doesn't have it
2014-06-30 11:15:39 -07:00
Igor Minar ba62e975f1 fix($parse): make the window check in ensureSafeObject IE8 friendly 2014-06-30 11:06:18 -07:00
rodyhaddad b89d941cdf style(parseSpec): make jshint happy 2014-06-30 10:50:02 -07:00
rodyhaddad 07fa87a8a8 fix($parse): prevent invocation of Function's bind, call and apply
BREAKING CHANGE:
You can no longer invoke .bind, .call or .apply on a function in angular expressions.
This is to disallow changing the behaviour of existing functions
in an unforseen fashion.
2014-06-30 10:43:29 -07:00
rodyhaddad 0af70eb99e refactor($parse): move around previous security changes made to $parse 2014-06-30 10:40:24 -07:00
Jann Horn cb713e6045 fix($parse): forbid __proto__ properties in angular expressions
__proto__ can be used to mess with global prototypes and it's
deprecated. Therefore, blacklisting it seems like a good idea.

BREAKING CHANGE:
The (deprecated) __proto__ propery does not work inside angular expressions
anymore.
2014-06-30 09:32:38 -07:00
Jann Horn 89ca859734 fix($parse): forbid __{define,lookup}{Getter,Setter}__ properties
It was possible to use `{}.__defineGetter__.call(null, 'alert', (0).valueOf.bind(0))` to set
`window.alert` to a false-ish value, thereby breaking the `isWindow` check, which might lead
to arbitrary code execution in browsers that let you obtain the window object using Array methods.
Prevent that by blacklisting the nasty __{define,lookup}{Getter,Setter}__ properties.

BREAKING CHANGE:
This prevents the use of __{define,lookup}{Getter,Setter}__ inside angular
expressions. If you really need them for some reason, please wrap/bind them to make them
less dangerous, then make them available through the scope object.
2014-06-30 09:29:53 -07:00
Jann Horn bc6fb7cc94 fix($parse): forbid referencing Object in angular expressions
It was possible to run arbitrary JS from inside angular expressions using the
`Object.getOwnPropertyDescriptor` method like this since commit 4ab16aaa:
    ''.sub.call.call(
      ({})["constructor"].getOwnPropertyDescriptor(''.sub.__proto__, "constructor").value,
      null,
      "alert(1)"
    )()
Fix that by blocking access to `Object` because `Object` isn't accessible
without tricks anyway and it provides some other nasty functions.

BREAKING CHANGE:
This prevents the use of `Object` inside angular expressions.
If you need Object.keys, make it accessible in the scope.
2014-06-30 09:26:29 -07:00
Kristian Hellang 0c80df21b6 fix($http): should not read statusText on IE<10 when request is aborted
Commit 1d2414c introduced a regression by retrieving the statusText
of an aborted xhr request. This breaks IE9, which throws a c00c023f
error when accessing properties of an aborted xhr request. The fix
is similar to the one in commit 6f1050d.
2014-06-30 08:09:59 -07:00
vaibhav kohli 3141dbf179 style(Angular.js): remove extra whitespace 2014-06-29 09:42:34 -07:00
Archer aad502bad8 docs(guide/$location): fix a typo
Change "window.location.path" to "window.location.pathname".

Closes #8012
2014-06-28 20:05:32 -07:00
Elnur Abdurrakhimov dca8972367 docs(Getting Started): fix typo
Closes #8015
2014-06-28 19:42:02 -07:00
rodyhaddad 284de57435 test($interval): add tests making sure $interval uses the methods from $window 2014-06-28 17:35:14 -07:00
Praveen f780ccfa1c fix($interval): when canceling, use clearInterval from $window instead of global scope.
In $interval.cancel, use clearInterval from the $window service instead of from global scope.
The variable clearInterval declared above isn't visible here.
2014-06-28 17:35:06 -07:00
Efthymis Sarbanis 55f99e0710 chore: use triple equals comparison with typeof operator.
It is common practice for typeof operator to be used with '==='.

Closes #8009
2014-06-27 16:53:32 -07:00
Domenic Denicola 1a99ca9c08 docs($q): remove unnecessary $scope.apply wrapping
As of Angular 1.2, this kind of thing is no longer necessary (thank goodness!)
2014-06-26 12:37:04 -07:00
Laurent Curau 37500fca83 docs(guide/unit-testing): correct spelling 2014-06-26 12:37:04 -07:00
rodyhaddad 4fe4fc5abf chore(ngMock): replace misplaced comma with semicolon 2014-06-26 12:32:51 -07:00
rodyhaddad 74e1cc683b fix(jqLite): change expando property to a more unique name
This was causing issue when element === window
A better strategy can be thought of later on.
2014-06-26 12:32:30 -07:00
Igor Minar a4faa5cde7 perf(jqLite): don't use reflection to access expandoId
Since we allow only one copy of Angular to be loaded at a time it doesn't
make much sense randomly generate the expando property name and then be
forced to use slow reflective calles to retrieve the IDs.
2014-06-26 12:32:30 -07:00
Eddie Hedges 32cb40b86d docs(guide/introduction): use durandal as an example of a framework
To me knockout is a library that does data binding well.
Durandal is a framework that uses knockout as it's data binding component.
2014-06-26 12:19:56 -07:00
m-tretyak d1cd677433 docs(srcset): fix mistake in example 2014-06-26 12:08:55 -07:00
thorn0 43c735a816 docs($location): hashPrefix, html5Mode are methods
Closes #7915
2014-06-25 14:48:19 -07:00
Caitlin Potter ab2e83c8c8 fix(input): improve html5 validation support
This CL improves mocking support for HTML5 validation, fixes the behaviour which invokes validators.

Previously, an input would only be revalidated if either its value changed, or if it was the empty
string but did not suffer from bad input --- now, it will be revalidated if either the value has
changed, or the value is the empty string, there is a ValidityState for the element, and that
ValidityState is being tested by one of the validators in the pipeline.

Closes #7937
Closes #7957
2014-06-24 08:35:47 -04:00
Igor Minar e5f454c8af fix(numberFilter): correctly round fractions despite floating-point arithmetics issues in JS
Closes #7870
Closes #7878
2014-06-24 00:37:50 -07:00
Igor Minar 67c11b9a39 fix($injector): check if a fn is an array explicitly
This change makes the code easier to read and also fixes a compatibility issue
with opal.js which pollutes the global state by setting $inject property on
Array prototype

Closes #7904
Closes #2653
2014-06-23 17:20:09 -07:00
Zacky Ma 5a306b7ba3 docs(guide/compiler): change {{user}} to {{user.name}} in example
If user has an `actions` property, it should be an object,
which means if you {{user}}, it'll print out the object.
2014-06-23 13:36:04 -07:00
ephigabay 8ce61bf178 docs(ngModelController): update setValidity
Needs to be `$error[validationErrorKey]!=isValid` and not
`$error[validationErrorKey]=isValid`.

See https://github.com/angular/angular.js/blob/master/src/ng/directive/input.js#L1627

Closes #7934
2014-06-23 13:21:44 -07:00
Chaker Nakhli 9d452bc845 docs(ngSrc): srcset used instead of src for img attribute
In `ngSrc` documentation `srcset` is used instead of `src` as `img` element attribute in the example.

Closes #7951
2014-06-23 13:15:03 -07:00
Julie 192fecc790 chore(grunt): check files in src for ddescribe/iit
Previously, only files in test/ were checked. This does not capture
end to end tests, which are in comments in src/.
2014-06-23 11:28:21 -07:00
Julie 32be6369e4 chore(tests): remove a lingering iit in end to end tests 2014-06-23 11:22:37 -07:00
Shahar Talmi 2a45cea0ba fix(input): escape forward slash in email regexp
This messed up with syntax coloring and variable hovering in chrome developer tools and made debugging really difficult.

Closes #7938
2014-06-22 21:28:55 -04:00
Peter Bacon Darwin ea653e4cdd docs($provide): it is a service not an object
Closes #7917
2014-06-20 17:01:10 +01:00
Alex Muntada 03777445e8 docs(tutorial/step-4): fix e2e test
After a protractor update the test syntax had to be changed.

Closes #7919
2014-06-20 14:36:13 +01:00
Neil Giarratana 8b25ea129a docs(ngPluralize): spell Mary's name correctly
Update ngPluralize.js

Just a silly change to the name of one of the examples that appears to be a typo. Changing Marry to
Mary as the first would be a verb and the latter would be an extremely common name.

Closes #7884
2014-06-17 17:44:17 -04:00
Shahar Talmi d71f16e745 fix(injector): allow multiple loading of function modules
Change HashMap to give $$hashKey also for functions so it will be possible to load multiple module
function instances. In order to prevent problem in angular's test suite,  added an option to HashMap
to maintain its own id counter and added cleanup of $$hashKey from all module functions after each
test.

Before this CL, functions were added to the HashMap via toString(), which could potentially return
the same value for different actual instances of a function. This corrects this behaviour by
ensuring that functions are mapped with hashKeys, and ensuring that hashKeys are removed from
functions and objects at the end of tests.

In addition to these changes, the injector uses its own set of UIDs in order to prevent confusingly
breaking tests which expect scopes or ng-repeated items to have specific hash keys.

Closes #7255
2014-06-16 20:45:49 -04:00
Jason Bedard ed59370d80 fix($compile): bind ng-attr-* even if unbound attribute follows ng-attr-*
Previously, <element ng-attr-foo="{{binding}}" foo="bar"></element>'s "foo" attribute would always
equal "bar", because the bound version was overwritten. This CL corrects this behaviour and ensures
that the ordering of attributes does not have an effect on whether or not ng-attr-bound attributes
do their work.
2014-06-16 20:35:13 -04:00
Ahmad Moussawi d8e5acfe27 docs($sce): update the parseAs method name 2014-06-16 13:46:02 -07:00
James Harrison Fisher af59f4e69a docs(ngMock): fix typo providers -> provides
Should be a verb, ☆.。.:・゜☆MERCI BEAUCOUP☆.。.:・゜☆
2014-06-16 11:47:45 -04:00
Colin Casey 24aee81634 refact(select): use prop to modify the select property
jQuery suggests using `prop` rather than `attr` to modify the `select` property of an element.
You can see the full list of migration warnings for jQuery:
https://github.com/jquery/jquery-migrate/blob/master/warnings.md

Closes #4107
Closes #4122
2014-06-14 17:28:12 +01:00
rodyhaddad f81d56e66c docs(CHANGELOG.md): add changes for 1.2.18 2014-06-13 14:52:28 -07:00
43 changed files with 705 additions and 192 deletions
+46
View File
@@ -1,3 +1,49 @@
<a name="1.2.18"></a>
# 1.2.18 ear-extendability (2014-06-13)
## Bug Fixes
- **$compile:**
- ensure transclude works at root of templateUrl
([fd420c40](https://github.com/angular/angular.js/commit/fd420c40613d02b3a3f7b14d00a98664518c28f0),
[#7183](https://github.com/angular/angular.js/issues/7183), [#7772](https://github.com/angular/angular.js/issues/7772))
- bound transclusion to correct scope
([1382d4e8](https://github.com/angular/angular.js/commit/1382d4e88ec486b7749e45e6ccc864b3ec388cfe))
- don't pass transcludes to non-transclude templateUrl directives
([b9ddef2a](https://github.com/angular/angular.js/commit/b9ddef2a495b44cb5fe678b8753de0b7a369244d))
- don't pass transclude to template of non-transclude directive
([eafba9e2](https://github.com/angular/angular.js/commit/eafba9e2e5ddc668c534e930d83031d2e8dc32b9))
- fix nested isolated transclude directives
([bb931097](https://github.com/angular/angular.js/commit/bb9310974b6765c2b87e74ee7b8485a6e9c24740),
[#1809](https://github.com/angular/angular.js/issues/1809), [#7499](https://github.com/angular/angular.js/issues/7499))
- pass transcludeFn down to nested transclude directives
([8df5f325](https://github.com/angular/angular.js/commit/8df5f3259aa776f28bf3d869fb1c03e10a897c84),
[#7240](https://github.com/angular/angular.js/issues/7240), [#7387](https://github.com/angular/angular.js/issues/7387))
- **$injector:** report circularity in circular dependency error message
([14e797c1](https://github.com/angular/angular.js/commit/14e797c1a10eabd15bf8e845b62213398bcc0f58),
[#7500](https://github.com/angular/angular.js/issues/7500))
- **ngResource:** don't convert literal values into Resource objects when isArray is true
([f0904cf1](https://github.com/angular/angular.js/commit/f0904cf12e4f01daa2d4fcbb20c762050125ca55),
[#6314](https://github.com/angular/angular.js/issues/6314), [#7741](https://github.com/angular/angular.js/issues/7741))
## Performance Improvements
- **$compile:** move ng-binding class stamping for interpolation into compile phase
([81b7e5ab](https://github.com/angular/angular.js/commit/81b7e5ab0ee3fea410b16b09144359ceb99f5191))
- **$http:** move xsrf cookie check to after cache check in $http
([8b86d363](https://github.com/angular/angular.js/commit/8b86d363aa252c3264201b54b57c3e34f9632d45),
[#7717](https://github.com/angular/angular.js/issues/7717))
- **isArray:** use native Array.isArray
([6c14fb1e](https://github.com/angular/angular.js/commit/6c14fb1eb61dc0a0552fbcb2ca3ace11c9a2f6a5))
- **jqLite:** cache collection length for all methods that work on a single element
([6d418ef5](https://github.com/angular/angular.js/commit/6d418ef5e3a775577996caf0709f79f447f77025))
- **ngBind:** set the ng-binding class during compilation instead of linking
([1b189027](https://github.com/angular/angular.js/commit/1b1890274e5a75553ddf9915bb23da48800275f9))
<a name="1.2.17"></a>
# 1.2.17 - quantum disentanglement (2014-06-06)
+4 -1
View File
@@ -220,8 +220,11 @@ module.exports = function(grunt) {
"ddescribe-iit": {
files: [
'src/**/*.js',
'test/**/*.js',
'!test/ngScenario/DescribeSpec.js'
'!test/ngScenario/DescribeSpec.js',
'!src/ng/directive/booleanAttrs.js', // legitimate xit here
'!src/ngScenario/**/*.js'
]
},
+17
View File
@@ -0,0 +1,17 @@
@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:
```
<div>{{user.sendInfo.call({}, true)}}</div>
```
+18 -9
View File
@@ -1,18 +1,27 @@
@ngdoc error
@name $parse:isecfld
@fullName Referencing 'constructor' Field in Expression
@fullName Referencing Disallowed Field in Expression
@description
Occurs when an expression attempts to access an objects constructor field.
Occurs when an expression attempts to access one of the following fields:
AngularJS bans constructor access from within expressions since constructor
access is a known way to execute arbitrary Javascript code.
* __proto__
* __defineGetter__
* __defineSetter__
* __lookupGetter__
* __lookupSetter__
To resolve this error, avoid constructor access. As a last resort, alias
the constructor and access it through the alias instead.
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.
Example expression that would result in this error:
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:
```
<div>{{user.constructor.name}}</div>
```
<div>{{user.__proto__.hasOwnProperty = $emit}}</div>
<div>{{user.__defineGetter__('name', noop)}}</div>
```
+1 -1
View File
@@ -60,7 +60,7 @@ changes to $location are reflected into the browser address bar.
<tr>
<td class="head">aware of docroot/context from which the application is loaded</td>
<td>no - window.location.path returns "/docroot/actual/path"</td>
<td>no - window.location.pathname returns "/docroot/actual/path"</td>
<td>yes - $location.path() returns "/actual/path"</td>
</tr>
+2 -2
View File
@@ -226,7 +226,7 @@ moved to the compile function for performance reasons.
To understand, let's look at a real-world example with `ngRepeat`:
```html
Hello {{user}}, you have these actions:
Hello {{user.name}}, you have these actions:
<ul>
<li ng-repeat="action in user.actions">
{{action.description}}
@@ -236,7 +236,7 @@ Hello {{user}}, you have these actions:
When the above example is compiled, the compiler visits every node and looks for directives.
`{{user}}` matches the {@link ng.$interpolate interpolation directive}
`{{user.name}}` matches the {@link ng.$interpolate interpolation directive}
and `ng-repeat` matches the {@link ng.directive:ngRepeat `ngRepeat` directive}.
But {@link ng.directive:ngRepeat ngRepeat} has a dilemma.
+1 -1
View File
@@ -22,7 +22,7 @@ The impedance mismatch between dynamic applications and static documents is ofte
in charge and it calls into the library when it sees fit. E.g., `jQuery`.
* **frameworks** - a particular implementation of a web application, where your code fills in
the details. The framework is in charge and it calls into your code when it needs something
app specific. E.g., `knockout`, `ember`, etc.
app specific. E.g., `durandal`, `ember`, etc.
Angular takes another approach. It attempts to minimize the impedance mismatch between document
+1 -1
View File
@@ -49,7 +49,7 @@ Out of the four options in the list above, only the last one is testable. Let's
### Using the `new` operator
While there is nothing wrong with the `new` operator fundamentally, a problem arises when calling `new`
on a constructor. This permanently binds the call site to the type. For example, lets say that we try to
on a constructor. This permanently binds the call site to the type. For example, let's say that we try to
instantiate an `XHR` that will retrieve data from the server.
```js
+1 -1
View File
@@ -10,7 +10,7 @@ becoming an Angular expert.
1. Read the {@link guide/concepts conceptual overview}.<br/>Understand Angular's vocabulary and how all the Angular
components work together.
1. Do the {@link tutorial/ AngularJS Tutorial}.<br/>Walk end-to-end through building an application complete with tests
on top of a node.js web server. Covers every major AngularJS feature and show you how to set up your development
on top of a node.js web server. Covers every major AngularJS feature and shows you how to set up your development
environment.
1. Download or clone the [Seed App project template](https://github.com/angular/angular-seed).<br/>Gives you a
starter app with a directory layout, test harness, and scripts to begin building your application.
+1 -1
View File
@@ -166,7 +166,7 @@ __`test/e2e/scenarios.js`:__
"MOTOROLA XOOM\u2122"
]);
element(by.model('orderProp')).findElement(by.css('option[value="name"]')).click();
element(by.model('orderProp')).element(by.css('option[value="name"]')).click();
expect(getNames()).toEqual([
"MOTOROLA XOOM\u2122",
+1
View File
@@ -100,6 +100,7 @@
"assertNotHasOwnProperty": false,
"getter": false,
"getBlockElements": false,
"VALIDITY_STATE_PROPERTY": false,
/* AngularPublic.js */
"version": false,
+7 -2
View File
@@ -13,6 +13,7 @@
-angularModule,
-nodeName_,
-uid,
-VALIDITY_STATE_PROPERTY,
-lowercase,
-uppercase,
@@ -102,6 +103,10 @@
* <div doc-module-components="ng"></div>
*/
// The name of a form control's ValidityState property.
// This is used so that it's possible for internal tests to create mock ValidityStates.
var VALIDITY_STATE_PROPERTY = 'validity';
/**
* @ngdoc function
* @name angular.lowercase
@@ -1274,7 +1279,7 @@ function angularInit(element, bootstrap) {
*
* Angular will detect if it has been loaded into the browser more than once and only allow the
* first loaded script to be bootstrapped and will report a warning to the browser console for
* each of the subsequent scripts. This prevents strange results in applications, where otherwise
* each of the subsequent scripts. This prevents strange results in applications, where otherwise
* multiple instances of Angular try to work on the DOM.
*
* <example name="multi-bootstrap" module="multi-bootstrap">
@@ -1404,7 +1409,7 @@ function assertArgFn(arg, name, acceptArrayAnnotation) {
}
assertArg(isFunction(arg), name, 'not a function, got ' +
(arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg));
(arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg));
return arg;
}
+13 -7
View File
@@ -13,16 +13,16 @@
* @returns {string} hash string such that the same input will have the same hash string.
* The resulting string key is in 'type:hashKey' format.
*/
function hashKey(obj) {
function hashKey(obj, nextUidFn) {
var objType = typeof obj,
key;
if (objType == 'object' && obj !== null) {
if (objType == 'function' || (objType == 'object' && obj !== null)) {
if (typeof (key = obj.$$hashKey) == 'function') {
// must invoke on object to keep the right this
key = obj.$$hashKey();
} else if (key === undefined) {
key = obj.$$hashKey = nextUid();
key = obj.$$hashKey = (nextUidFn || nextUid)();
}
} else {
key = obj;
@@ -34,7 +34,13 @@ function hashKey(obj) {
/**
* HashMap which can use objects as keys
*/
function HashMap(array){
function HashMap(array, isolatedUid) {
if (isolatedUid) {
var uid = 0;
this.nextUid = function() {
return ++uid;
};
}
forEach(array, this.put, this);
}
HashMap.prototype = {
@@ -44,7 +50,7 @@ HashMap.prototype = {
* @param value value to store can be any type
*/
put: function(key, value) {
this[hashKey(key)] = value;
this[hashKey(key, this.nextUid)] = value;
},
/**
@@ -52,7 +58,7 @@ HashMap.prototype = {
* @returns {Object} the value for the key
*/
get: function(key) {
return this[hashKey(key)];
return this[hashKey(key, this.nextUid)];
},
/**
@@ -60,7 +66,7 @@ HashMap.prototype = {
* @param key
*/
remove: function(key) {
var value = this[key = hashKey(key)];
var value = this[key = hashKey(key, this.nextUid)];
delete this[key];
return value;
}
+4 -5
View File
@@ -72,7 +72,7 @@ function annotate(fn) {
argDecl,
last;
if (typeof fn == 'function') {
if (typeof fn === 'function') {
if (!($inject = fn.$inject)) {
$inject = [];
if (fn.length) {
@@ -285,7 +285,7 @@ function annotate(fn) {
/**
* @ngdoc object
* @ngdoc service
* @name $provide
*
* @description
@@ -591,7 +591,7 @@ function createInjector(modulesToLoad) {
var INSTANTIATING = {},
providerSuffix = 'Provider',
path = [],
loadedModules = new HashMap(),
loadedModules = new HashMap([], true),
providerCache = {
$provide: {
provider: supportObject(provider),
@@ -762,8 +762,7 @@ function createInjector(modulesToLoad) {
: getService(key)
);
}
if (!fn.$inject) {
// this means that we must be an array.
if (isArray(fn)) {
fn = fn[length];
}
+6 -5
View File
@@ -98,8 +98,9 @@
* @returns {Object} jQuery object.
*/
JQLite.expando = 'ng339';
var jqCache = JQLite.cache = {},
jqName = JQLite.expando = 'ng' + new Date().getTime(),
jqId = 1,
addEventListenerFn = (window.document.addEventListener
? function(element, type, fn) {element.addEventListener(type, fn, false);}
@@ -309,7 +310,7 @@ function jqLiteOff(element, type, fn, unsupported) {
}
function jqLiteRemoveData(element, name) {
var expandoId = element[jqName],
var expandoId = element.ng339,
expandoStore = jqCache[expandoId];
if (expandoStore) {
@@ -323,17 +324,17 @@ function jqLiteRemoveData(element, name) {
jqLiteOff(element);
}
delete jqCache[expandoId];
element[jqName] = undefined; // ie does not allow deletion of attributes on elements.
element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it
}
}
function jqLiteExpandoStore(element, key, value) {
var expandoId = element[jqName],
var expandoId = element.ng339,
expandoStore = jqCache[expandoId || -1];
if (isDefined(value)) {
if (!expandoStore) {
element[jqName] = expandoId = jqNextId();
element.ng339 = expandoId = jqNextId();
expandoStore = jqCache[expandoId] = {};
}
expandoStore[key] = value;
+9 -5
View File
@@ -1010,7 +1010,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective);
// iterate over the attributes
for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes,
for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes,
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
var attrStartName = false;
var attrEndName = false;
@@ -1018,9 +1018,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
attr = nAttrs[j];
if (!msie || msie >= 8 || attr.specified) {
name = attr.name;
value = trim(attr.value);
// support ngAttr attribute binding
ngAttrName = directiveNormalize(name);
if (NG_ATTR_BINDING.test(ngAttrName)) {
if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) {
name = snake_case(ngAttrName.substr(6), '-');
}
@@ -1033,9 +1035,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nName = directiveNormalize(name.toLowerCase());
attrsMap[nName] = name;
attrs[nName] = value = trim(attr.value);
if (getBooleanAttrName(node, nName)) {
attrs[nName] = true; // presence means true
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
attrs[nName] = value;
if (getBooleanAttrName(node, nName)) {
attrs[nName] = true; // presence means true
}
}
addAttrInterpolateDirective(node, directives, value, nName);
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
+1 -1
View File
@@ -74,7 +74,7 @@ function $ControllerProvider() {
instance = $injector.instantiate(expression, locals);
if (identifier) {
if (!(locals && typeof locals.$scope == 'object')) {
if (!(locals && typeof locals.$scope === 'object')) {
throw minErr('$controller')('noscp',
"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.",
constructor || expression.name, identifier);
+30 -13
View File
@@ -9,7 +9,7 @@
*/
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i;
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i;
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
var inputType = {
@@ -435,15 +435,29 @@ function validate(ctrl, validatorName, validity, value){
return validity ? value : undefined;
}
function testFlags(validity, flags) {
var i, flag;
if (flags) {
for (i=0; i<flags.length; ++i) {
flag = flags[i];
if (validity[flag]) {
return true;
}
}
}
return false;
}
function addNativeHtml5Validators(ctrl, validatorName, element) {
var validity = element.prop('validity');
// Pass validity so that behaviour can be mocked easier.
function addNativeHtml5Validators(ctrl, validatorName, badFlags, ignoreFlags, validity) {
if (isObject(validity)) {
ctrl.$$hasNativeValidators = true;
var validator = function(value) {
// Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can
// perform the required validation)
if (!ctrl.$error[validatorName] && (validity.badInput || validity.customError ||
validity.typeMismatch) && !validity.valueMissing) {
if (!ctrl.$error[validatorName] &&
!testFlags(validity, ignoreFlags) &&
testFlags(validity, badFlags)) {
ctrl.$setValidity(validatorName, false);
return;
}
@@ -454,8 +468,9 @@ function addNativeHtml5Validators(ctrl, validatorName, element) {
}
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
var validity = element.prop('validity');
var validity = element.prop(VALIDITY_STATE_PROPERTY);
var placeholder = element[0].placeholder, noevent = {};
ctrl.$$validityState = validity;
// In composition mode, users are still inputing intermediate text buffer,
// hold the listener until composition is done.
@@ -493,11 +508,11 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
value = trim(value);
}
if (ctrl.$viewValue !== value ||
// If the value is still empty/falsy, and there is no `required` error, run validators
// again. This enables HTML5 constraint validation errors to affect Angular validation
// even when the first character entered causes an error.
(validity && value === '' && !validity.valueMissing)) {
// If a control is suffering from bad input, browsers discard its value, so it may be
// necessary to revalidate even if the control's value is the same empty value twice in
// a row.
var revalidate = validity && ctrl.$$hasNativeValidators;
if (ctrl.$viewValue !== value || (value === '' && revalidate)) {
if (scope.$$phase) {
ctrl.$setViewValue(value);
} else {
@@ -603,6 +618,8 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
}
var numberBadFlags = ['badInput'];
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
@@ -617,7 +634,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
});
addNativeHtml5Validators(ctrl, 'number', element);
addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState);
ctrl.$formatters.push(function(value) {
return ctrl.$isEmpty(value) ? '' : '' + value;
@@ -1099,7 +1116,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* This method should be called by validators - i.e. the parser or formatter functions.
*
* @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign
* to `$error[validationErrorKey]=isValid` so that it is available for data-binding.
* to `$error[validationErrorKey]=!isValid` 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}}` .
+1 -1
View File
@@ -76,7 +76,7 @@
* When one person, perhaps John, views the document, "John is viewing" will be shown.
* When three people view the document, no explicit number rule is found, so
* an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category.
* In this case, plural category 'one' is matched and "John, Marry and one other person are viewing"
* In this case, plural category 'one' is matched and "John, Mary and one other person are viewing"
* is shown.
*
* Note that when you specify offsets, you must provide explicit number rules for
+1 -1
View File
@@ -36,7 +36,7 @@
<file name="protractor.js" type="protractor">
var colorSpan = element(by.css('span'));
iit('should check ng-style', function() {
it('should check ng-style', function() {
expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)');
element(by.css('input[value=\'set color\']')).click();
expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)');
+1 -1
View File
@@ -553,7 +553,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
// rather then the element.
(element = optionTemplate.clone())
.val(option.id)
.attr('selected', option.selected)
.prop('selected', option.selected)
.text(option.label);
}
+1 -1
View File
@@ -195,7 +195,7 @@ function filterFilter() {
// jshint +W086
for (var key in expression) {
(function(path) {
if (typeof expression[path] == 'undefined') return;
if (typeof expression[path] === 'undefined') return;
predicates.push(function(value) {
return search(path == '$' ? value : (value && value[path]), expression[path]);
});
+6 -2
View File
@@ -131,6 +131,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
if (match && match[2] == '-' && match[3] > fractionSize + 1) {
numStr = '0';
number = 0;
} else {
formatedText = numStr;
hasExponent = true;
@@ -145,8 +146,11 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac);
}
var pow = Math.pow(10, fractionSize + 1);
number = Math.floor(number * pow + 5) / pow;
// safely round numbers in JS without hitting imprecisions of floating-point arithmetics
// inspired by:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize);
var fraction = ('' + number).split(DECIMAL_SEP);
var whole = fraction[0];
fraction = fraction[1] || '';
+9 -2
View File
@@ -81,7 +81,8 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
// Safari respectively.
if (xhr && xhr.readyState == 4) {
var responseHeaders = null,
response = null;
response = null,
statusText = '';
if(status !== ABORTED) {
responseHeaders = xhr.getAllResponseHeaders();
@@ -91,11 +92,17 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
response = ('response' in xhr) ? xhr.response : xhr.responseText;
}
// Accessing statusText on an aborted xhr object will
// throw an 'c00c023f error' in IE9 and lower, don't touch it.
if (!(status === ABORTED && msie < 10)) {
statusText = xhr.statusText;
}
completeRequest(callback,
status || xhr.status,
response,
responseHeaders,
xhr.statusText || '');
statusText);
}
};
+1 -1
View File
@@ -174,7 +174,7 @@ function $IntervalProvider() {
interval.cancel = function(promise) {
if (promise && promise.$$intervalId in intervals) {
intervals[promise.$$intervalId].reject('canceled');
clearInterval(promise.$$intervalId);
$window.clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
return true;
}
+2 -2
View File
@@ -551,7 +551,7 @@ function $LocationProvider(){
html5Mode = false;
/**
* @ngdoc property
* @ngdoc method
* @name $locationProvider#hashPrefix
* @description
* @param {string=} prefix Prefix for hash part (containing path and search)
@@ -567,7 +567,7 @@ function $LocationProvider(){
};
/**
* @ngdoc property
* @ngdoc method
* @name $locationProvider#html5Mode
* @description
* @param {boolean=} mode Use HTML5 strategy if available.
+36 -15
View File
@@ -12,14 +12,7 @@ var promiseWarning;
//
// As an example, consider the following Angular expression:
//
// {}.toString.constructor(alert("evil JS code"))
//
// We want to prevent this type of access. For the sake of performance, during the lexing phase we
// disallow any "dotted" access to any member named "constructor".
//
// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor
// while evaluating the expression, which is a stronger but more expensive test. Since reflective
// calls are expensive anyway, this is not such a big deal compared to static dereferencing.
// {}.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
@@ -27,17 +20,19 @@ var promiseWarning;
// practice and therefore we are not even trying to protect against interaction with an object
// explicitly exposed in this way.
//
// A developer could foil the name check by aliasing the Function constructor under a different
// name on the scope.
//
// 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.
function ensureSafeMemberName(name, fullExpression) {
if (name === "constructor") {
if (name === "__defineGetter__" || name === "__defineSetter__"
|| name === "__lookupGetter__" || name === "__lookupSetter__"
|| name === "__proto__") {
throw $parseMinErr('isecfld',
'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}',
fullExpression);
'Attempting to access a disallowed field in Angular expressions! '
+'Expression: {0}', fullExpression);
}
return name;
}
@@ -59,11 +54,34 @@ function ensureSafeObject(obj, fullExpression) {
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;
}
var CALL = Function.prototype.call;
var APPLY = Function.prototype.apply;
var BIND = Function.prototype.bind;
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 || (BIND && obj === BIND)) {
throw $parseMinErr('isecff',
'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}',
fullExpression);
}
}
}
var OPERATORS = {
/* jshint bitwise : false */
'null':function(){return null;},
@@ -698,6 +716,7 @@ Parser.prototype = {
i = indexFn(self, locals),
v, p;
ensureSafeMemberName(i, parser.text);
if (!o) return undefined;
v = ensureSafeObject(o[i], parser.text);
if (v && v.then && parser.options.unwrapPromises) {
@@ -740,7 +759,7 @@ Parser.prototype = {
var fnPtr = fn(scope, locals, context) || noop;
ensureSafeObject(context, parser.text);
ensureSafeObject(fnPtr, parser.text);
ensureSafeFunction(fnPtr, parser.text);
// IE stupidity! (IE doesn't have apply for some native functions)
var v = fnPtr.apply
@@ -849,6 +868,8 @@ function setter(obj, path, setValue, fullExp, options) {
}
}
key = ensureSafeMemberName(element.shift(), fullExp);
ensureSafeObject(obj, fullExp);
ensureSafeObject(obj[key], fullExp);
obj[key] = setValue;
return setValue;
}
+6 -10
View File
@@ -23,17 +23,13 @@
* var deferred = $q.defer();
*
* setTimeout(function() {
* // since this fn executes async in a future turn of the event loop, we need to wrap
* // our code into an $apply call so that the model changes are properly observed.
* scope.$apply(function() {
* deferred.notify('About to greet ' + name + '.');
* deferred.notify('About to greet ' + name + '.');
*
* if (okToGreet(name)) {
* deferred.resolve('Hello, ' + name + '!');
* } else {
* deferred.reject('Greeting ' + name + ' is not allowed.');
* }
* });
* if (okToGreet(name)) {
* deferred.resolve('Hello, ' + name + '!');
* } else {
* deferred.reject('Greeting ' + name + ' is not allowed.');
* }
* }, 1000);
*
* return deferred.promise;
+1 -1
View File
@@ -639,7 +639,7 @@ function $RootScopeProvider(){
if ((value = watch.get(current)) !== (last = watch.last) &&
!(watch.eq
? equals(value, last)
: (typeof value == 'number' && typeof last == 'number'
: (typeof value === 'number' && typeof last === 'number'
&& isNaN(value) && isNaN(last)))) {
dirty = true;
lastDirtyWatch = watch;
+1 -1
View File
@@ -764,7 +764,7 @@ function $SceProvider() {
/**
* @ngdoc method
* @name $sce#parse
* @name $sce#parseAs
*
* @description
* Converts Angular {@link guide/expression expression} into a function. This is like {@link
+8 -2
View File
@@ -455,7 +455,7 @@ angular.mock.$IntervalProvider = function() {
iteration = 0,
skipApply = (angular.isDefined(invokeApply) && !invokeApply);
count = (angular.isDefined(count)) ? count : 0,
count = (angular.isDefined(count)) ? count : 0;
promise.then(null, null, fn);
promise.$$intervalId = nextRepeatId;
@@ -1719,7 +1719,7 @@ angular.mock.$RootElementProvider = function() {
*
* # ngMock
*
* The `ngMock` module providers support to inject and mock Angular services into unit tests.
* The `ngMock` module provides support to inject and mock Angular services into unit tests.
* In addition, ngMock also extends various core ng services such that they can be
* inspected and controlled in a synchronous manner within test code.
*
@@ -1958,6 +1958,12 @@ if(window.jasmine || window.mocha) {
(window.afterEach || window.teardown)(function() {
var injector = currentSpec.$injector;
angular.forEach(currentSpec.$modules, function(module) {
if (module && module.$$hashKey) {
module.$$hashKey = undefined;
}
});
currentSpec.$injector = null;
currentSpec.$modules = null;
currentSpec = null;
+1 -1
View File
@@ -294,7 +294,7 @@ _jQuery.fn.bindings = function(windowJquery, bindExp) {
function push(value) {
if (value === undefined) {
value = '';
} else if (typeof value != 'string') {
} else if (typeof value !== 'string') {
value = angular.toJson(value);
}
result.push('' + value);
+1
View File
@@ -101,6 +101,7 @@
"assertNotHasOwnProperty": false,
"getter": false,
"getBlockElements": false,
"VALIDITY_STATE_PROPERTY": true,
/* filters.js */
"getFirstThursdayOfYear": false,
+30
View File
@@ -22,6 +22,36 @@ describe('api', function() {
expect(map.get('b')).toBe(1);
expect(map.get('c')).toBe(undefined);
});
it('should maintain hashKey for object keys', function() {
var map = new HashMap();
var key = {};
map.get(key);
expect(key.$$hashKey).toBeDefined();
});
it('should maintain hashKey for function keys', function() {
var map = new HashMap();
var key = function() {};
map.get(key);
expect(key.$$hashKey).toBeDefined();
});
it('should share hashKey between HashMap by default', function() {
var map1 = new HashMap(), map2 = new HashMap();
var key1 = {}, key2 = {};
map1.get(key1);
map2.get(key2);
expect(key1.$$hashKey).not.toEqual(key2.$$hashKey);
});
it('should maintain hashKey per HashMap if flag is passed', function() {
var map1 = new HashMap([], true), map2 = new HashMap([], true);
var key1 = {}, key2 = {};
map1.get(key1);
map2.get(key2);
expect(key1.$$hashKey).toEqual(key2.$$hashKey);
});
});
});
+23
View File
@@ -293,6 +293,29 @@ describe('injector', function() {
expect(log).toEqual('abc');
});
it('should load different instances of dependent functions', function() {
function generateValueModule(name, value) {
return function ($provide) {
$provide.value(name, value);
};
}
var injector = createInjector([generateValueModule('name1', 'value1'),
generateValueModule('name2', 'value2')]);
expect(injector.get('name2')).toBe('value2');
});
it('should load same instance of dependent function only once', function() {
var count = 0;
function valueModule($provide) {
count++;
$provide.value('name', 'value');
}
var injector = createInjector([valueModule, valueModule]);
expect(injector.get('name')).toBe('value');
expect(count).toBe(1);
});
it('should execute runBlocks after injector creation', function() {
var log = '';
angular.module('a', [], function(){ log += 'a'; }).run(function() { log += 'A'; });
+2 -2
View File
@@ -248,13 +248,13 @@ function isCssVisible(node) {
function assertHidden(node) {
if (isCssVisible(node)) {
throw new Error('Node should be hidden but was visible: ' + angular.module.ngMock.dump(node));
throw new Error('Node should be hidden but was visible: ' + angular.mock.dump(node));
}
}
function assertVisible(node) {
if (!isCssVisible(node)) {
throw new Error('Node should be visible but was hidden: ' + angular.module.ngMock.dump(node));
throw new Error('Node should be visible but was hidden: ' + angular.mock.dump(node));
}
}
+77
View File
@@ -4901,6 +4901,83 @@ describe('$compile', function() {
expect(element.attr('test')).toBe('Misko');
}));
it('should bind after digest but not before when after overridden attribute', inject(function($compile, $rootScope) {
$rootScope.name = "Misko";
element = $compile('<span test="123" ng-attr-test="{{name}}"></span>')($rootScope);
expect(element.attr('test')).toBe('123');
$rootScope.$digest();
expect(element.attr('test')).toBe('Misko');
}));
it('should bind after digest but not before when before overridden attribute', inject(function($compile, $rootScope) {
$rootScope.name = "Misko";
element = $compile('<span ng-attr-test="{{name}}" test="123"></span>')($rootScope);
expect(element.attr('test')).toBe('123');
$rootScope.$digest();
expect(element.attr('test')).toBe('Misko');
}));
describe('in directive', function() {
beforeEach(module(function() {
directive('syncTest', function(log) {
return {
link: {
pre: function(s, e, attr) { log(attr.test); },
post: function(s, e, attr) { log(attr.test); }
}
};
});
directive('asyncTest', function(log) {
return {
templateUrl: 'async.html',
link: {
pre: function(s, e, attr) { log(attr.test); },
post: function(s, e, attr) { log(attr.test); }
}
};
});
}));
beforeEach(inject(function($templateCache) {
$templateCache.put('async.html', '<h1>Test</h1>');
}));
it('should provide post-digest value in synchronous directive link functions when after overridden attribute',
inject(function(log, $rootScope, $compile) {
$rootScope.test = "TEST";
element = $compile('<div sync-test test="123" ng-attr-test="{{test}}"></div>')($rootScope);
expect(element.attr('test')).toBe('123');
expect(log.toArray()).toEqual(['TEST', 'TEST']);
}));
it('should provide post-digest value in synchronous directive link functions when before overridden attribute',
inject(function(log, $rootScope, $compile) {
$rootScope.test = "TEST";
element = $compile('<div sync-test ng-attr-test="{{test}}" test="123"></div>')($rootScope);
expect(element.attr('test')).toBe('123');
expect(log.toArray()).toEqual(['TEST', 'TEST']);
}));
it('should provide post-digest value in asynchronous directive link functions when after overridden attribute',
inject(function(log, $rootScope, $compile) {
$rootScope.test = "TEST";
element = $compile('<div async-test test="123" ng-attr-test="{{test}}"></div>')($rootScope);
expect(element.attr('test')).toBe('123');
$rootScope.$digest();
expect(log.toArray()).toEqual(['TEST', 'TEST']);
}));
it('should provide post-digest value in asynchronous directive link functions when before overridden attribute',
inject(function(log, $rootScope, $compile) {
$rootScope.test = "TEST";
element = $compile('<div async-test ng-attr-test="{{test}}" test="123"></div>')($rootScope);
expect(element.attr('test')).toBe('123');
$rootScope.$digest();
expect(log.toArray()).toEqual(['TEST', 'TEST']);
}));
});
it('should work with different prefixes', inject(function($compile, $rootScope) {
$rootScope.name = "Misko";
+47 -2
View File
@@ -410,15 +410,33 @@ describe('ngModel', function() {
describe('input', function() {
var formElm, inputElm, scope, $compile, $sniffer, $browser, changeInputValueTo;
var formElm, inputElm, scope, $compile, $sniffer, $browser, changeInputValueTo, currentSpec;
function compileInput(inputHtml) {
function compileInput(inputHtml, mockValidity) {
inputElm = jqLite(inputHtml);
if (isObject(mockValidity)) {
VALIDITY_STATE_PROPERTY = 'ngMockValidity';
inputElm.prop(VALIDITY_STATE_PROPERTY, mockValidity);
currentSpec.after(function() {
VALIDITY_STATE_PROPERTY = 'validity';
});
}
formElm = jqLite('<form name="form"></form>');
formElm.append(inputElm);
$compile(formElm)(scope);
}
var attrs;
beforeEach(function() { currentSpec = this; });
afterEach(function() { currentSpec = null; });
beforeEach(module(function($compileProvider) {
$compileProvider.directive('attrCapture', function() {
return function(scope, element, $attrs) {
attrs = $attrs;
};
});
}));
beforeEach(inject(function($injector, _$sniffer_, _$browser_) {
$sniffer = _$sniffer_;
$browser = _$browser_;
@@ -844,6 +862,33 @@ describe('input', function() {
});
it('should invalidate number if suffering from bad input', function() {
compileInput('<input type="number" ng-model="age" />', {
valid: false,
badInput: true
});
changeInputValueTo('10a');
expect(scope.age).toBeUndefined();
expect(inputElm).toBeInvalid();
});
it('should validate number if transition from bad input to empty string', function() {
var validity = {
valid: false,
badInput: true
};
compileInput('<input type="number" ng-model="age" />', validity);
changeInputValueTo('10a');
validity.badInput = false;
validity.valid = true;
changeInputValueTo('');
expect(scope.age).toBeNull();
expect(inputElm).toBeValid();
});
describe('min', function() {
it('should validate', function() {
+5
View File
@@ -145,6 +145,10 @@ describe('filters', function() {
expect(number(.99, 2)).toEqual("0.99");
expect(number(.999, 3)).toEqual("0.999");
expect(number(.9999, 3)).toEqual("1.000");
expect(number(1.9, 2)).toEqual("1.90");
expect(number(1.99, 2)).toEqual("1.99");
expect(number(1.999, 3)).toEqual("1.999");
expect(number(1.9999, 3)).toEqual("2.000");
expect(number(1234.567, 0)).toEqual("1,235");
expect(number(1234.567, 1)).toEqual("1,234.6");
expect(number(1234.567, 2)).toEqual("1,234.57");
@@ -152,6 +156,7 @@ describe('filters', function() {
expect(number(1.255, 1)).toEqual("1.3");
expect(number(1.255, 2)).toEqual("1.26");
expect(number(1.255, 3)).toEqual("1.255");
expect(number(0, 8)).toEqual("0.00000000");
});
it('should filter exponentially large numbers', function() {
+19
View File
@@ -98,6 +98,25 @@ describe('$httpBackend', function() {
expect(callback).toHaveBeenCalledOnce();
});
it('should not touch xhr.statusText when request is aborted on IE9 or lower', function() {
callback.andCallFake(function(status, response, headers, statusText) {
expect(statusText).toBe((!msie || msie >= 10) ? 'OK' : '');
});
$backend('GET', '/url', null, callback, {}, 2000);
xhr = MockXhr.$$lastInstance;
spyOn(xhr, 'abort');
fakeTimeout.flush();
expect(xhr.abort).toHaveBeenCalledOnce();
xhr.status = 0;
xhr.readyState = 4;
xhr.statusText = 'OK';
xhr.onreadystatechange();
expect(callback).toHaveBeenCalledOnce();
});
it('should call completion function with empty string if not present', function() {
callback.andCallFake(function(status, response, headers, statusText) {
expect(statusText).toBe('');
+21
View File
@@ -267,4 +267,25 @@ describe('$interval', function() {
expect($interval.cancel()).toBe(false);
}));
});
describe('$window delegation', function() {
it('should use $window.setInterval instead of the global function', inject(function ($interval, $window) {
var setIntervalSpy = spyOn($window, 'setInterval');
$interval(noop, 1000);
expect(setIntervalSpy).toHaveBeenCalled();
}));
it('should use $window.clearInterval instead of the global function', inject(function ($interval, $window) {
var clearIntervalSpy = spyOn($window, 'clearInterval');
$interval(noop, 1000, 1);
$window.flush(1000);
expect(clearIntervalSpy).toHaveBeenCalled();
clearIntervalSpy.reset();
$interval.cancel($interval(noop, 1000));
expect(clearIntervalSpy).toHaveBeenCalled();
}));
});
});
+225 -92
View File
@@ -637,90 +637,52 @@ describe('parser', function() {
expect(scope.$eval('a + \n b.c + \r "\td" + \t \r\n\r "\r\n\n"')).toEqual("abc\td\r\n\n");
});
describe('sandboxing', function() {
describe('Function constructor', function() {
it('should NOT allow access to Function constructor in getter', function() {
expect(function() {
scope.$eval('{}.toString.constructor');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: {}.toString.constructor');
expect(function() {
scope.$eval('{}.toString.constructor("alert(1)")');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: {}.toString.constructor("alert(1)")');
expect(function() {
scope.$eval('[].toString.constructor.foo');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: [].toString.constructor.foo');
expect(function() {
scope.$eval('{}.toString["constructor"]');
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: {}.toString["constructor"]');
expect(function() {
scope.$eval('{}["toString"]["constructor"]');
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: {}["toString"]["constructor"]');
scope.a = [];
expect(function() {
scope.$eval('a.toString.constructor', scope);
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: a.toString.constructor');
expect(function() {
scope.$eval('a.toString["constructor"]', scope);
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: a.toString["constructor"]');
});
it('should NOT allow access to Function constructor in setter', function() {
expect(function() {
scope.$eval('{}.toString.constructor = 1');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: {}.toString.constructor = 1');
expect(function() {
scope.$eval('{}.toString.constructor.a = 1');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'$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! ' +
'$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');
'$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! ' +
'$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', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: a.toString.constructor = 1');
});
@@ -730,21 +692,84 @@ describe('parser', function() {
expect(function() {
scope.$eval('foo["bar"]');
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: foo["bar"]');
});
it('should NOT allow access to Function constructor in getter', function() {
expect(function() {
scope.$eval('{}.toString.constructor');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: {}.toString.constructor');
});
});
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()');
});
// IE8 doesn't have Function.prototype.bind
if (!msie || msie > 8) {
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', 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)');
});
});
describe('Window and $element/node', function() {
it('should NOT allow access to the Window or DOM when indexing', inject(function($window, $document) {
@@ -753,15 +778,16 @@ describe('parser', function() {
expect(function() {
scope.$eval('wrap["w"]', scope);
}).toThrowMinErr(
'$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' +
'$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 ' +
'$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' +
'disallowed! Expression: wrap["d"]');
}));
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);
@@ -769,12 +795,12 @@ describe('parser', function() {
expect(function() {
scope.$eval('getWin()', scope);
}).toThrowMinErr(
'$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' +
'$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 ' +
'$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' +
'disallowed! Expression: getDoc()');
}));
@@ -783,12 +809,12 @@ describe('parser', function() {
expect(function() {
scope.$eval('a.b.win.alert(1)', scope);
}).toThrowMinErr(
'$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' +
'$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 ' +
'$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' +
'disallowed! Expression: a.b.doc.on("click")');
}));
@@ -814,39 +840,145 @@ describe('parser', function() {
expect(function() { scope.$eval('array'); }).not.toThrow();
});
});
});
describe('overriding constructor', function() {
it('should evaluate grouped expressions', function() {
scope.foo = function foo() {
return "foo";
};
// When not overridden, access should be restricted both by the dot operator and by the
// index operator.
expect(function() {
scope.$eval('foo.constructor()', scope)
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: foo.constructor()');
expect(function() {
scope.$eval('foo["constructor"]()', scope)
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: foo["constructor"]()');
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');
// User defined value assigned to constructor.
scope.foo.constructor = function constructor() {
return "custom constructor";
};
// Dot operator should still block it.
expect(function() {
scope.$eval('foo.constructor()', scope)
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: foo.constructor()');
// However, the index operator should allow it.
expect(scope.$eval('foo["constructor"]()', scope)).toBe('custom constructor');
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__.foo = 1');
}).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}["__proto__"]');
}).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}["__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('' +
' "".sub.call.call(' +
'({})["constructor"].getOwnPropertyDescriptor("".sub.__proto__, "constructor").value,' +
'null,' +
'"alert(1)"' +
')()' +
'')
}).toThrow();
})
});
it('should call the function from the received instance and not from a new one', function() {
@@ -1005,6 +1137,7 @@ describe('parser', function() {
}));
});
describe('constant', function() {
it('should mark scalar value expressions as constant', inject(function($parse) {
expect($parse('12.3').constant).toBe(true);
+17
View File
@@ -805,6 +805,23 @@ describe('ngMock', function() {
expect(example).toEqual('win');
});
});
describe('module cleanup', function() {
function testFn() {
}
it('should add hashKey to module function', function() {
module(testFn);
inject(function () {
expect(testFn.$$hashKey).toBeDefined();
});
});
it('should cleanup hashKey after previous test', function() {
expect(testFn.$$hashKey).toBeUndefined();
});
});
});
describe('in DSL', function() {