4d0614fd0d
Closes #9969
1290 lines
38 KiB
JavaScript
1290 lines
38 KiB
JavaScript
'use strict';
|
||
|
||
var $parseMinErr = minErr('$parse');
|
||
var promiseWarningCache = {};
|
||
var promiseWarning;
|
||
|
||
// 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")')
|
||
//
|
||
// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits
|
||
// against the expression language, but not to prevent exploits that were enabled by exposing
|
||
// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good
|
||
// practice and therefore we are not even trying to protect against interaction with an object
|
||
// explicitly exposed in this way.
|
||
//
|
||
// In general, it is not possible to access a Window object from an angular expression unless a
|
||
// window or some DOM object that has a reference to window is published onto a Scope.
|
||
// Similarly we prevent invocations of function known to be dangerous, as well as assignments to
|
||
// native objects.
|
||
//
|
||
// See https://docs.angularjs.org/guide/security
|
||
|
||
|
||
function ensureSafeMemberName(name, fullExpression) {
|
||
if (name === "__defineGetter__" || name === "__defineSetter__"
|
||
|| name === "__lookupGetter__" || name === "__lookupSetter__"
|
||
|| name === "__proto__") {
|
||
throw $parseMinErr('isecfld',
|
||
'Attempting to access a disallowed field in Angular expressions! '
|
||
+'Expression: {0}', fullExpression);
|
||
}
|
||
return name;
|
||
}
|
||
|
||
function ensureSafeObject(obj, fullExpression) {
|
||
// nifty check if obj is Function that is fast and works across iframes and other contexts
|
||
if (obj) {
|
||
if (obj.constructor === obj) {
|
||
throw $parseMinErr('isecfn',
|
||
'Referencing Function in Angular expressions is disallowed! Expression: {0}',
|
||
fullExpression);
|
||
} else if (// isWindow(obj)
|
||
obj.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.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) {
|
||
throw $parseMinErr('isecdom',
|
||
'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}',
|
||
fullExpression);
|
||
} else if (// block Object so that we can't get hold of dangerous Object.* methods
|
||
obj === Object) {
|
||
throw $parseMinErr('isecobj',
|
||
'Referencing Object in Angular expressions is disallowed! Expression: {0}',
|
||
fullExpression);
|
||
}
|
||
}
|
||
return obj;
|
||
}
|
||
|
||
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;},
|
||
'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);}
|
||
};
|
||
/* jshint bitwise: true */
|
||
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
|
||
|
||
|
||
/////////////////////////////////////////
|
||
|
||
|
||
/**
|
||
* @constructor
|
||
*/
|
||
var Lexer = function (options) {
|
||
this.options = options;
|
||
};
|
||
|
||
Lexer.prototype = {
|
||
constructor: Lexer,
|
||
|
||
lex: function (text) {
|
||
this.text = text;
|
||
|
||
this.index = 0;
|
||
this.ch = undefined;
|
||
this.lastCh = ':'; // can start regexp
|
||
|
||
this.tokens = [];
|
||
|
||
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();
|
||
} else if (this.is('(){}[].,;:?')) {
|
||
this.tokens.push({
|
||
index: this.index,
|
||
text: this.ch
|
||
});
|
||
this.index++;
|
||
} else if (this.isWhitespace(this.ch)) {
|
||
this.index++;
|
||
continue;
|
||
} 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
|
||
});
|
||
this.index += 1;
|
||
} else {
|
||
this.throwError('Unexpected next character ', this.index, this.index + 1);
|
||
}
|
||
}
|
||
this.lastCh = this.ch;
|
||
}
|
||
return this.tokens;
|
||
},
|
||
|
||
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) {
|
||
// IE treats non-breaking space as \u00A0
|
||
return (ch === ' ' || ch === '\r' || ch === '\t' ||
|
||
ch === '\n' || ch === '\v' || ch === '\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,
|
||
literal: true,
|
||
constant: 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;
|
||
}
|
||
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.literal = true;
|
||
token.constant = true;
|
||
} else {
|
||
var getter = getterFn(ident, this.options, this.text);
|
||
token.fn = extend(function(self, locals) {
|
||
return (getter(self, locals));
|
||
}, {
|
||
assign: function(self, value) {
|
||
return setter(self, ident, value, parser.text, parser.options);
|
||
}
|
||
});
|
||
}
|
||
|
||
this.tokens.push(token);
|
||
|
||
if (methodName) {
|
||
this.tokens.push({
|
||
index:lastDot,
|
||
text: '.'
|
||
});
|
||
this.tokens.push({
|
||
index: lastDot + 1,
|
||
text: methodName
|
||
});
|
||
}
|
||
},
|
||
|
||
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];
|
||
string = string + (rep || ch);
|
||
}
|
||
escape = false;
|
||
} else if (ch === '\\') {
|
||
escape = true;
|
||
} else if (ch === quote) {
|
||
this.index++;
|
||
this.tokens.push({
|
||
index: start,
|
||
text: rawString,
|
||
string: string,
|
||
literal: true,
|
||
constant: true,
|
||
fn: function() { return string; }
|
||
});
|
||
return;
|
||
} else {
|
||
string += ch;
|
||
}
|
||
this.index++;
|
||
}
|
||
this.throwError('Unterminated quote', start);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* @constructor
|
||
*/
|
||
var Parser = function (lexer, $filter, options) {
|
||
this.lexer = lexer;
|
||
this.$filter = $filter;
|
||
this.options = options;
|
||
};
|
||
|
||
Parser.ZERO = extend(function () {
|
||
return 0;
|
||
}, {
|
||
constant: true
|
||
});
|
||
|
||
Parser.prototype = {
|
||
constructor: Parser,
|
||
|
||
parse: function (text) {
|
||
this.text = text;
|
||
|
||
this.tokens = this.lexer.lex(text);
|
||
|
||
var value = 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);
|
||
}
|
||
primary.literal = !!token.literal;
|
||
primary.constant = !!token.constant;
|
||
}
|
||
|
||
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];
|
||
var t = token.text;
|
||
if (t === e1 || t === e2 || t === e3 || t === e4 ||
|
||
(!e1 && !e2 && !e3 && !e4)) {
|
||
return token;
|
||
}
|
||
}
|
||
return false;
|
||
},
|
||
|
||
expect: function(e1, e2, e3, e4){
|
||
var token = this.peek(e1, e2, e3, e4);
|
||
if (token) {
|
||
this.tokens.shift();
|
||
return token;
|
||
}
|
||
return false;
|
||
},
|
||
|
||
consume: function(e1){
|
||
if (!this.expect(e1)) {
|
||
this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());
|
||
}
|
||
},
|
||
|
||
unaryFn: function(fn, right) {
|
||
return extend(function(self, locals) {
|
||
return fn(self, locals, right);
|
||
}, {
|
||
constant:right.constant
|
||
});
|
||
},
|
||
|
||
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
|
||
});
|
||
},
|
||
|
||
statements: function() {
|
||
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;
|
||
};
|
||
}
|
||
}
|
||
},
|
||
|
||
filterChain: function() {
|
||
var left = this.expression();
|
||
var token;
|
||
while (true) {
|
||
if ((token = this.expect('|'))) {
|
||
left = this.binaryFn(left, token.fn, this.filter());
|
||
} else {
|
||
return left;
|
||
}
|
||
}
|
||
},
|
||
|
||
filter: function() {
|
||
var token = this.expect();
|
||
var fn = this.$filter(token.text);
|
||
var argsFn = [];
|
||
while (true) {
|
||
if ((token = this.expect(':'))) {
|
||
argsFn.push(this.expression());
|
||
} else {
|
||
var fnInvoke = function(self, locals, input) {
|
||
var args = [input];
|
||
for (var i = 0; i < argsFn.length; i++) {
|
||
args.push(argsFn[i](self, locals));
|
||
}
|
||
return fn.apply(self, args);
|
||
};
|
||
return function() {
|
||
return fnInvoke;
|
||
};
|
||
}
|
||
}
|
||
},
|
||
|
||
expression: function() {
|
||
return this.assignment();
|
||
},
|
||
|
||
assignment: function() {
|
||
var left = this.ternary();
|
||
var right;
|
||
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);
|
||
}
|
||
right = this.ternary();
|
||
return function(scope, locals) {
|
||
return left.assign(scope, right(scope, locals), locals);
|
||
};
|
||
}
|
||
return left;
|
||
},
|
||
|
||
ternary: function() {
|
||
var left = this.logicalOR();
|
||
var middle;
|
||
var token;
|
||
if ((token = this.expect('?'))) {
|
||
middle = this.assignment();
|
||
if ((token = this.expect(':'))) {
|
||
return this.ternaryFn(left, middle, this.assignment());
|
||
} else {
|
||
this.throwError('expected :', token);
|
||
}
|
||
} else {
|
||
return left;
|
||
}
|
||
},
|
||
|
||
logicalOR: function() {
|
||
var left = this.logicalAND();
|
||
var token;
|
||
while (true) {
|
||
if ((token = this.expect('||'))) {
|
||
left = this.binaryFn(left, token.fn, this.logicalAND());
|
||
} else {
|
||
return left;
|
||
}
|
||
}
|
||
},
|
||
|
||
logicalAND: function() {
|
||
var left = this.equality();
|
||
var token;
|
||
if ((token = this.expect('&&'))) {
|
||
left = this.binaryFn(left, token.fn, this.logicalAND());
|
||
}
|
||
return left;
|
||
},
|
||
|
||
equality: function() {
|
||
var left = this.relational();
|
||
var token;
|
||
if ((token = this.expect('==','!=','===','!=='))) {
|
||
left = this.binaryFn(left, token.fn, this.equality());
|
||
}
|
||
return left;
|
||
},
|
||
|
||
relational: function() {
|
||
var left = this.additive();
|
||
var token;
|
||
if ((token = this.expect('<', '>', '<=', '>='))) {
|
||
left = this.binaryFn(left, token.fn, this.relational());
|
||
}
|
||
return left;
|
||
},
|
||
|
||
additive: function() {
|
||
var left = this.multiplicative();
|
||
var token;
|
||
while ((token = this.expect('+','-'))) {
|
||
left = this.binaryFn(left, token.fn, this.multiplicative());
|
||
}
|
||
return left;
|
||
},
|
||
|
||
multiplicative: function() {
|
||
var left = this.unary();
|
||
var token;
|
||
while ((token = this.expect('*','/','%'))) {
|
||
left = this.binaryFn(left, token.fn, this.unary());
|
||
}
|
||
return left;
|
||
},
|
||
|
||
unary: function() {
|
||
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());
|
||
} else {
|
||
return this.primary();
|
||
}
|
||
},
|
||
|
||
fieldAccess: function(object) {
|
||
var parser = this;
|
||
var field = this.expect().text;
|
||
var getter = getterFn(field, this.options, this.text);
|
||
|
||
return extend(function(scope, locals, self) {
|
||
return getter(self || object(scope, locals));
|
||
}, {
|
||
assign: function(scope, value, locals) {
|
||
var o = object(scope, locals);
|
||
if (!o) object.assign(scope, o = {});
|
||
return setter(o, field, value, parser.text, parser.options);
|
||
}
|
||
});
|
||
},
|
||
|
||
objectIndex: function(obj) {
|
||
var parser = this;
|
||
|
||
var indexFn = this.expression();
|
||
this.consume(']');
|
||
|
||
return extend(function(self, locals) {
|
||
var o = obj(self, locals),
|
||
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) {
|
||
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 = ensureSafeMemberName(indexFn(self, locals), parser.text);
|
||
// prevent overwriting of Function.constructor which would break ensureSafeObject check
|
||
var o = ensureSafeObject(obj(self, locals), parser.text);
|
||
if (!o) obj.assign(self, o = {});
|
||
return o[key] = value;
|
||
}
|
||
});
|
||
},
|
||
|
||
functionCall: function(fn, contextGetter) {
|
||
var argsFn = [];
|
||
if (this.peekToken().text !== ')') {
|
||
do {
|
||
argsFn.push(this.expression());
|
||
} while (this.expect(','));
|
||
}
|
||
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(ensureSafeObject(argsFn[i](scope, locals), parser.text));
|
||
}
|
||
var fnPtr = fn(scope, locals, context) || noop;
|
||
|
||
ensureSafeObject(context, parser.text);
|
||
ensureSafeFunction(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]);
|
||
|
||
return ensureSafeObject(v, parser.text);
|
||
};
|
||
},
|
||
|
||
// This is used with json array declaration
|
||
arrayDeclaration: function () {
|
||
var elementFns = [];
|
||
var allConstant = true;
|
||
if (this.peekToken().text !== ']') {
|
||
do {
|
||
if (this.peek(']')) {
|
||
// Support trailing commas per ES5.1.
|
||
break;
|
||
}
|
||
var elementFn = this.expression();
|
||
elementFns.push(elementFn);
|
||
if (!elementFn.constant) {
|
||
allConstant = false;
|
||
}
|
||
} while (this.expect(','));
|
||
}
|
||
this.consume(']');
|
||
|
||
return extend(function(self, locals) {
|
||
var array = [];
|
||
for (var i = 0; i < elementFns.length; i++) {
|
||
array.push(elementFns[i](self, locals));
|
||
}
|
||
return array;
|
||
}, {
|
||
literal: true,
|
||
constant: allConstant
|
||
});
|
||
},
|
||
|
||
object: function () {
|
||
var keyValues = [];
|
||
var allConstant = true;
|
||
if (this.peekToken().text !== '}') {
|
||
do {
|
||
if (this.peek('}')) {
|
||
// Support trailing commas per ES5.1.
|
||
break;
|
||
}
|
||
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(','));
|
||
}
|
||
this.consume('}');
|
||
|
||
return extend(function(self, locals) {
|
||
var object = {};
|
||
for (var i = 0; i < keyValues.length; i++) {
|
||
var keyValue = keyValues[i];
|
||
object[keyValue.key] = keyValue.value(self, locals);
|
||
}
|
||
return object;
|
||
}, {
|
||
literal: true,
|
||
constant: allConstant
|
||
});
|
||
}
|
||
};
|
||
|
||
|
||
//////////////////////////////////////////////////
|
||
// Parser helper functions
|
||
//////////////////////////////////////////////////
|
||
|
||
function setter(obj, path, setValue, fullExp, options) {
|
||
ensureSafeObject(obj, fullExp);
|
||
|
||
//needed?
|
||
options = options || {};
|
||
|
||
var element = path.split('.'), key;
|
||
for (var i = 0; element.length > 1; i++) {
|
||
key = ensureSafeMemberName(element.shift(), fullExp);
|
||
var propertyObj = ensureSafeObject(obj[key], fullExp);
|
||
if (!propertyObj) {
|
||
propertyObj = {};
|
||
obj[key] = propertyObj;
|
||
}
|
||
obj = propertyObj;
|
||
if (obj.then && options.unwrapPromises) {
|
||
promiseWarning(fullExp);
|
||
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);
|
||
ensureSafeObject(obj[key], fullExp);
|
||
obj[key] = setValue;
|
||
return setValue;
|
||
}
|
||
|
||
var getterFnCacheDefault = {};
|
||
var getterFnCacheExpensive = {};
|
||
|
||
function isPossiblyDangerousMemberName(name) {
|
||
return name == 'constructor';
|
||
}
|
||
|
||
/**
|
||
* 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, options) {
|
||
ensureSafeMemberName(key0, fullExp);
|
||
ensureSafeMemberName(key1, fullExp);
|
||
ensureSafeMemberName(key2, fullExp);
|
||
ensureSafeMemberName(key3, fullExp);
|
||
ensureSafeMemberName(key4, fullExp);
|
||
var eso = function(o) {
|
||
return ensureSafeObject(o, fullExp);
|
||
};
|
||
var expensiveChecks = options.expensiveChecks;
|
||
var eso0 = (expensiveChecks || isPossiblyDangerousMemberName(key0)) ? eso : identity;
|
||
var eso1 = (expensiveChecks || isPossiblyDangerousMemberName(key1)) ? eso : identity;
|
||
var eso2 = (expensiveChecks || isPossiblyDangerousMemberName(key2)) ? eso : identity;
|
||
var eso3 = (expensiveChecks || isPossiblyDangerousMemberName(key3)) ? eso : identity;
|
||
var eso4 = (expensiveChecks || isPossiblyDangerousMemberName(key4)) ? eso : identity;
|
||
|
||
return !options.unwrapPromises
|
||
? function cspSafeGetter(scope, locals) {
|
||
var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope;
|
||
|
||
if (pathVal == null) return pathVal;
|
||
pathVal = eso0(pathVal[key0]);
|
||
|
||
if (!key1) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = eso1(pathVal[key1]);
|
||
|
||
if (!key2) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = eso2(pathVal[key2]);
|
||
|
||
if (!key3) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = eso3(pathVal[key3]);
|
||
|
||
if (!key4) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = eso4(pathVal[key4]);
|
||
|
||
return pathVal;
|
||
}
|
||
: function cspSafePromiseEnabledGetter(scope, locals) {
|
||
var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope,
|
||
promise;
|
||
|
||
if (pathVal == null) return pathVal;
|
||
|
||
pathVal = eso0(pathVal[key0]);
|
||
if (pathVal && pathVal.then) {
|
||
promiseWarning(fullExp);
|
||
if (!("$$v" in pathVal)) {
|
||
promise = pathVal;
|
||
promise.$$v = undefined;
|
||
promise.then(function(val) { promise.$$v = eso0(val); });
|
||
}
|
||
pathVal = eso0(pathVal.$$v);
|
||
}
|
||
|
||
if (!key1) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = eso1(pathVal[key1]);
|
||
if (pathVal && pathVal.then) {
|
||
promiseWarning(fullExp);
|
||
if (!("$$v" in pathVal)) {
|
||
promise = pathVal;
|
||
promise.$$v = undefined;
|
||
promise.then(function(val) { promise.$$v = eso1(val); });
|
||
}
|
||
pathVal = eso1(pathVal.$$v);
|
||
}
|
||
|
||
if (!key2) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = eso2(pathVal[key2]);
|
||
if (pathVal && pathVal.then) {
|
||
promiseWarning(fullExp);
|
||
if (!("$$v" in pathVal)) {
|
||
promise = pathVal;
|
||
promise.$$v = undefined;
|
||
promise.then(function(val) { promise.$$v = eso2(val); });
|
||
}
|
||
pathVal = eso2(pathVal.$$v);
|
||
}
|
||
|
||
if (!key3) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = eso3(pathVal[key3]);
|
||
if (pathVal && pathVal.then) {
|
||
promiseWarning(fullExp);
|
||
if (!("$$v" in pathVal)) {
|
||
promise = pathVal;
|
||
promise.$$v = undefined;
|
||
promise.then(function(val) { promise.$$v = eso3(val); });
|
||
}
|
||
pathVal = eso3(pathVal.$$v);
|
||
}
|
||
|
||
if (!key4) return pathVal;
|
||
if (pathVal == null) return undefined;
|
||
pathVal = eso4(pathVal[key4]);
|
||
if (pathVal && pathVal.then) {
|
||
promiseWarning(fullExp);
|
||
if (!("$$v" in pathVal)) {
|
||
promise = pathVal;
|
||
promise.$$v = undefined;
|
||
promise.then(function(val) { promise.$$v = eso4(val); });
|
||
}
|
||
pathVal = eso4(pathVal.$$v);
|
||
}
|
||
return pathVal;
|
||
};
|
||
}
|
||
|
||
function getterFnWithExtraArgs(fn, fullExpression) {
|
||
return function(s, l) {
|
||
return fn(s, l, promiseWarning, ensureSafeObject, fullExpression);
|
||
};
|
||
}
|
||
|
||
function getterFn(path, options, fullExp) {
|
||
var expensiveChecks = options.expensiveChecks;
|
||
var getterFnCache = (expensiveChecks ? getterFnCacheExpensive : getterFnCacheDefault);
|
||
// 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;
|
||
|
||
// http://jsperf.com/angularjs-parse-getter/6
|
||
if (options.csp) {
|
||
if (pathKeysLength < 6) {
|
||
fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp,
|
||
options);
|
||
} else {
|
||
fn = function(scope, locals) {
|
||
var i = 0, val;
|
||
do {
|
||
val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++],
|
||
pathKeys[i++], fullExp, options)(scope, locals);
|
||
|
||
locals = undefined; // clear after first iteration
|
||
scope = val;
|
||
} while (i < pathKeysLength);
|
||
return val;
|
||
};
|
||
}
|
||
} else {
|
||
var code = 'var p;\n';
|
||
if (expensiveChecks) {
|
||
code += 's = eso(s, fe);\nl = eso(l, fe);\n';
|
||
}
|
||
var needsEnsureSafeObject = expensiveChecks;
|
||
forEach(pathKeys, function(key, index) {
|
||
ensureSafeMemberName(key, fullExp);
|
||
var lookupJs = (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
|
||
: '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '["' + key + '"]';
|
||
var wrapWithEso = expensiveChecks || isPossiblyDangerousMemberName(key);
|
||
if (wrapWithEso) {
|
||
lookupJs = 'eso(' + lookupJs + ', fe)';
|
||
needsEnsureSafeObject = true;
|
||
}
|
||
code += 'if(s == null) return undefined;\n' +
|
||
's=' + lookupJs + ';\n';
|
||
if (options.unwrapPromises) {
|
||
code += 'if (s && s.then) {\n' +
|
||
' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' +
|
||
' if (!("$$v" in s)) {\n' +
|
||
' p=s;\n' +
|
||
' p.$$v = undefined;\n' +
|
||
' p.then(function(v) {p.$$v=' + (wrapWithEso ? 'eso(v)' : 'v') + ';});\n' +
|
||
'}\n' +
|
||
' s=' + (wrapWithEso ? 'eso(s.$$v)' : 's.$$v') + '\n' +
|
||
'}\n';
|
||
|
||
}
|
||
});
|
||
code += 'return s;';
|
||
|
||
/* jshint -W054 */
|
||
// s=scope, l=locals, pw=promiseWarning, eso=ensureSafeObject, fe=fullExpression
|
||
var evaledFnGetter = new Function('s', 'l', 'pw', 'eso', 'fe', code);
|
||
/* jshint +W054 */
|
||
evaledFnGetter.toString = valueFn(code);
|
||
if (needsEnsureSafeObject || options.unwrapPromises) {
|
||
evaledFnGetter = getterFnWithExtraArgs(evaledFnGetter, fullExp);
|
||
}
|
||
fn = evaledFnGetter;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
///////////////////////////////////
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $parse
|
||
* @kind function
|
||
*
|
||
* @description
|
||
*
|
||
* Converts Angular {@link guide/expression expression} into a function.
|
||
*
|
||
* ```js
|
||
* 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');
|
||
* ```
|
||
*
|
||
*
|
||
* @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
|
||
* are evaluated against (typically a scope object).
|
||
* * `locals` – `{object=}` – local variables context object, useful for overriding values in
|
||
* `context`.
|
||
*
|
||
* 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.
|
||
*
|
||
*/
|
||
|
||
|
||
/**
|
||
* @ngdoc provider
|
||
* @name $parseProvider
|
||
* @kind function
|
||
*
|
||
* @description
|
||
* `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse}
|
||
* service.
|
||
*/
|
||
function $ParseProvider() {
|
||
var cacheDefault = {};
|
||
var cacheExpensive = {};
|
||
|
||
var $parseOptions = {
|
||
csp: false,
|
||
unwrapPromises: false,
|
||
logPromiseWarnings: true,
|
||
expensiveChecks: false
|
||
};
|
||
|
||
|
||
/**
|
||
* @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future.
|
||
*
|
||
* @ngdoc method
|
||
* @name $parseProvider#unwrapPromises
|
||
* @description
|
||
*
|
||
* **This feature is deprecated, see deprecation notes below for more info**
|
||
*
|
||
* If set to true (default is false), $parse will unwrap promises automatically when a promise is
|
||
* found at any part of the expression. In other words, if set to true, the expression will always
|
||
* result in a non-promise value.
|
||
*
|
||
* While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled,
|
||
* the fulfillment value is used in place of the promise while evaluating the expression.
|
||
*
|
||
* **Deprecation notice**
|
||
*
|
||
* This is a feature that didn't prove to be wildly useful or popular, primarily because of the
|
||
* dichotomy between data access in templates (accessed as raw values) and controller code
|
||
* (accessed as promises).
|
||
*
|
||
* In most code we ended up resolving promises manually in controllers anyway and thus unifying
|
||
* the model access there.
|
||
*
|
||
* Other downsides of automatic promise unwrapping:
|
||
*
|
||
* - when building components it's often desirable to receive the raw promises
|
||
* - adds complexity and slows down expression evaluation
|
||
* - makes expression code pre-generation unattractive due to the amount of code that needs to be
|
||
* generated
|
||
* - makes IDE auto-completion and tool support hard
|
||
*
|
||
* **Warning Logs**
|
||
*
|
||
* If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a
|
||
* promise (to reduce the noise, each expression is logged only once). To disable this logging use
|
||
* `$parseProvider.logPromiseWarnings(false)` api.
|
||
*
|
||
*
|
||
* @param {boolean=} value New value.
|
||
* @returns {boolean|self} Returns the current setting when used as getter and self if used as
|
||
* setter.
|
||
*/
|
||
this.unwrapPromises = function(value) {
|
||
if (isDefined(value)) {
|
||
$parseOptions.unwrapPromises = !!value;
|
||
return this;
|
||
} else {
|
||
return $parseOptions.unwrapPromises;
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future.
|
||
*
|
||
* @ngdoc method
|
||
* @name $parseProvider#logPromiseWarnings
|
||
* @description
|
||
*
|
||
* Controls whether Angular should log a warning on any encounter of a promise in an expression.
|
||
*
|
||
* The default is set to `true`.
|
||
*
|
||
* This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well.
|
||
*
|
||
* @param {boolean=} value New value.
|
||
* @returns {boolean|self} Returns the current setting when used as getter and self if used as
|
||
* setter.
|
||
*/
|
||
this.logPromiseWarnings = function(value) {
|
||
if (isDefined(value)) {
|
||
$parseOptions.logPromiseWarnings = value;
|
||
return this;
|
||
} else {
|
||
return $parseOptions.logPromiseWarnings;
|
||
}
|
||
};
|
||
|
||
|
||
this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) {
|
||
$parseOptions.csp = $sniffer.csp;
|
||
var $parseOptionsExpensive = {
|
||
csp: $parseOptions.csp,
|
||
unwrapPromises: $parseOptions.unwrapPromises,
|
||
logPromiseWarnings: $parseOptions.logPromiseWarnings,
|
||
expensiveChecks: true
|
||
};
|
||
|
||
promiseWarning = function promiseWarningFn(fullExp) {
|
||
if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return;
|
||
promiseWarningCache[fullExp] = true;
|
||
$log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' +
|
||
'Automatic unwrapping of promises in Angular expressions is deprecated.');
|
||
};
|
||
|
||
return function(exp, expensiveChecks) {
|
||
var parsedExpression;
|
||
|
||
switch (typeof exp) {
|
||
case 'string':
|
||
|
||
var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
|
||
if (cache.hasOwnProperty(exp)) {
|
||
return cache[exp];
|
||
}
|
||
|
||
var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
|
||
var lexer = new Lexer(parseOptions);
|
||
var parser = new Parser(lexer, $filter, parseOptions);
|
||
parsedExpression = parser.parse(exp);
|
||
|
||
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;
|
||
}
|
||
};
|
||
}];
|
||
}
|