feat($parse): CSP compatibility

CSP (content security policy) forbids apps to use eval or
Function(string) generated functions (among other things). For us to be
compatible, we just need to implement the "getterFn" in $parse without
violating any of these restrictions.

We currently use Function(string) generated functions as a speed
optimization. With this change, it will be possible to opt into the CSP
compatible mode using the ngCsp directive. When this mode is on Angular
will evaluate all expressions up to 30% slower than in non-CSP mode, but
no security violations will be raised.

In order to use this feature put ngCsp directive on the root element of
the application. For example:

<!doctype html>
<html ng-app ng-csp>
  ...
  ...
</html>

Closes #893
This commit is contained in:
Igor Minar 2012-04-27 15:20:54 -07:00
parent 2b1b257034
commit 2b87c814ab
7 changed files with 569 additions and 422 deletions

1
angularFiles.js vendored
View file

@ -45,6 +45,7 @@ angularFiles = {
'src/ng/directive/ngClass.js',
'src/ng/directive/ngCloak.js',
'src/ng/directive/ngController.js',
'src/ng/directive/ngCsp.js',
'src/ng/directive/ngEventDirs.js',
'src/ng/directive/ngInclude.js',
'src/ng/directive/ngInit.js',

View file

@ -76,6 +76,7 @@ function publishExternalAPI(angular){
ngClass: ngClassDirective,
ngClassEven: ngClassEvenDirective,
ngClassOdd: ngClassOddDirective,
ngCsp: ngCspDirective,
ngCloak: ngCloakDirective,
ngController: ngControllerDirective,
ngForm: ngFormDirective,

26
src/ng/directive/ngCsp.js Normal file
View file

@ -0,0 +1,26 @@
'use strict';
/**
* TODO(i): this directive is not publicly documented until we know for sure that CSP can't be
* safely feature-detected.
*
* @name angular.module.ng.$compileProvider.directive.ngCsp
* @priority 1000
*
* @description
* Enables CSP (Content Security Protection) support. This directive should be used on the `<html>`
* element before any kind of interpolation or expression is processed.
*
* If enabled the performance of $parse will suffer.
*
* @element html
*/
var ngCspDirective = ['$sniffer', function($sniffer) {
return {
priority: 1000,
compile: function() {
$sniffer.csp = true;
}
};
}];

View file

@ -27,7 +27,7 @@ var OPERATORS = {
};
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
function lex(text){
function lex(text, csp){
var tokens = [],
token,
index = 0,
@ -187,7 +187,7 @@ function lex(text){
if (OPERATORS.hasOwnProperty(ident)) {
token.fn = token.json = OPERATORS[ident];
} else {
var getter = getterFn(ident);
var getter = getterFn(ident, csp);
token.fn = extend(function(self, locals) {
return (getter(self, locals));
}, {
@ -261,10 +261,10 @@ function lex(text){
/////////////////////////////////////////
function parser(text, json, $filter){
function parser(text, json, $filter, csp){
var ZERO = valueFn(0),
value,
tokens = lex(text),
tokens = lex(text, csp),
assignment = _assignment,
functionCall = _functionCall,
fieldAccess = _fieldAccess,
@ -532,7 +532,7 @@ function parser(text, json, $filter){
function _fieldAccess(object) {
var field = expect().text;
var getter = getterFn(field);
var getter = getterFn(field, csp);
return extend(
function(self, locals) {
return getter(object(self, locals), locals);
@ -685,32 +685,119 @@ function getter(obj, path, bindFnToScope) {
var getterFnCache = {};
function getterFn(path) {
/**
* Implementation of the "Black Hole" variant from:
* - http://jsperf.com/angularjs-parse-getter/4
* - http://jsperf.com/path-evaluation-simplified/7
*/
function cspSafeGetterFn(key0, key1, key2, key3, key4) {
return function(scope, locals) {
var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope,
promise;
if (!pathVal) return pathVal;
pathVal = pathVal[key0];
if (pathVal && pathVal.then) {
if (!("$$v" in pathVal)) {
promise = pathVal;
promise.$$v = undefined;
promise.then(function(val) { promise.$$v = val; });
}
pathVal = pathVal.$$v;
}
if (!key1 || !pathVal) return pathVal;
pathVal = pathVal[key1];
if (pathVal && pathVal.then) {
if (!("$$v" in pathVal)) {
promise = pathVal;
promise.$$v = undefined;
promise.then(function(val) { promise.$$v = val; });
}
pathVal = pathVal.$$v;
}
if (!key2 || !pathVal) return pathVal;
pathVal = pathVal[key2];
if (pathVal && pathVal.then) {
if (!("$$v" in pathVal)) {
promise = pathVal;
promise.$$v = undefined;
promise.then(function(val) { promise.$$v = val; });
}
pathVal = pathVal.$$v;
}
if (!key3 || !pathVal) return pathVal;
pathVal = pathVal[key3];
if (pathVal && pathVal.then) {
if (!("$$v" in pathVal)) {
promise = pathVal;
promise.$$v = undefined;
promise.then(function(val) { promise.$$v = val; });
}
pathVal = pathVal.$$v;
}
if (!key4 || !pathVal) return pathVal;
pathVal = pathVal[key4];
if (pathVal && pathVal.then) {
if (!("$$v" in pathVal)) {
promise = pathVal;
promise.$$v = undefined;
promise.then(function(val) { promise.$$v = val; });
}
pathVal = pathVal.$$v;
}
return pathVal;
};
};
function getterFn(path, csp) {
if (getterFnCache.hasOwnProperty(path)) {
return getterFnCache[path];
}
var fn, code = 'var l, fn, p;\n';
forEach(path.split('.'), function(key, index) {
code += 'if(!s) return s;\n' +
'l=s;\n' +
's='+ (index
// we simply direference 's' on any .dot notation
? 's'
// but if we are first then we check locals firs, and if so read it first
: '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' +
'if (s && s.then) {\n' +
' if (!("$$v" in s)) {\n' +
' p=s;\n' +
' p.$$v = undefined;\n' +
' p.then(function(v) {p.$$v=v;});\n' +
'}\n' +
' s=s.$$v\n' +
'}\n';
});
code += 'return s;';
fn = Function('s', 'k', code);
fn.toString = function() { return code; };
var pathKeys = path.split('.'),
pathKeysLength = pathKeys.length,
fn;
if (csp) {
fn = (pathKeysLength < 6)
? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4])
: function(scope, locals) {
var i = 0, val;
do {
val = cspSafeGetterFn(
pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++]
)(scope, locals);
locals = undefined; // clear after first iteration
} while (i < pathKeysLength);
};
} else {
var code = 'var l, fn, p;\n';
forEach(pathKeys, function(key, index) {
code += 'if(!s) return s;\n' +
'l=s;\n' +
's='+ (index
// we simply dereference 's' on any .dot notation
? 's'
// but if we are first then we check locals first, and if so read it first
: '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' +
'if (s && s.then) {\n' +
' if (!("$$v" in s)) {\n' +
' p=s;\n' +
' p.$$v = undefined;\n' +
' p.then(function(v) {p.$$v=v;});\n' +
'}\n' +
' s=s.$$v\n' +
'}\n';
});
code += 'return s;';
fn = Function('s', 'k', code); // s=scope, k=locals
fn.toString = function() { return code; };
}
return getterFnCache[path] = fn;
}
@ -719,13 +806,13 @@ function getterFn(path) {
function $ParseProvider() {
var cache = {};
this.$get = ['$filter', function($filter) {
this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
return function(exp) {
switch(typeof exp) {
case 'string':
return cache.hasOwnProperty(exp)
? cache[exp]
: cache[exp] = parser(exp, false, $filter);
: cache[exp] = parser(exp, false, $filter, $sniffer.csp);
case 'function':
return exp;
default:

View file

@ -28,7 +28,9 @@ function $SnifferProvider() {
}
return eventSupport[event];
}
},
// TODO(i): currently there is no way to feature detect CSP without triggering alerts
csp: false
};
}];
}

View file

@ -0,0 +1,10 @@
'use strict';
describe('ngCsp', function() {
it('it should turn on CSP mode in $sniffer', inject(function($sniffer, $compile) {
expect($sniffer.csp).toBe(false);
$compile('<div ng-csp></div>');
expect($sniffer.csp).toBe(true);
}));
});

View file

@ -165,467 +165,487 @@ describe('parser', function() {
});
});
var scope, $filterProvider;
var $filterProvider, scope;
beforeEach(module(['$filterProvider', function (filterProvider) {
$filterProvider = filterProvider;
}]));
beforeEach(inject(function ($rootScope) {
scope = $rootScope;
}));
it('should parse expressions', function() {
expect(scope.$eval("-1")).toEqual(-1);
expect(scope.$eval("1 + 2.5")).toEqual(3.5);
expect(scope.$eval("1 + -2.5")).toEqual(-1.5);
expect(scope.$eval("1+2*3/4")).toEqual(1+2*3/4);
expect(scope.$eval("0--1+1.5")).toEqual(0- -1 + 1.5);
expect(scope.$eval("-0--1++2*-3/-4")).toEqual(-0- -1+ +2*-3/-4);
expect(scope.$eval("1/2*3")).toEqual(1/2*3);
});
it('should parse comparison', function() {
expect(scope.$eval("false")).toBeFalsy();
expect(scope.$eval("!true")).toBeFalsy();
expect(scope.$eval("1==1")).toBeTruthy();
expect(scope.$eval("1!=2")).toBeTruthy();
expect(scope.$eval("1<2")).toBeTruthy();
expect(scope.$eval("1<=1")).toBeTruthy();
expect(scope.$eval("1>2")).toEqual(1>2);
expect(scope.$eval("2>=1")).toEqual(2>=1);
expect(scope.$eval("true==2<3")).toEqual(true === 2<3);
});
forEach([true, false], function(cspEnabled) {
it('should parse logical', function() {
expect(scope.$eval("0&&2")).toEqual(0&&2);
expect(scope.$eval("0||2")).toEqual(0||2);
expect(scope.$eval("0||1&&2")).toEqual(0||1&&2);
});
it('should parse string', function() {
expect(scope.$eval("'a' + 'b c'")).toEqual("ab c");
});
it('should parse filters', function() {
$filterProvider.register('substring', valueFn(function(input, start, end) {
return input.substring(start, end);
beforeEach(inject(function ($rootScope, $sniffer) {
scope = $rootScope;
$sniffer.csp = cspEnabled;
}));
expect(function() {
scope.$eval("1|nonexistent");
}).toThrow(new Error("Unknown provider: nonexistentFilterProvider <- nonexistentFilter"));
scope.offset = 3;
expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc");
expect(scope.$eval("'abcd'|substring:1:3|uppercase")).toEqual("BC");
});
it('should parse expressions', function() {
expect(scope.$eval("-1")).toEqual(-1);
expect(scope.$eval("1 + 2.5")).toEqual(3.5);
expect(scope.$eval("1 + -2.5")).toEqual(-1.5);
expect(scope.$eval("1+2*3/4")).toEqual(1+2*3/4);
expect(scope.$eval("0--1+1.5")).toEqual(0- -1 + 1.5);
expect(scope.$eval("-0--1++2*-3/-4")).toEqual(-0- -1+ +2*-3/-4);
expect(scope.$eval("1/2*3")).toEqual(1/2*3);
});
it('should access scope', function() {
scope.a = 123;
scope.b = {c: 456};
expect(scope.$eval("a", scope)).toEqual(123);
expect(scope.$eval("b.c", scope)).toEqual(456);
expect(scope.$eval("x.y.z", scope)).not.toBeDefined();
});
it('should parse comparison', function() {
expect(scope.$eval("false")).toBeFalsy();
expect(scope.$eval("!true")).toBeFalsy();
expect(scope.$eval("1==1")).toBeTruthy();
expect(scope.$eval("1!=2")).toBeTruthy();
expect(scope.$eval("1<2")).toBeTruthy();
expect(scope.$eval("1<=1")).toBeTruthy();
expect(scope.$eval("1>2")).toEqual(1>2);
expect(scope.$eval("2>=1")).toEqual(2>=1);
expect(scope.$eval("true==2<3")).toEqual(true === 2<3);
});
it('should support property names that colide with native object properties', function() {
// regression
scope.watch = 1;
scope.constructor = 2;
scope.toString = 3;
it('should parse logical', function() {
expect(scope.$eval("0&&2")).toEqual(0&&2);
expect(scope.$eval("0||2")).toEqual(0||2);
expect(scope.$eval("0||1&&2")).toEqual(0||1&&2);
});
expect(scope.$eval('watch', scope)).toBe(1);
expect(scope.$eval('constructor', scope)).toBe(2);
expect(scope.$eval('toString', scope)).toBe(3);
});
it('should parse string', function() {
expect(scope.$eval("'a' + 'b c'")).toEqual("ab c");
});
it('should evaluate grouped expressions', function() {
expect(scope.$eval("(1+2)*3")).toEqual((1+2)*3);
});
it('should parse filters', function() {
$filterProvider.register('substring', valueFn(function(input, start, end) {
return input.substring(start, end);
}));
it('should evaluate assignments', function() {
expect(scope.$eval("a=12")).toEqual(12);
expect(scope.a).toEqual(12);
expect(function() {
scope.$eval("1|nonexistent");
}).toThrow(new Error("Unknown provider: nonexistentFilterProvider <- nonexistentFilter"));
expect(scope.$eval("x.y.z=123;")).toEqual(123);
expect(scope.x.y.z).toEqual(123);
scope.offset = 3;
expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc");
expect(scope.$eval("'abcd'|substring:1:3|uppercase")).toEqual("BC");
});
expect(scope.$eval("a=123; b=234")).toEqual(234);
expect(scope.a).toEqual(123);
expect(scope.b).toEqual(234);
});
it('should access scope', function() {
scope.a = 123;
scope.b = {c: 456};
expect(scope.$eval("a", scope)).toEqual(123);
expect(scope.$eval("b.c", scope)).toEqual(456);
expect(scope.$eval("x.y.z", scope)).not.toBeDefined();
});
it('should evaluate function call without arguments', function() {
scope['const'] = function(a,b){return 123;};
expect(scope.$eval("const()")).toEqual(123);
});
it('should resolve deeply nested paths (important for CSP mode)', function() {
scope.a = {b: {c: {d: {e: {f: {g: {h: {i: {j: {k: {l: {m: {n: 'nooo!'}}}}}}}}}}}}};
expect(scope.$eval("a.b.c.d.e.f.g.h.i.j.k.l.m.n", scope)).toBe('nooo!');
});
it('should evaluate function call with arguments', function() {
scope.add = function(a,b) {
return a+b;
};
expect(scope.$eval("add(1,2)")).toEqual(3);
});
it('should be forgiving', function() {
scope.a = {b: 23};
expect(scope.$eval('b')).toBeUndefined();
expect(scope.$eval('a.x')).toBeUndefined();
expect(scope.$eval('a.b.c.d')).toBeUndefined();
});
it('should evaluate function call from a return value', function() {
scope.val = 33;
scope.getter = function() { return function() { return this.val; }};
expect(scope.$eval("getter()()")).toBe(33);
});
it('should support property names that collide with native object properties', function() {
// regression
scope.watch = 1;
scope.constructor = 2;
scope.toString = 3;
it('should evaluate multiplication and division', function() {
scope.taxRate = 8;
scope.subTotal = 100;
expect(scope.$eval("taxRate / 100 * subTotal")).toEqual(8);
expect(scope.$eval("subTotal * taxRate / 100")).toEqual(8);
});
expect(scope.$eval('watch', scope)).toBe(1);
expect(scope.$eval('constructor', scope)).toBe(2);
expect(scope.$eval('toString', scope)).toBe(3);
});
it('should evaluate array', function() {
expect(scope.$eval("[]").length).toEqual(0);
expect(scope.$eval("[1, 2]").length).toEqual(2);
expect(scope.$eval("[1, 2]")[0]).toEqual(1);
expect(scope.$eval("[1, 2]")[1]).toEqual(2);
});
it('should evaluate grouped expressions', function() {
expect(scope.$eval("(1+2)*3")).toEqual((1+2)*3);
});
it('should evaluate array access', function() {
expect(scope.$eval("[1][0]")).toEqual(1);
expect(scope.$eval("[[1]][0][0]")).toEqual(1);
expect(scope.$eval("[].length")).toEqual(0);
expect(scope.$eval("[1, 2].length")).toEqual(2);
});
it('should evaluate assignments', function() {
expect(scope.$eval("a=12")).toEqual(12);
expect(scope.a).toEqual(12);
it('should evaluate object', function() {
expect(toJson(scope.$eval("{}"))).toEqual("{}");
expect(toJson(scope.$eval("{a:'b'}"))).toEqual('{"a":"b"}');
expect(toJson(scope.$eval("{'a':'b'}"))).toEqual('{"a":"b"}');
expect(toJson(scope.$eval("{\"a\":'b'}"))).toEqual('{"a":"b"}');
});
expect(scope.$eval("x.y.z=123;")).toEqual(123);
expect(scope.x.y.z).toEqual(123);
it('should evaluate object access', function() {
expect(scope.$eval("{false:'WC', true:'CC'}[false]")).toEqual("WC");
});
expect(scope.$eval("a=123; b=234")).toEqual(234);
expect(scope.a).toEqual(123);
expect(scope.b).toEqual(234);
});
it('should evaluate JSON', function() {
expect(toJson(scope.$eval("[{}]"))).toEqual("[{}]");
expect(toJson(scope.$eval("[{a:[]}, {b:1}]"))).toEqual('[{"a":[]},{"b":1}]');
});
it('should evaluate function call without arguments', function() {
scope['const'] = function(a,b){return 123;};
expect(scope.$eval("const()")).toEqual(123);
});
it('should evaluate multiple statements', function() {
expect(scope.$eval("a=1;b=3;a+b")).toEqual(4);
expect(scope.$eval(";;1;;")).toEqual(1);
});
it('should evaluate function call with arguments', function() {
scope.add = function(a,b) {
return a+b;
};
expect(scope.$eval("add(1,2)")).toEqual(3);
});
it('should evaluate object methods in correct context (this)', function() {
var C = function () {
this.a = 123;
};
C.prototype.getA = function() {
return this.a;
};
it('should evaluate function call from a return value', function() {
scope.val = 33;
scope.getter = function() { return function() { return this.val; }};
expect(scope.$eval("getter()()")).toBe(33);
});
scope.obj = new C();
expect(scope.$eval("obj.getA()")).toEqual(123);
expect(scope.$eval("obj['getA']()")).toEqual(123);
});
it('should evaluate multiplication and division', function() {
scope.taxRate = 8;
scope.subTotal = 100;
expect(scope.$eval("taxRate / 100 * subTotal")).toEqual(8);
expect(scope.$eval("subTotal * taxRate / 100")).toEqual(8);
});
it('should evaluate methods in correct context (this) in argument', function() {
var C = function () {
this.a = 123;
};
C.prototype.sum = function(value) {
return this.a + value;
};
C.prototype.getA = function() {
return this.a;
};
it('should evaluate array', function() {
expect(scope.$eval("[]").length).toEqual(0);
expect(scope.$eval("[1, 2]").length).toEqual(2);
expect(scope.$eval("[1, 2]")[0]).toEqual(1);
expect(scope.$eval("[1, 2]")[1]).toEqual(2);
});
scope.obj = new C();
expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246);
expect(scope.$eval("obj['sum'](obj.getA())")).toEqual(246);
});
it('should evaluate array access', function() {
expect(scope.$eval("[1][0]")).toEqual(1);
expect(scope.$eval("[[1]][0][0]")).toEqual(1);
expect(scope.$eval("[].length")).toEqual(0);
expect(scope.$eval("[1, 2].length")).toEqual(2);
});
it('should evaluate objects on scope context', function() {
scope.a = "abc";
expect(scope.$eval("{a:a}").a).toEqual("abc");
});
it('should evaluate object', function() {
expect(toJson(scope.$eval("{}"))).toEqual("{}");
expect(toJson(scope.$eval("{a:'b'}"))).toEqual('{"a":"b"}');
expect(toJson(scope.$eval("{'a':'b'}"))).toEqual('{"a":"b"}');
expect(toJson(scope.$eval("{\"a\":'b'}"))).toEqual('{"a":"b"}');
});
it('should evaluate field access on function call result', function() {
scope.a = function() {
return {name:'misko'};
};
expect(scope.$eval("a().name")).toEqual("misko");
});
it('should evaluate object access', function() {
expect(scope.$eval("{false:'WC', true:'CC'}[false]")).toEqual("WC");
});
it('should evaluate field access after array access', function () {
scope.items = [{}, {name:'misko'}];
expect(scope.$eval('items[1].name')).toEqual("misko");
});
it('should evaluate JSON', function() {
expect(toJson(scope.$eval("[{}]"))).toEqual("[{}]");
expect(toJson(scope.$eval("[{a:[]}, {b:1}]"))).toEqual('[{"a":[]},{"b":1}]');
});
it('should evaluate array assignment', function() {
scope.items = [];
it('should evaluate multiple statements', function() {
expect(scope.$eval("a=1;b=3;a+b")).toEqual(4);
expect(scope.$eval(";;1;;")).toEqual(1);
});
expect(scope.$eval('items[1] = "abc"')).toEqual("abc");
expect(scope.$eval('items[1]')).toEqual("abc");
// Dont know how to make this work....
// expect(scope.$eval('books[1] = "moby"')).toEqual("moby");
// expect(scope.$eval('books[1]')).toEqual("moby");
});
it('should evaluate object methods in correct context (this)', function() {
var C = function () {
this.a = 123;
};
C.prototype.getA = function() {
return this.a;
};
it('should evaluate grouped filters', function() {
scope.name = 'MISKO';
expect(scope.$eval('n = (name|lowercase)')).toEqual('misko');
expect(scope.$eval('n')).toEqual('misko');
});
scope.obj = new C();
expect(scope.$eval("obj.getA()")).toEqual(123);
expect(scope.$eval("obj['getA']()")).toEqual(123);
});
it('should evaluate remainder', function() {
expect(scope.$eval('1%2')).toEqual(1);
});
it('should evaluate methods in correct context (this) in argument', function() {
var C = function () {
this.a = 123;
};
C.prototype.sum = function(value) {
return this.a + value;
};
C.prototype.getA = function() {
return this.a;
};
it('should evaluate sum with undefined', function() {
expect(scope.$eval('1+undefined')).toEqual(1);
expect(scope.$eval('undefined+1')).toEqual(1);
});
scope.obj = new C();
expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246);
expect(scope.$eval("obj['sum'](obj.getA())")).toEqual(246);
});
it('should throw exception on non-closed bracket', function() {
expect(function() {
scope.$eval('[].count(');
}).toThrow('Unexpected end of expression: [].count(');
});
it('should evaluate objects on scope context', function() {
scope.a = "abc";
expect(scope.$eval("{a:a}").a).toEqual("abc");
});
it('should evaluate double negation', function() {
expect(scope.$eval('true')).toBeTruthy();
expect(scope.$eval('!true')).toBeFalsy();
expect(scope.$eval('!!true')).toBeTruthy();
expect(scope.$eval('{true:"a", false:"b"}[!!true]')).toEqual('a');
});
it('should evaluate field access on function call result', function() {
scope.a = function() {
return {name:'misko'};
};
expect(scope.$eval("a().name")).toEqual("misko");
});
it('should evaluate negation', function() {
expect(scope.$eval("!false || true")).toEqual(!false || true);
expect(scope.$eval("!11 == 10")).toEqual(!11 == 10);
expect(scope.$eval("12/6/2")).toEqual(12/6/2);
});
it('should evaluate field access after array access', function () {
scope.items = [{}, {name:'misko'}];
expect(scope.$eval('items[1].name')).toEqual("misko");
});
it('should evaluate exclamation mark', function() {
expect(scope.$eval('suffix = "!"')).toEqual('!');
});
it('should evaluate array assignment', function() {
scope.items = [];
it('should evaluate minus', function() {
expect(scope.$eval("{a:'-'}")).toEqual({a: "-"});
});
expect(scope.$eval('items[1] = "abc"')).toEqual("abc");
expect(scope.$eval('items[1]')).toEqual("abc");
// Dont know how to make this work....
// expect(scope.$eval('books[1] = "moby"')).toEqual("moby");
// expect(scope.$eval('books[1]')).toEqual("moby");
});
it('should evaluate undefined', function() {
expect(scope.$eval("undefined")).not.toBeDefined();
expect(scope.$eval("a=undefined")).not.toBeDefined();
expect(scope.a).not.toBeDefined();
});
it('should evaluate grouped filters', function() {
scope.name = 'MISKO';
expect(scope.$eval('n = (name|lowercase)')).toEqual('misko');
expect(scope.$eval('n')).toEqual('misko');
});
it('should allow assignment after array dereference', function() {
scope.obj = [{}];
scope.$eval('obj[0].name=1');
expect(scope.obj.name).toBeUndefined();
expect(scope.obj[0].name).toEqual(1);
});
it('should evaluate remainder', function() {
expect(scope.$eval('1%2')).toEqual(1);
});
it('should short-circuit AND operator', function() {
scope.run = function() {
throw "IT SHOULD NOT HAVE RUN";
};
expect(scope.$eval('false && run()')).toBe(false);
});
it('should evaluate sum with undefined', function() {
expect(scope.$eval('1+undefined')).toEqual(1);
expect(scope.$eval('undefined+1')).toEqual(1);
});
it('should short-circuit OR operator', function() {
scope.run = function() {
throw "IT SHOULD NOT HAVE RUN";
};
expect(scope.$eval('true || run()')).toBe(true);
});
it('should throw exception on non-closed bracket', function() {
expect(function() {
scope.$eval('[].count(');
}).toThrow('Unexpected end of expression: [].count(');
});
it('should evaluate double negation', function() {
expect(scope.$eval('true')).toBeTruthy();
expect(scope.$eval('!true')).toBeFalsy();
expect(scope.$eval('!!true')).toBeTruthy();
expect(scope.$eval('{true:"a", false:"b"}[!!true]')).toEqual('a');
});
it('should evaluate negation', function() {
expect(scope.$eval("!false || true")).toEqual(!false || true);
expect(scope.$eval("!11 == 10")).toEqual(!11 == 10);
expect(scope.$eval("12/6/2")).toEqual(12/6/2);
});
it('should evaluate exclamation mark', function() {
expect(scope.$eval('suffix = "!"')).toEqual('!');
});
it('should evaluate minus', function() {
expect(scope.$eval("{a:'-'}")).toEqual({a: "-"});
});
it('should evaluate undefined', function() {
expect(scope.$eval("undefined")).not.toBeDefined();
expect(scope.$eval("a=undefined")).not.toBeDefined();
expect(scope.a).not.toBeDefined();
});
it('should allow assignment after array dereference', function() {
scope.obj = [{}];
scope.$eval('obj[0].name=1');
expect(scope.obj.name).toBeUndefined();
expect(scope.obj[0].name).toEqual(1);
});
it('should short-circuit AND operator', function() {
scope.run = function() {
throw "IT SHOULD NOT HAVE RUN";
};
expect(scope.$eval('false && run()')).toBe(false);
});
it('should short-circuit OR operator', function() {
scope.run = function() {
throw "IT SHOULD NOT HAVE RUN";
};
expect(scope.$eval('true || run()')).toBe(true);
});
describe('promises', function() {
var deferred, promise, q;
describe('promises', function() {
var deferred, promise, q;
beforeEach(inject(function($q) {
q = $q;
deferred = q.defer();
promise = deferred.promise;
}));
beforeEach(inject(function($q) {
q = $q;
deferred = q.defer();
promise = deferred.promise;
}));
describe('{{promise}}', function() {
it('should evaluated resolved promise and get its value', function() {
deferred.resolve('hello!');
scope.greeting = promise;
expect(scope.$eval('greeting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe('hello!');
describe('{{promise}}', function() {
it('should evaluated resolved promise and get its value', function() {
deferred.resolve('hello!');
scope.greeting = promise;
expect(scope.$eval('greeting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe('hello!');
});
it('should evaluated rejected promise and ignore the rejection reason', function() {
deferred.reject('sorry');
scope.greeting = promise;
expect(scope.$eval('gretting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe(undefined);
});
it('should evaluate a promise and eventualy get its value', function() {
scope.greeting = promise;
expect(scope.$eval('greeting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe(undefined);
deferred.resolve('hello!');
expect(scope.$eval('greeting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe('hello!');
});
it('should evaluate a promise and eventualy ignore its rejection', function() {
scope.greeting = promise;
expect(scope.$eval('greeting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe(undefined);
deferred.reject('sorry');
expect(scope.$eval('greeting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe(undefined);
});
});
describe('dereferencing', function() {
it('should evaluate and dereference properties leading to and from a promise', function() {
scope.obj = {greeting: promise};
expect(scope.$eval('obj.greeting')).toBe(undefined);
expect(scope.$eval('obj.greeting.polite')).toBe(undefined);
it('should evaluated rejected promise and ignore the rejection reason', function() {
deferred.reject('sorry');
scope.greeting = promise;
expect(scope.$eval('gretting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe(undefined);
});
scope.$digest();
expect(scope.$eval('obj.greeting')).toBe(undefined);
expect(scope.$eval('obj.greeting.polite')).toBe(undefined);
deferred.resolve({polite: 'Good morning!'});
scope.$digest();
expect(scope.$eval('obj.greeting')).toEqual({polite: 'Good morning!'});
expect(scope.$eval('obj.greeting.polite')).toBe('Good morning!');
});
it('should evaluate and dereference properties leading to and from a promise via bracket ' +
'notation', function() {
scope.obj = {greeting: promise};
expect(scope.$eval('obj["greeting"]')).toBe(undefined);
expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('obj["greeting"]')).toBe(undefined);
expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined);
deferred.resolve({polite: 'Good morning!'});
scope.$digest();
expect(scope.$eval('obj["greeting"]')).toEqual({polite: 'Good morning!'});
expect(scope.$eval('obj["greeting"]["polite"]')).toBe('Good morning!');
});
it('should evaluate a promise and eventualy get its value', function() {
scope.greeting = promise;
expect(scope.$eval('greeting')).toBe(undefined);
it('should evaluate and dereference array references leading to and from a promise',
function() {
scope.greetings = [promise];
expect(scope.$eval('greetings[0]')).toBe(undefined);
expect(scope.$eval('greetings[0][0]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greetings[0]')).toBe(undefined);
expect(scope.$eval('greetings[0][0]')).toBe(undefined);
deferred.resolve('hello!');
expect(scope.$eval('greeting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe('hello!');
});
deferred.resolve(['Hi!', 'Cau!']);
scope.$digest();
expect(scope.$eval('greetings[0]')).toEqual(['Hi!', 'Cau!']);
expect(scope.$eval('greetings[0][0]')).toBe('Hi!');
});
it('should evaluate a promise and eventualy ignore its rejection', function() {
scope.greeting = promise;
expect(scope.$eval('greeting')).toBe(undefined);
it('should evaluate and dereference promises used as function arguments', function() {
scope.greet = function(name) { return 'Hi ' + name + '!'; };
scope.name = promise;
expect(scope.$eval('greet(name)')).toBe('Hi undefined!');
scope.$digest();
expect(scope.$eval('greeting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greet(name)')).toBe('Hi undefined!');
deferred.reject('sorry');
expect(scope.$eval('greeting')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greeting')).toBe(undefined);
deferred.resolve('Veronica');
expect(scope.$eval('greet(name)')).toBe('Hi undefined!');
scope.$digest();
expect(scope.$eval('greet(name)')).toBe('Hi Veronica!');
});
it('should evaluate and dereference promises used as array indexes', function() {
scope.childIndex = promise;
scope.kids = ['Adam', 'Veronica', 'Elisa'];
expect(scope.$eval('kids[childIndex]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('kids[childIndex]')).toBe(undefined);
deferred.resolve(1);
expect(scope.$eval('kids[childIndex]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('kids[childIndex]')).toBe('Veronica');
});
it('should evaluate and dereference promises used as keys in bracket notation', function() {
scope.childKey = promise;
scope.kids = {'a': 'Adam', 'v': 'Veronica', 'e': 'Elisa'};
expect(scope.$eval('kids[childKey]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('kids[childKey]')).toBe(undefined);
deferred.resolve('v');
expect(scope.$eval('kids[childKey]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('kids[childKey]')).toBe('Veronica');
});
it('should not mess with the promise if it was not directly evaluated', function() {
scope.obj = {greeting: promise, username: 'hi'};
var obj = scope.$eval('obj');
expect(obj.username).toEqual('hi');
expect(typeof obj.greeting.then).toBe('function');
});
});
});
describe('dereferencing', function() {
it('should evaluate and dereference properties leading to and from a promise', function() {
scope.obj = {greeting: promise};
expect(scope.$eval('obj.greeting')).toBe(undefined);
expect(scope.$eval('obj.greeting.polite')).toBe(undefined);
scope.$digest();
expect(scope.$eval('obj.greeting')).toBe(undefined);
expect(scope.$eval('obj.greeting.polite')).toBe(undefined);
deferred.resolve({polite: 'Good morning!'});
scope.$digest();
expect(scope.$eval('obj.greeting')).toEqual({polite: 'Good morning!'});
expect(scope.$eval('obj.greeting.polite')).toBe('Good morning!');
});
it('should evaluate and dereference properties leading to and from a promise via bracket ' +
'notation', function() {
scope.obj = {greeting: promise};
expect(scope.$eval('obj["greeting"]')).toBe(undefined);
expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('obj["greeting"]')).toBe(undefined);
expect(scope.$eval('obj["greeting"]["polite"]')).toBe(undefined);
deferred.resolve({polite: 'Good morning!'});
scope.$digest();
expect(scope.$eval('obj["greeting"]')).toEqual({polite: 'Good morning!'});
expect(scope.$eval('obj["greeting"]["polite"]')).toBe('Good morning!');
});
it('should evaluate and dereference array references leading to and from a promise',
function() {
scope.greetings = [promise];
expect(scope.$eval('greetings[0]')).toBe(undefined);
expect(scope.$eval('greetings[0][0]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('greetings[0]')).toBe(undefined);
expect(scope.$eval('greetings[0][0]')).toBe(undefined);
deferred.resolve(['Hi!', 'Cau!']);
scope.$digest();
expect(scope.$eval('greetings[0]')).toEqual(['Hi!', 'Cau!']);
expect(scope.$eval('greetings[0][0]')).toBe('Hi!');
});
it('should evaluate and dereference promises used as function arguments', function() {
scope.greet = function(name) { return 'Hi ' + name + '!'; };
scope.name = promise;
expect(scope.$eval('greet(name)')).toBe('Hi undefined!');
scope.$digest();
expect(scope.$eval('greet(name)')).toBe('Hi undefined!');
deferred.resolve('Veronica');
expect(scope.$eval('greet(name)')).toBe('Hi undefined!');
scope.$digest();
expect(scope.$eval('greet(name)')).toBe('Hi Veronica!');
});
it('should evaluate and dereference promises used as array indexes', function() {
scope.childIndex = promise;
scope.kids = ['Adam', 'Veronica', 'Elisa'];
expect(scope.$eval('kids[childIndex]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('kids[childIndex]')).toBe(undefined);
deferred.resolve(1);
expect(scope.$eval('kids[childIndex]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('kids[childIndex]')).toBe('Veronica');
});
it('should evaluate and dereference promises used as keys in bracket notation', function() {
scope.childKey = promise;
scope.kids = {'a': 'Adam', 'v': 'Veronica', 'e': 'Elisa'};
expect(scope.$eval('kids[childKey]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('kids[childKey]')).toBe(undefined);
deferred.resolve('v');
expect(scope.$eval('kids[childKey]')).toBe(undefined);
scope.$digest();
expect(scope.$eval('kids[childKey]')).toBe('Veronica');
});
it('should not mess with the promise if it was not directly evaluated', function() {
scope.obj = {greeting: promise, username: 'hi'};
var obj = scope.$eval('obj');
expect(obj.username).toEqual('hi');
expect(typeof obj.greeting.then).toBe('function');
});
describe('assignable', function() {
it('should expose assignment function', inject(function($parse) {
var fn = $parse('a');
expect(fn.assign).toBeTruthy();
var scope = {};
fn.assign(scope, 123);
expect(scope).toEqual({a:123});
}));
});
});
describe('assignable', function() {
it('should expose assignment function', inject(function($parse) {
var fn = $parse('a');
expect(fn.assign).toBeTruthy();
var scope = {};
fn.assign(scope, 123);
expect(scope).toEqual({a:123});
}));
});
describe('locals', function() {
it('should expose local variables', inject(function($parse) {
expect($parse('a')({a: 0}, {a: 1})).toEqual(1);
expect($parse('add(a,b)')({b: 1, add: function(a, b) { return a + b; }}, {a: 2})).toEqual(3);
}));
describe('locals', function() {
it('should expose local variables', inject(function($parse) {
expect($parse('a')({a: 0}, {a: 1})).toEqual(1);
expect($parse('add(a,b)')({b: 1, add: function(a, b) { return a + b; }}, {a: 2})).toEqual(3);
}));
it('should expose traverse locals', inject(function($parse) {
expect($parse('a.b')({a: {b: 0}}, {a: {b:1}})).toEqual(1);
expect($parse('a.b')({a: null}, {a: {b:1}})).toEqual(1);
expect($parse('a.b')({a: {b: 0}}, {a: null})).toEqual(undefined);
}));
it('should expose traverse locals', inject(function($parse) {
expect($parse('a.b')({a: {b: 0}}, {a: {b:1}})).toEqual(1);
expect($parse('a.b')({a: null}, {a: {b:1}})).toEqual(1);
expect($parse('a.b')({a: {b: 0}}, {a: null})).toEqual(undefined);
}));
});
});
});