angular.js/src/ng/parse.js

1075 lines
31 KiB
JavaScript
Raw Normal View History

'use strict';
var $parseMinErr = minErr('$parse');
// Sandboxing Angular Expressions
// ------------------------------
// Angular expressions are generally considered safe because these expressions only have direct access to $scope and
// locals. However, one can obtain the ability to execute arbitrary JS code by obtaining a reference to native JS
// functions such as the Function constructor.
//
// 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.
//
// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits against the
// expression language, but not to prevent exploits that were enabled by exposing sensitive JavaScript or browser apis
// on Scope. Exposing such objects on a Scope is never a good practice and therefore we are not even trying to protect
// against interaction with an object explicitly exposed in this way.
//
// 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.
function ensureSafeMemberName(name, fullExpression) {
if (name === "constructor") {
throw $parseMinErr('isecfld',
'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', fullExpression);
}
return name;
};
function ensureSafeObject(obj, fullExpression) {
// nifty check if obj is Function that is fast and works across iframes and other contexts
if (obj && obj.constructor === obj) {
throw $parseMinErr('isecfn',
'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression);
} else if (// isWindow(obj)
obj && obj.document && obj.location && obj.alert && obj.setInterval) {
throw $parseMinErr('isecwindow',
'Referencing the Window in Angular expressions is disallowed! Expression: {0}', fullExpression);
} else if (// isElement(obj)
obj && (obj.nodeName || (obj.on && obj.find))) {
throw $parseMinErr('isecdom',
'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', fullExpression);
} else {
return obj;
}
}
var OPERATORS = {
'null':function(){return null;},
'true':function(){return true;},
'false':function(){return false;},
undefined:noop,
'+':function(self, locals, a,b){
a=a(self, locals); b=b(self, locals);
if (isDefined(a)) {
if (isDefined(b)) {
return a + b;
}
return a;
}
return isDefined(b)?b:undefined;},
'-':function(self, locals, a,b){a=a(self, locals); b=b(self, locals); return (isDefined(a)?a:0)-(isDefined(b)?b:0);},
'*':function(self, locals, a,b){return a(self, locals)*b(self, locals);},
'/':function(self, locals, a,b){return a(self, locals)/b(self, locals);},
'%':function(self, locals, a,b){return a(self, locals)%b(self, locals);},
'^':function(self, locals, a,b){return a(self, locals)^b(self, locals);},
'=':noop,
'===':function(self, locals, a, b){return a(self, locals)===b(self, locals);},
'!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);},
'==':function(self, locals, a,b){return a(self, locals)==b(self, locals);},
'!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);},
'<':function(self, locals, a,b){return a(self, locals)<b(self, locals);},
'>':function(self, locals, a,b){return a(self, locals)>b(self, locals);},
'<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);},
'>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);},
'&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);},
'||':function(self, locals, a,b){return a(self, locals)||b(self, locals);},
'&':function(self, locals, a,b){return a(self, locals)&b(self, locals);},
// '|':function(self, locals, a,b){return a|b;},
'|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));},
'!':function(self, locals, a){return !a(self, locals);}
2010-01-06 00:36:58 +00:00
};
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
2010-01-06 00:36:58 +00:00
/////////////////////////////////////////
/**
* @constructor
*/
var Lexer = function (csp) {
this.csp = csp;
};
Lexer.prototype = {
constructor: Lexer,
lex: function (text) {
this.text = text;
this.index = 0;
this.ch = undefined;
this.lastCh = ':'; // can start regexp
this.tokens = [];
var token;
var json = [];
while (this.index < this.text.length) {
this.ch = this.text.charAt(this.index);
if (this.is('"\'')) {
this.readString(this.ch);
} else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) {
this.readNumber();
} else if (this.isIdent(this.ch)) {
this.readIdent();
// identifiers can only be if the preceding char was a { or ,
if (this.was('{,') && json[0] === '{' &&
(token = this.tokens[this.tokens.length - 1])) {
token.json = token.text.indexOf('.') === -1;
}
} else if (this.is('(){}[].,;:?')) {
this.tokens.push({
index: this.index,
text: this.ch,
json: (this.was(':[,') && this.is('{[')) || this.is('}]:,')
});
if (this.is('{[')) json.unshift(this.ch);
if (this.is('}]')) json.shift();
this.index++;
} else if (this.isWhitespace(this.ch)) {
this.index++;
continue;
2010-01-12 01:32:33 +00:00
} else {
var ch2 = this.ch + this.peek();
var ch3 = ch2 + this.peek(2);
var fn = OPERATORS[this.ch];
var fn2 = OPERATORS[ch2];
var fn3 = OPERATORS[ch3];
if (fn3) {
this.tokens.push({index: this.index, text: ch3, fn: fn3});
this.index += 3;
} else if (fn2) {
this.tokens.push({index: this.index, text: ch2, fn: fn2});
this.index += 2;
} else if (fn) {
this.tokens.push({
index: this.index,
text: this.ch,
fn: fn,
json: (this.was('[,:') && this.is('+-'))
});
this.index += 1;
} else {
this.throwError('Unexpected next character ', this.index, this.index + 1);
}
2010-01-06 00:36:58 +00:00
}
this.lastCh = this.ch;
2010-01-12 01:32:33 +00:00
}
return this.tokens;
},
2010-03-24 17:35:01 +00:00
is: function(chars) {
return chars.indexOf(this.ch) !== -1;
},
was: function(chars) {
return chars.indexOf(this.lastCh) !== -1;
},
peek: function(i) {
var num = i || 1;
return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false;
},
isNumber: function(ch) {
return ('0' <= ch && ch <= '9');
},
isWhitespace: function(ch) {
return (ch === ' ' || ch === '\r' || ch === '\t' ||
ch === '\n' || ch === '\v' || ch === '\u00A0'); // IE treats non-breaking space as \u00A0
},
isIdent: function(ch) {
return ('a' <= ch && ch <= 'z' ||
'A' <= ch && ch <= 'Z' ||
'_' === ch || ch === '$');
},
isExpOperator: function(ch) {
return (ch === '-' || ch === '+' || this.isNumber(ch));
},
throwError: function(error, start, end) {
end = end || this.index;
var colStr = (isDefined(start)
? 's ' + start + '-' + this.index + ' [' + this.text.substring(start, end) + ']'
: ' ' + end);
throw $parseMinErr('lexerr', 'Lexer Error: {0} at column{1} in expression [{2}].',
error, colStr, this.text);
},
readNumber: function() {
var number = '';
var start = this.index;
while (this.index < this.text.length) {
var ch = lowercase(this.text.charAt(this.index));
if (ch == '.' || this.isNumber(ch)) {
number += ch;
} else {
var peekCh = this.peek();
if (ch == 'e' && this.isExpOperator(peekCh)) {
number += ch;
} else if (this.isExpOperator(ch) &&
peekCh && this.isNumber(peekCh) &&
number.charAt(number.length - 1) == 'e') {
number += ch;
} else if (this.isExpOperator(ch) &&
(!peekCh || !this.isNumber(peekCh)) &&
number.charAt(number.length - 1) == 'e') {
this.throwError('Invalid exponent');
} else {
break;
}
}
this.index++;
}
number = 1 * number;
this.tokens.push({
index: start,
text: number,
json: true,
fn: function() { return number; }
});
},
readIdent: function() {
var parser = this;
var ident = '';
var start = this.index;
var lastDot, peekIndex, methodName, ch;
while (this.index < this.text.length) {
ch = this.text.charAt(this.index);
if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) {
if (ch === '.') lastDot = this.index;
ident += ch;
} else {
break;
2010-01-12 01:32:33 +00:00
}
this.index++;
}
//check if this is not a method invocation and if it is back out to last dot
if (lastDot) {
peekIndex = this.index;
while (peekIndex < this.text.length) {
ch = this.text.charAt(peekIndex);
if (ch === '(') {
methodName = ident.substr(lastDot - start + 1);
ident = ident.substr(0, lastDot - start);
this.index = peekIndex;
break;
}
if (this.isWhitespace(ch)) {
peekIndex++;
} else {
break;
}
}
}
var token = {
index: start,
text: ident
};
// OPERATORS is our own object so we don't need to use special hasOwnPropertyFn
if (OPERATORS.hasOwnProperty(ident)) {
token.fn = OPERATORS[ident];
token.json = OPERATORS[ident];
} else {
var getter = getterFn(ident, this.csp, this.text);
token.fn = extend(function(self, locals) {
return (getter(self, locals));
}, {
assign: function(self, value) {
return setter(self, ident, value, parser.text);
}
});
}
this.tokens.push(token);
if (methodName) {
this.tokens.push({
index:lastDot,
text: '.',
json: false
});
this.tokens.push({
index: lastDot + 1,
text: methodName,
json: false
});
}
},
readString: function(quote) {
var start = this.index;
this.index++;
var string = '';
var rawString = quote;
var escape = false;
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
rawString += ch;
if (escape) {
if (ch === 'u') {
var hex = this.text.substring(this.index + 1, this.index + 5);
if (!hex.match(/[\da-f]{4}/i))
this.throwError('Invalid unicode escape [\\u' + hex + ']');
this.index += 4;
string += String.fromCharCode(parseInt(hex, 16));
} else {
var rep = ESCAPE[ch];
if (rep) {
string += rep;
} else {
string += ch;
}
}
escape = false;
} else if (ch === '\\') {
escape = true;
} else if (ch === quote) {
this.index++;
this.tokens.push({
index: start,
text: rawString,
string: string,
json: true,
fn: function() { return string; }
});
return;
} else {
string += ch;
}
this.index++;
}
this.throwError('Unterminated quote', start);
}
};
2010-01-06 00:36:58 +00:00
/**
* @constructor
*/
var Parser = function (lexer, $filter, csp) {
this.lexer = lexer;
this.$filter = $filter;
this.csp = csp;
};
Parser.ZERO = function () { return 0; };
2010-03-24 17:35:01 +00:00
Parser.prototype = {
constructor: Parser,
2010-03-24 17:35:01 +00:00
parse: function (text, json) {
this.text = text;
//TODO(i): strip all the obsolte json stuff from this file
this.json = json;
this.tokens = this.lexer.lex(text, this.csp);
if (json) {
// The extra level of aliasing is here, just in case the lexer misses something, so that
// we prevent any accidental execution in JSON.
this.assignment = this.logicalOR;
this.functionCall =
this.fieldAccess =
this.objectIndex =
this.filterChain = function() {
this.throwError('is not valid json', {text: text, index: 0});
};
}
var value = json ? this.primary() : this.statements();
if (this.tokens.length !== 0) {
this.throwError('is an unexpected token', this.tokens[0]);
}
value.literal = !!value.literal;
value.constant = !!value.constant;
return value;
},
primary: function () {
var primary;
if (this.expect('(')) {
primary = this.filterChain();
this.consume(')');
} else if (this.expect('[')) {
primary = this.arrayDeclaration();
} else if (this.expect('{')) {
primary = this.object();
} else {
var token = this.expect();
primary = token.fn;
if (!primary) {
this.throwError('not a primary expression', token);
}
if (token.json) {
primary.constant = true;
primary.literal = true;
}
}
var next, context;
while ((next = this.expect('(', '[', '.'))) {
if (next.text === '(') {
primary = this.functionCall(primary, context);
context = null;
} else if (next.text === '[') {
context = primary;
primary = this.objectIndex(primary);
} else if (next.text === '.') {
context = primary;
primary = this.fieldAccess(primary);
} else {
this.throwError('IMPOSSIBLE');
}
}
return primary;
},
throwError: function(msg, token) {
throw $parseMinErr('syntax',
'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].',
token.text, msg, (token.index + 1), this.text, this.text.substring(token.index));
},
peekToken: function() {
if (this.tokens.length === 0)
throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text);
return this.tokens[0];
},
peek: function(e1, e2, e3, e4) {
if (this.tokens.length > 0) {
var token = this.tokens[0];
2010-01-12 01:32:33 +00:00
var t = token.text;
if (t === e1 || t === e2 || t === e3 || t === e4 ||
2010-01-12 01:32:33 +00:00
(!e1 && !e2 && !e3 && !e4)) {
return token;
}
}
return false;
},
2010-03-24 17:35:01 +00:00
expect: function(e1, e2, e3, e4){
var token = this.peek(e1, e2, e3, e4);
2010-01-12 01:32:33 +00:00
if (token) {
if (this.json && !token.json) {
this.throwError('is not valid json', token);
}
this.tokens.shift();
2010-01-06 00:36:58 +00:00
return token;
}
2010-01-12 01:32:33 +00:00
return false;
},
2010-03-24 17:35:01 +00:00
consume: function(e1){
if (!this.expect(e1)) {
this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());
2010-01-06 00:36:58 +00:00
}
},
2010-03-24 17:35:01 +00:00
unaryFn: function(fn, right) {
return extend(function(self, locals) {
return fn(self, locals, right);
}, {
constant:right.constant
});
},
2010-03-24 17:35:01 +00:00
ternaryFn: function(left, middle, right){
return extend(function(self, locals){
return left(self, locals) ? middle(self, locals) : right(self, locals);
}, {
constant: left.constant && middle.constant && right.constant
});
},
binaryFn: function(left, fn, right) {
return extend(function(self, locals) {
return fn(self, locals, left, right);
}, {
constant:left.constant && right.constant
});
},
2010-03-24 17:35:01 +00:00
statements: function() {
2010-01-12 01:32:33 +00:00
var statements = [];
while (true) {
if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']'))
statements.push(this.filterChain());
if (!this.expect(';')) {
// optimize for the common case where there is only one statement.
// TODO(size): maybe we should not support multiple statements?
return (statements.length === 1)
? statements[0]
: function(self, locals) {
var value;
for (var i = 0; i < statements.length; i++) {
var statement = statements[i];
if (statement) {
value = statement(self, locals);
}
}
return value;
};
2010-01-12 01:32:33 +00:00
}
2010-01-06 00:36:58 +00:00
}
},
2010-03-24 17:35:01 +00:00
filterChain: function() {
var left = this.expression();
2010-01-12 01:32:33 +00:00
var token;
while (true) {
if ((token = this.expect('|'))) {
left = this.binaryFn(left, token.fn, this.filter());
2010-01-12 01:32:33 +00:00
} else {
return left;
}
}
},
2010-03-24 17:35:01 +00:00
filter: function() {
var token = this.expect();
var fn = this.$filter(token.text);
2010-01-12 01:32:33 +00:00
var argsFn = [];
while (true) {
if ((token = this.expect(':'))) {
argsFn.push(this.expression());
2010-01-12 01:32:33 +00:00
} else {
var fnInvoke = function(self, locals, input) {
2010-01-12 01:32:33 +00:00
var args = [input];
for (var i = 0; i < argsFn.length; i++) {
args.push(argsFn[i](self, locals));
2010-01-12 01:32:33 +00:00
}
return fn.apply(self, args);
2010-01-12 01:32:33 +00:00
};
return function() {
2010-01-12 01:32:33 +00:00
return fnInvoke;
};
}
}
},
2010-03-24 17:35:01 +00:00
expression: function() {
return this.assignment();
},
2010-03-24 17:35:01 +00:00
assignment: function() {
var left = this.ternary();
var right;
2010-01-12 01:32:33 +00:00
var token;
if ((token = this.expect('='))) {
if (!left.assign) {
this.throwError('implies assignment but [' +
this.text.substring(0, token.index) + '] can not be assigned to', token);
2010-01-12 01:32:33 +00:00
}
right = this.ternary();
return function(scope, locals) {
return left.assign(scope, right(scope, locals), locals);
};
2010-01-06 00:36:58 +00:00
}
return left;
},
2010-03-24 17:35:01 +00:00
ternary: function() {
var left = this.logicalOR();
var middle;
var token;
if ((token = this.expect('?'))) {
middle = this.ternary();
if ((token = this.expect(':'))) {
return this.ternaryFn(left, middle, this.ternary());
} else {
this.throwError('expected :', token);
}
} else {
return left;
}
},
logicalOR: function() {
var left = this.logicalAND();
2010-01-12 01:32:33 +00:00
var token;
while (true) {
if ((token = this.expect('||'))) {
left = this.binaryFn(left, token.fn, this.logicalAND());
2010-01-12 01:32:33 +00:00
} else {
return left;
}
}
},
2010-03-24 17:35:01 +00:00
logicalAND: function() {
var left = this.equality();
2010-01-12 01:32:33 +00:00
var token;
if ((token = this.expect('&&'))) {
left = this.binaryFn(left, token.fn, this.logicalAND());
2010-01-06 00:36:58 +00:00
}
return left;
},
2010-03-24 17:35:01 +00:00
equality: function() {
var left = this.relational();
2010-01-12 01:32:33 +00:00
var token;
if ((token = this.expect('==','!=','===','!=='))) {
left = this.binaryFn(left, token.fn, this.equality());
2010-01-12 01:32:33 +00:00
}
return left;
},
2010-03-24 17:35:01 +00:00
relational: function() {
var left = this.additive();
2010-01-12 01:32:33 +00:00
var token;
if ((token = this.expect('<', '>', '<=', '>='))) {
left = this.binaryFn(left, token.fn, this.relational());
2010-01-12 01:32:33 +00:00
}
return left;
},
2010-03-24 17:35:01 +00:00
additive: function() {
var left = this.multiplicative();
2010-01-12 01:32:33 +00:00
var token;
while ((token = this.expect('+','-'))) {
left = this.binaryFn(left, token.fn, this.multiplicative());
2010-01-12 01:32:33 +00:00
}
return left;
},
2010-03-24 17:35:01 +00:00
multiplicative: function() {
var left = this.unary();
2010-01-12 01:32:33 +00:00
var token;
while ((token = this.expect('*','/','%'))) {
left = this.binaryFn(left, token.fn, this.unary());
2010-01-12 01:32:33 +00:00
}
return left;
},
2010-03-24 17:35:01 +00:00
unary: function() {
2010-01-12 01:32:33 +00:00
var token;
if (this.expect('+')) {
return this.primary();
} else if ((token = this.expect('-'))) {
return this.binaryFn(Parser.ZERO, token.fn, this.unary());
} else if ((token = this.expect('!'))) {
return this.unaryFn(token.fn, this.unary());
2010-01-06 00:36:58 +00:00
} else {
return this.primary();
2010-01-06 00:36:58 +00:00
}
},
2010-03-24 17:35:01 +00:00
fieldAccess: function(object) {
var parser = this;
var field = this.expect().text;
var getter = getterFn(field, this.csp, this.text);
2010-03-24 17:35:01 +00:00
return extend(function(scope, locals, self) {
return getter(self || object(scope, locals), locals);
}, {
assign: function(scope, value, locals) {
return setter(object(scope, locals), field, value, parser.text);
}
});
},
objectIndex: function(obj) {
var parser = this;
2010-03-24 17:35:01 +00:00
var indexFn = this.expression();
this.consume(']');
2010-03-24 17:35:01 +00:00
return extend(function(self, locals) {
var o = obj(self, locals),
i = indexFn(self, locals),
v, p;
if (!o) return undefined;
v = ensureSafeObject(o[i], parser.text);
if (v && v.then) {
p = v;
if (!('$$v' in v)) {
p.$$v = undefined;
p.then(function(val) { p.$$v = val; });
}
v = v.$$v;
}
return v;
}, {
assign: function(self, value, locals) {
var key = indexFn(self, locals);
// prevent overwriting of Function.constructor which would break ensureSafeObject check
var safe = ensureSafeObject(obj(self, locals), parser.text);
return safe[key] = value;
}
});
},
2010-03-24 17:35:01 +00:00
functionCall: function(fn, contextGetter) {
2010-01-12 01:32:33 +00:00
var argsFn = [];
if (this.peekToken().text !== ')') {
2010-01-12 01:32:33 +00:00
do {
argsFn.push(this.expression());
} while (this.expect(','));
2010-01-12 01:32:33 +00:00
}
this.consume(')');
var parser = this;
return function(scope, locals) {
var args = [];
var context = contextGetter ? contextGetter(scope, locals) : scope;
for (var i = 0; i < argsFn.length; i++) {
args.push(argsFn[i](scope, locals));
2010-01-12 01:32:33 +00:00
}
var fnPtr = fn(scope, locals, context) || noop;
ensureSafeObject(fnPtr, parser.text);
// IE stupidity! (IE doesn't have apply for some native functions)
var v = fnPtr.apply
? fnPtr.apply(context, args)
: fnPtr(args[0], args[1], args[2], args[3], args[4]);
// Check for promise
if (v && v.then) {
var p = v;
if (!('$$v' in v)) {
p.$$v = undefined;
p.then(function(val) { p.$$v = val; });
}
v = v.$$v;
}
return ensureSafeObject(v, parser.text);
2010-01-06 00:36:58 +00:00
};
},
2010-03-24 17:35:01 +00:00
2010-01-12 01:32:33 +00:00
// This is used with json array declaration
arrayDeclaration: function () {
2010-01-12 01:32:33 +00:00
var elementFns = [];
var allConstant = true;
if (this.peekToken().text !== ']') {
2010-01-12 01:32:33 +00:00
do {
var elementFn = this.expression();
elementFns.push(elementFn);
if (!elementFn.constant) {
allConstant = false;
}
} while (this.expect(','));
2010-01-12 01:32:33 +00:00
}
this.consume(']');
return extend(function(self, locals) {
2010-01-12 01:32:33 +00:00
var array = [];
for (var i = 0; i < elementFns.length; i++) {
array.push(elementFns[i](self, locals));
2010-01-12 01:32:33 +00:00
}
return array;
}, {
literal: true,
constant: allConstant
});
},
2010-03-24 17:35:01 +00:00
object: function () {
2010-01-12 01:32:33 +00:00
var keyValues = [];
var allConstant = true;
if (this.peekToken().text !== '}') {
2010-01-12 01:32:33 +00:00
do {
var token = this.expect(),
key = token.string || token.text;
this.consume(':');
var value = this.expression();
keyValues.push({key: key, value: value});
if (!value.constant) {
allConstant = false;
}
} while (this.expect(','));
2010-01-12 01:32:33 +00:00
}
this.consume('}');
return extend(function(self, locals) {
2010-01-12 01:32:33 +00:00
var object = {};
for (var i = 0; i < keyValues.length; i++) {
2010-01-12 01:32:33 +00:00
var keyValue = keyValues[i];
object[keyValue.key] = keyValue.value(self, locals);
2010-01-12 01:32:33 +00:00
}
return object;
}, {
literal: true,
constant: allConstant
});
},
};
//////////////////////////////////////////////////
// Parser helper functions
//////////////////////////////////////////////////
function setter(obj, path, setValue, fullExp) {
var element = path.split('.'), key;
for (var i = 0; element.length > 1; i++) {
key = ensureSafeMemberName(element.shift(), fullExp);
var propertyObj = obj[key];
if (!propertyObj) {
propertyObj = {};
obj[key] = propertyObj;
}
obj = propertyObj;
if (obj.then) {
if (!("$$v" in obj)) {
(function(promise) {
promise.then(function(val) { promise.$$v = val; }); }
)(obj);
}
if (obj.$$v === undefined) {
obj.$$v = {};
}
obj = obj.$$v;
}
}
key = ensureSafeMemberName(element.shift(), fullExp);
obj[key] = setValue;
return setValue;
}
2012-01-24 10:42:20 +00:00
var getterFnCache = {};
/**
* 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, fullExp) {
ensureSafeMemberName(key0, fullExp);
ensureSafeMemberName(key1, fullExp);
ensureSafeMemberName(key2, fullExp);
ensureSafeMemberName(key3, fullExp);
ensureSafeMemberName(key4, fullExp);
return function(scope, locals) {
var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope,
promise;
if (pathVal === null || pathVal === undefined) 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 === null || pathVal === undefined) 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 === null || pathVal === undefined) 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 === null || pathVal === undefined) 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 === null || pathVal === undefined) 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, fullExp) {
// Check whether the cache has this getter already.
// We can use hasOwnProperty directly on the cache because we ensure,
// see below, that the cache never stores a path called 'hasOwnProperty'
if (getterFnCache.hasOwnProperty(path)) {
return getterFnCache[path];
}
var pathKeys = path.split('.'),
pathKeysLength = pathKeys.length,
fn;
if (csp) {
fn = (pathKeysLength < 6)
? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp)
: function(scope, locals) {
var i = 0, val;
do {
val = cspSafeGetterFn(
pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp
)(scope, locals);
locals = undefined; // clear after first iteration
scope = val;
} while (i < pathKeysLength);
return val;
}
} else {
var code = 'var l, fn, p;\n';
forEach(pathKeys, function(key, index) {
ensureSafeMemberName(key, fullExp);
code += 'if(s === null || s === undefined) 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; };
}
// Only cache the value if it's not going to mess up the cache object
// This is more performant that using Object.prototype.hasOwnProperty.call
if (path !== 'hasOwnProperty') {
getterFnCache[path] = fn;
}
return fn;
}
///////////////////////////////////
2012-02-28 22:29:58 +00:00
/**
* @ngdoc function
* @name ng.$parse
2012-02-28 22:29:58 +00:00
* @function
*
* @description
*
* Converts Angular {@link guide/expression expression} into a function.
2012-02-28 22:29:58 +00:00
*
* <pre>
* var getter = $parse('user.name');
* var setter = getter.assign;
* var context = {user:{name:'angular'}};
* var locals = {user:{name:'local'}};
*
* expect(getter(context)).toEqual('angular');
* setter(context, 'newValue');
* expect(context.user.name).toEqual('newValue');
* expect(getter(context, locals)).toEqual('local');
* </pre>
*
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
*
* * `context` `{object}` an object against which any expressions embedded in the strings
2013-03-21 19:09:47 +00:00
* are evaluated against (typically a scope object).
* * `locals` `{object=}` local variables context object, useful for overriding values in
* `context`.
2012-02-28 22:29:58 +00:00
*
* The returned function also has the following properties:
* * `literal` `{boolean}` whether the expression's top-level node is a JavaScript
* literal.
* * `constant` `{boolean}` whether the expression is made entirely of JavaScript
* constant literals.
* * `assign` `{?function(context, value)}` if the expression is assignable, this will be
* set to a function to change its value on the given context.
2012-02-28 22:29:58 +00:00
*
*/
function $ParseProvider() {
var cache = {};
this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
return function(exp) {
var lexer = new Lexer($sniffer.csp);
var parser = new Parser(lexer, $filter, $sniffer.csp);
var parsedExpression;
switch (typeof exp) {
case 'string':
if (cache.hasOwnProperty(exp)) {
return cache[exp];
}
parsedExpression = parser.parse(exp, false);
if (exp !== 'hasOwnProperty') {
// Only cache the value if it's not going to mess up the cache object
// This is more performant that using Object.prototype.hasOwnProperty.call
cache[exp] = parsedExpression;
}
return parsedExpression;
case 'function':
return exp;
default:
return noop;
}
};
}];
}