targaryen
Version:
Test Firebase security rules without connecting to Firebase.
218 lines (167 loc) • 6.05 kB
JavaScript
/**
* Node handling Object's property expressions validation and evaluation.
*/
'use strict';
const stringMethods = require('../string-methods');
const base = require('./base');
const types = require('../types');
const scopeFactory = require('../scope');
const Node = base.Node;
const ParseError = base.ParseError;
class MemberNode extends Node {
static get properties() {
return {
Query: {
orderByKey: 'boolean',
orderByPriority: 'boolean',
orderByValue: 'boolean',
orderByChild: 'string',
startAt: 'primitive',
endAt: 'primitive',
equalTo: 'primitive',
limitToFirst: 'number',
limitToLast: 'number'
},
string: {
contains: {name: 'contains', args: ['string'], returnType: 'boolean'},
beginsWith: {name: 'beginsWith', args: ['string'], returnType: 'boolean'},
endsWith: {name: 'endsWith', args: ['string'], returnType: 'boolean'},
replace: {name: 'replace', args: ['string', 'string'], returnType: 'string'},
toLowerCase: {name: 'toLowerCase', args: [], returnType: 'string'},
toUpperCase: {name: 'toUpperCase', args: [], returnType: 'string'},
matches: {name: 'matches', args: ['RegExp'], returnType: 'boolean'},
length: 'number'
},
RuleDataSnapshot: {
val: {name: 'val', args: [], returnType: 'primitive'},
child: {name: 'child', args: ['string'], returnType: 'RuleDataSnapshot'},
parent: {name: 'parent', args: [], returnType: 'RuleDataSnapshot'},
hasChild: {name: 'hasChild', args: ['string'], returnType: 'boolean'},
hasChildren: {
name: 'hasChildren',
args(scope, fnNode, mustComply) {
if (fnNode.arguments.length === 0) {
return;
}
if (fnNode.arguments.length > 1) {
throw new ParseError(fnNode, 'Too many arguments to hasChildren');
}
const keyList = fnNode.arguments[0];
if (!keyList.elements) {
throw new ParseError(fnNode, 'hasChildren takes 1 argument: an array of strings');
}
if (keyList.elements.length === 0) {
throw new ParseError(fnNode, 'hasChildren got an empty array, expected some strings in there');
}
keyList.elements.forEach(
element => fnNode.assertType(element.inferredType, 'string', {
mustComply,
msg: 'hasChildren got an array with a non-string value'
})
);
},
returnType: 'boolean'
},
exists: {name: 'exists', args: [], returnType: 'boolean'},
getPriority: {name: 'getPriority', args: [], returnType: 'any'},
isNumber: {name: 'isNumber', args: [], returnType: 'boolean'},
isString: {name: 'isString', args: [], returnType: 'boolean'},
isBoolean: {name: 'iBoolean', args: [], returnType: 'boolean'}
}
};
}
init(source, astNode, scope) {
this.object = Node.from(source, astNode.object, scope);
const msg = `No such method/property ${this.astNode.property.name}`;
let objectType = this.object.inferredType;
this.assertType(objectType, ['Query', 'RuleDataSnapshot', 'string'], {msg});
if (types.isPrimitive(objectType)) {
objectType = this.object.inferredType = 'string';
}
if (this.computed) {
this.property = Node.from(source, astNode.property, scope);
return;
}
try {
this.property = Node.from(
source,
astNode.property,
objectType === 'any' ? scopeFactory.any() : scopeFactory.create(MemberNode.properties[objectType])
);
} catch (e) {
throw new ParseError(this, msg);
}
}
get computed() {
return this.astNode.computed;
}
inferType() {
if (!this.computed) {
return this.property.inferredType;
}
const msg = 'Invalid property access.';
const objectType = this.object.inferredType;
if (types.isFuzzy(objectType)) {
return 'any';
}
if (this.property.type !== 'Literal') {
throw new ParseError(this, msg);
}
const scope = MemberNode.properties[objectType];
if (scope == null) {
throw new ParseError(this, msg);
}
const type = scope[this.property.value];
if (type == null) {
throw new ParseError(this, msg);
}
return type;
}
inferAsStringMethod() {
this.object.inferredType = 'string';
if (!this.computed) {
this.property.inferredType = this.property.inferType(scopeFactory.create(MemberNode.properties.string));
}
return this.inferType();
}
evaluate(state) {
const object = this.object.evaluate(state);
const key = this.computed ? this.property.evaluate(state) : this.property.name;
return this.evaluateWith(state, object, key);
}
debug(state, cb) {
const object = this.object.debug(state, cb);
let detailed, key;
if (this.computed) {
const ev = this.property.debug(state, cb);
key = ev.value;
detailed = `${object.detailed}[${ev.detailed}]`;
} else {
key = this.property.name;
detailed = `${object.detailed}.${key}`;
}
const value = this.evaluateWith(state, object.value, key);
cb({
type: this.astNode.type,
original: this.original,
detailed,
value
});
return {detailed, value};
}
evaluateWith(state, object, key) {
const isPatched = types.isString(types.from(object)) && stringMethods[key];
const property = isPatched ? stringMethods[key] : (object == null ? null : object[key]);
if (property === undefined) {
return null;
}
if (typeof property !== 'function') {
return property;
}
return isPatched ? property.bind(null, object) : property.bind(object);
}
toString() {
return this.computer ? `${this.object}[${this.property}]` : `${this.object}.${this.property}`;
}
}
Node.register('MemberExpression', MemberNode);