UNPKG

firebase-bolt

Version:

Firebase Bolt Security and Modeling Language Compiler

1,338 lines (1,330 loc) 748 kB
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ "use strict"; exports.__esModule = true; /* * AST builders for Firebase Rules Language. * * Copyright 2015 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var util = require("./util"); var logger = require("./logger"); var errors = { typeMismatch: "Unexpected type: ", duplicatePathPart: "A path component name is duplicated: " }; ; ; var PathPart = /** @class */ (function () { // "label", undefined - static path part // "$label", X - variable path part // X, !undefined - variable path part function PathPart(label, variable) { if (label[0] === '$' && variable === undefined) { variable = label; } if (variable && label[0] !== '$') { label = '$' + label; } this.label = label; this.variable = variable; } return PathPart; }()); exports.PathPart = PathPart; var PathTemplate = /** @class */ (function () { function PathTemplate(parts) { if (parts === void 0) { parts = []; } this.parts = parts.map(function (part) { if (util.isType(part, 'string')) { return new PathPart(part); } else { return part; } }); } PathTemplate.prototype.copy = function () { var result = new PathTemplate(); result.push(this); return result; }; PathTemplate.prototype.getLabels = function () { return this.parts.map(function (part) { return part.label; }); }; // Mapping from variables to JSON labels PathTemplate.prototype.getScope = function () { var result = {}; this.parts.forEach(function (part) { if (part.variable) { if (result[part.variable]) { throw new Error(errors.duplicatePathPart + part.variable); } result[part.variable] = literal(part.label); } }); return result; }; PathTemplate.prototype.push = function (temp) { util.extendArray(this.parts, temp.parts); }; PathTemplate.prototype.pop = function (temp) { var _this = this; temp.parts.forEach(function (part) { _this.parts.pop(); }); }; PathTemplate.prototype.length = function () { return this.parts.length; }; PathTemplate.prototype.getPart = function (i) { if (i > this.parts.length || i < -this.parts.length) { var l = this.parts.length; throw new Error("Path reference out of bounds: " + i + " [" + -l + " .. " + l + "]"); } if (i < 0) { return this.parts[this.parts.length + i]; } return this.parts[i]; }; return PathTemplate; }()); exports.PathTemplate = PathTemplate; ; var Schema = /** @class */ (function () { function Schema() { } Schema.isGeneric = function (schema) { return schema.params !== undefined && schema.params.length > 0; }; return Schema; }()); exports.Schema = Schema; ; exports.string = valueGen('String'); exports.boolean = valueGen('Boolean'); exports.number = valueGen('Number'); exports.array = valueGen('Array'); exports.neg = opGen('neg', 1); exports.not = opGen('!', 1); exports.mult = opGen('*'); exports.div = opGen('/'); exports.mod = opGen('%'); exports.add = opGen('+'); exports.sub = opGen('-'); exports.eq = opGen('=='); exports.lt = opGen('<'); exports.lte = opGen('<='); exports.gt = opGen('>'); exports.gte = opGen('>='); exports.ne = opGen('!='); exports.and = opGen('&&'); exports.or = opGen('||'); exports.ternary = opGen('?:', 3); exports.value = opGen('value', 1); function variable(name) { return { type: 'var', valueType: 'Any', name: name }; } exports.variable = variable; function literal(name) { return { type: 'literal', valueType: 'Any', name: name }; } exports.literal = literal; function nullType() { return { type: 'Null', valueType: 'Null' }; } exports.nullType = nullType; function reference(base, prop) { return { type: 'ref', valueType: 'Any', base: base, accessor: prop }; } exports.reference = reference; var reIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_]*$/; function isIdentifierStringExp(exp) { return exp.type === 'String' && reIdentifier.test(exp.value); } exports.isIdentifierStringExp = isIdentifierStringExp; // Shallow copy of an expression (so it can be modified and preserve // immutability of the original expression). function copyExp(exp) { exp = util.extend({}, exp); switch (exp.type) { case 'op': case 'call': var opExp = exp; opExp.args = util.copyArray(opExp.args); return opExp; case 'union': var unionExp = exp; unionExp.types = util.copyArray(unionExp.types); return unionExp; case 'generic': var genericExp = exp; genericExp.params = util.copyArray(genericExp.params); return genericExp; default: return exp; } } exports.copyExp = copyExp; // Make a (shallow) copy of the base expression, setting (or removing) it's // valueType. // // valueType is a string indicating the type of evaluating an expression (e.g. // 'Snapshot') - used to know when type coercion is needed in the context // of parent expressions. function cast(base, valueType) { var result = copyExp(base); result.valueType = valueType; return result; } exports.cast = cast; function call(ref, args) { if (args === void 0) { args = []; } return { type: 'call', valueType: 'Any', ref: ref, args: args }; } exports.call = call; // Return empty string if not a function. function getFunctionName(exp) { if (exp.ref.type === 'ref') { return ''; } return exp.ref.name; } exports.getFunctionName = getFunctionName; // Return empty string if not a (simple) method call -- ref.fn() function getMethodName(exp) { if (exp.ref.type === 'var') { return exp.ref.name; } if (exp.ref.type !== 'ref') { return ''; } return getPropName(exp.ref); } exports.getMethodName = getMethodName; function getPropName(ref) { if (ref.accessor.type !== 'String') { return ''; } return ref.accessor.value; } exports.getPropName = getPropName; // TODO: Type of function signature does not fail this declaration? function builtin(fn) { return { type: 'builtin', valueType: 'Any', fn: fn }; } exports.builtin = builtin; function snapshotVariable(name) { return cast(variable(name), 'Snapshot'); } exports.snapshotVariable = snapshotVariable; function snapshotParent(base) { if (base.valueType !== 'Snapshot') { throw new Error(errors.typeMismatch + "expected Snapshot"); } return cast(call(reference(cast(base, 'Any'), exports.string('parent'))), 'Snapshot'); } exports.snapshotParent = snapshotParent; function ensureValue(exp) { if (exp.valueType === 'Snapshot') { return snapshotValue(exp); } return exp; } exports.ensureValue = ensureValue; // ref.val() function snapshotValue(exp) { return call(reference(cast(exp, 'Any'), exports.string('val'))); } exports.snapshotValue = snapshotValue; // Ensure expression is a boolean (when used in a boolean context). function ensureBoolean(exp) { exp = ensureValue(exp); if (isCall(exp, 'val')) { exp = exports.eq(exp, exports.boolean(true)); } return exp; } exports.ensureBoolean = ensureBoolean; function isCall(exp, methodName) { return exp.type === 'call' && exp.ref.type === 'ref' && exp.ref.accessor.type === 'String' && exp.ref.accessor.value === methodName; } exports.isCall = isCall; // Return value generating function for a given Type. function valueGen(typeName) { return function (val) { return { type: typeName, valueType: typeName, value: val // The (constant) value itself. }; }; } function regexp(pattern, modifiers) { if (modifiers === void 0) { modifiers = ""; } switch (modifiers) { case "": case "i": break; default: throw new Error("Unsupported RegExp modifier: " + modifiers); } return { type: 'RegExp', valueType: 'RegExp', value: pattern, modifiers: modifiers }; } exports.regexp = regexp; function cmpValues(v1, v2) { if (v1.type !== v2.type) { return false; } return v1.value === v2.value; } function isOp(opType, exp) { return exp.type === 'op' && exp.op === opType; } // Return a generating function to make an operator exp node. function opGen(opType, arity) { if (arity === void 0) { arity = 2; } return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } if (args.length !== arity) { throw new Error("Operator has " + args.length + " arguments (expecting " + arity + ")."); } return op(opType, args); }; } exports.andArray = leftAssociateGen('&&', exports.boolean(true), exports.boolean(false)); exports.orArray = leftAssociateGen('||', exports.boolean(false), exports.boolean(true)); // Create an expression builder function which operates on arrays of values. // Returns new expression like v1 op v2 op v3 ... // // - Any identityValue's in array input are ignored. // - If zeroValue is found - just return zeroValue. // // Our function re-orders top-level op in array elements to the resulting // expression is left-associating. E.g.: // // [a && b, c && d] => (((a && b) && c) && d) // (NOT (a && b) && (c && d)) function leftAssociateGen(opType, identityValue, zeroValue) { return function (a) { var i; function reducer(result, current) { if (result === undefined) { return current; } return op(opType, [result, current]); } // First flatten all top-level op values to one flat array. var flat = []; for (i = 0; i < a.length; i++) { flatten(opType, a[i], flat); } var result = []; for (i = 0; i < flat.length; i++) { // Remove identifyValues from array. if (cmpValues(flat[i], identityValue)) { continue; } // Just return zeroValue if found if (cmpValues(flat[i], zeroValue)) { return zeroValue; } result.push(flat[i]); } if (result.length === 0) { return identityValue; } // Return left-associative expression of opType. return result.reduce(reducer); }; } // Flatten the top level tree of op into a single flat array of expressions. function flatten(opType, exp, flat) { var i; if (flat === undefined) { flat = []; } if (!isOp(opType, exp)) { flat.push(exp); return flat; } for (i = 0; i < exp.args.length; i++) { flatten(opType, exp.args[i], flat); } return flat; } exports.flatten = flatten; function op(opType, args) { return { type: 'op', valueType: 'Any', op: opType, args: args // Arguments to the operator Array<exp> }; } exports.op = op; // Warning: NOT an expression type! function method(params, body) { return { params: params, body: body }; } exports.method = method; function typeType(typeName) { return { type: "type", valueType: "type", name: typeName }; } exports.typeType = typeType; function unionType(types) { return { type: "union", valueType: "type", types: types }; } exports.unionType = unionType; function genericType(typeName, params) { return { type: "generic", valueType: "type", name: typeName, params: params }; } exports.genericType = genericType; var Symbols = /** @class */ (function () { function Symbols() { this.functions = {}; this.paths = []; this.schema = {}; } Symbols.prototype.register = function (map, typeName, name, object) { if (map[name]) { logger.error("Duplicated " + typeName + " definition: " + name + "."); } else { map[name] = object; } return map[name]; }; Symbols.prototype.registerFunction = function (name, params, body) { return this.register(this.functions, 'functions', name, method(params, body)); }; Symbols.prototype.registerPath = function (template, isType, methods) { if (methods === void 0) { methods = {}; } isType = isType || typeType('Any'); var p = { template: template.copy(), isType: isType, methods: methods }; this.paths.push(p); return p; }; Symbols.prototype.registerSchema = function (name, derivedFrom, properties, methods, params) { if (properties === void 0) { properties = {}; } if (methods === void 0) { methods = {}; } if (params === void 0) { params = []; } derivedFrom = derivedFrom || typeType(Object.keys(properties).length > 0 ? 'Object' : 'Any'); var s = { derivedFrom: derivedFrom, properties: properties, methods: methods, params: params }; return this.register(this.schema, 'schema', name, s); }; Symbols.prototype.isDerivedFrom = function (type, ancestor) { var _this = this; if (ancestor === 'Any') { return true; } switch (type.type) { case 'type': case 'generic': var simpleType = type; if (simpleType.name === ancestor) { return true; } if (simpleType.name === 'Any') { return false; } var schema = this.schema[simpleType.name]; if (!schema) { return false; } return this.isDerivedFrom(schema.derivedFrom, ancestor); case 'union': return type.types .map(function (subType) { return _this.isDerivedFrom(subType, ancestor); }) .reduce(util.or); default: throw new Error("Unknown type: " + type.type); } }; return Symbols; }()); exports.Symbols = Symbols; var JS_OPS = { 'value': { rep: "", p: 18 }, 'neg': { rep: "-", p: 15 }, '!': { p: 15 }, '*': { p: 14 }, '/': { p: 14 }, '%': { p: 14 }, '+': { p: 13 }, '-': { p: 13 }, '<': { p: 11 }, '<=': { p: 11 }, '>': { p: 11 }, '>=': { p: 11 }, 'in': { p: 11 }, '==': { p: 10 }, "!=": { p: 10 }, '&&': { p: 6 }, '||': { p: 5 }, '?:': { p: 4 }, ',': { p: 0 } }; // From an AST, decode as an expression (string). function decodeExpression(exp, outerPrecedence) { if (outerPrecedence === undefined) { outerPrecedence = 0; } var innerPrecedence = precedenceOf(exp); var result = ''; switch (exp.type) { case 'Boolean': case 'Number': result = JSON.stringify(exp.value); break; case 'String': result = util.quoteString(exp.value); break; // RegExp assumed to be in pre-quoted format. case 'RegExp': var regexp_1 = exp; result = '/' + regexp_1.value + '/'; if (regexp_1.modifiers !== '') { result += regexp_1.modifiers; } break; case 'Array': result = '[' + decodeArray(exp.value) + ']'; break; case 'Null': result = 'null'; break; case 'var': case 'literal': result = exp.name; break; case 'ref': var expRef = exp; if (isIdentifierStringExp(expRef.accessor)) { result = decodeExpression(expRef.base, innerPrecedence) + '.' + expRef.accessor.value; } else { result = decodeExpression(expRef.base, innerPrecedence) + '[' + decodeExpression(expRef.accessor) + ']'; } break; case 'call': var expCall = exp; result = decodeExpression(expCall.ref) + '(' + decodeArray(expCall.args) + ')'; break; case 'builtin': result = decodeExpression(exp); break; case 'op': var expOp = exp; var rep = JS_OPS[expOp.op].rep === undefined ? expOp.op : JS_OPS[expOp.op].rep; if (expOp.args.length === 1) { result = rep + decodeExpression(expOp.args[0], innerPrecedence); } else if (expOp.args.length === 2) { result = decodeExpression(expOp.args[0], innerPrecedence) + ' ' + rep + ' ' + // All ops are left associative - so nudge the innerPrecendence // down on the right hand side to force () for right-associating // operations. decodeExpression(expOp.args[1], innerPrecedence + 1); if ((innerPrecedence >= outerPrecedence) && ((expOp.op === '&&') || (expOp.op === '||'))) { result = '(' + result + ')'; } } else if (expOp.args.length === 3) { result = decodeExpression(expOp.args[0], innerPrecedence) + ' ? ' + decodeExpression(expOp.args[1], innerPrecedence) + ' : ' + decodeExpression(expOp.args[2], innerPrecedence); } break; case 'type': result = exp.name; break; case 'union': result = exp.types.map(decodeExpression).join(' | '); break; case 'generic': var genericType_1 = exp; return genericType_1.name + '<' + decodeArray(genericType_1.params) + '>'; default: result = "***UNKNOWN TYPE*** (" + exp.type + ")"; break; } if (innerPrecedence < outerPrecedence) { result = '(' + result + ')'; } return result; } exports.decodeExpression = decodeExpression; function decodeArray(args) { return args.map(decodeExpression).join(', '); } function precedenceOf(exp) { var result; switch (exp.type) { case 'op': result = JS_OPS[exp.op].p; break; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence // lists call as 17 and ref as 18 - but how could they be anything other than left to right? // http://www.scriptingmaster.com/javascript/operator-precedence.asp - agrees. case 'call': result = 18; break; case 'ref': result = 18; break; default: result = 19; break; } return result; } },{"./logger":3,"./util":7}],2:[function(require,module,exports){ "use strict"; /* * Copyright 2015 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ exports.__esModule = true; // TODO(koss): After node 0.10 leaves LTS - remove polyfilled Promise library. if (typeof Promise === 'undefined') { require('es6-promise').polyfill(); } var parser = require('./rules-parser'); var generator = require("./rules-generator"); var astImport = require("./ast"); exports.FILE_EXTENSION = 'bolt'; exports.ast = astImport; exports.parse = parser.parse; exports.Generator = generator.Generator; exports.decodeExpression = exports.ast.decodeExpression; exports.generate = generator.generate; },{"./ast":1,"./rules-generator":5,"./rules-parser":6,"es6-promise":8}],3:[function(require,module,exports){ "use strict"; exports.__esModule = true; /* * Copyright 2015 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var lastError; var lastMessage; var errorCount; var silenceOutput; var DEBUG = false; var getContext = function () { return ({}); }; reset(); function reset() { lastError = undefined; lastMessage = undefined; errorCount = 0; silenceOutput = false; } exports.reset = reset; function setDebug(debug) { if (debug === void 0) { debug = true; } DEBUG = debug; } exports.setDebug = setDebug; function silent(f) { if (f === void 0) { f = true; } silenceOutput = f; } exports.silent = silent; function setContext(fn) { getContext = fn; } exports.setContext = setContext; function error(s) { var err = errorString(s); // De-dup identical messages if (err === lastMessage) { return; } lastMessage = err; lastError = lastMessage; if (!silenceOutput) { console.error(lastError); if (DEBUG) { var e = new Error("Stack trace"); console.error(e.stack); } } errorCount += 1; } exports.error = error; function warn(s) { var err = errorString(s); // De-dup identical messages if (err === lastMessage) { return; } lastMessage = err; if (!silenceOutput) { console.warn(lastMessage); } } exports.warn = warn; function getLastMessage() { return lastMessage; } exports.getLastMessage = getLastMessage; function errorString(s) { var ctx = getContext(); if (ctx.line !== undefined && ctx.column !== undefined) { return 'bolt:' + ctx.line + ':' + ctx.column + ': ' + s; } else { return 'bolt: ' + s; } } function hasErrors() { return errorCount > 0; } exports.hasErrors = hasErrors; function errorSummary() { if (errorCount === 1) { return lastError; } if (errorCount !== 0) { return "Fatal errors: " + errorCount; } return ""; } exports.errorSummary = errorSummary; },{}],4:[function(require,module,exports){ "use strict"; exports.__esModule = true; /* * Copyright 2015 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var parser = require('./rules-parser'); function parseExpression(expression) { var result = parser.parse('function f() {return ' + expression + ';}'); return result.functions.f.body; } exports.parseExpression = parseExpression; },{"./rules-parser":6}],5:[function(require,module,exports){ "use strict"; exports.__esModule = true; /* * Copyright 2015 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var util = require("./util"); var ast = require("./ast"); var logger_1 = require("./logger"); var parser = require('./rules-parser'); var parse_util_1 = require("./parse-util"); var errors = { badIndex: "The index function must return a String or an array of Strings.", noPaths: "Must have at least one path expression.", nonObject: "Type contains properties and must extend 'Object'.", missingSchema: "Missing definition for type.", recursive: "Recursive function call.", mismatchParams: "Incorrect number of function arguments.", generateFailed: "Could not generate JSON: ", noSuchType: "No type definition for: ", badSchemaMethod: "Unsupported method name in type statement: ", badPathMethod: "Unsupported method name in path statement: ", badWriteAlias: "Cannot have both a write() method and a write-aliasing method: ", coercion: "Cannot convert value: ", undefinedFunction: "Undefined function: ", application: "Bolt application error: ", invalidGeneric: "Invalid generic schema usage: ", invalidMapKey: "Map<Key, T> - Key must derive from String type.", invalidWildChildren: "Types can have at most one $wild property and cannot mix with other properties.", invalidPropertyName: "Property names cannot contain any of: . $ # [ ] / or control characters: " }; var INVALID_KEY_REGEX = /[\[\].#$\/\u0000-\u001F\u007F]/; ; var builtinSchemaNames = ['Any', 'Null', 'String', 'Number', 'Boolean', 'Object']; // Method names allowed in Bolt files. var valueMethods = ['length', 'includes', 'startsWith', 'beginsWith', 'endsWith', 'replace', 'toLowerCase', 'toUpperCase', 'test', 'contains', 'matches']; // TODO: Make sure users don't call internal methods...make private to impl. var snapshotMethods = ['parent', 'child', 'hasChildren', 'val', 'isString', 'isNumber', 'isBoolean'].concat(valueMethods); var writeAliases = { 'create': parse_util_1.parseExpression('prior(this) == null'), 'update': parse_util_1.parseExpression('prior(this) != null && this != null'), 'delete': parse_util_1.parseExpression('prior(this) != null && this == null') }; // Usage: // json = bolt.generate(bolt-text) function generate(symbols) { if (typeof symbols === 'string') { symbols = parser.parse(symbols); } var gen = new Generator(symbols); return gen.generateRules(); } exports.generate = generate; // Symbols contains: // functions: {} // schema: {} // paths: {} var Generator = /** @class */ (function () { function Generator(symbols) { this.symbols = symbols; this.validators = {}; this.rules = {}; this.errorCount = 0; this.runSilently = false; this.allowUndefinedFunctions = false; this.keyIndex = 0; // TODO: globals should be part of this.symbols (nested scopes) this.globals = { "root": ast.call(ast.variable('@root')) }; this.registerBuiltinSchema(); } // Return Firebase compatible Rules JSON for a the given symbols definitions. Generator.prototype.generateRules = function () { var _this = this; this.errorCount = 0; var paths = this.symbols.paths; var schema = this.symbols.schema; var name; paths.forEach(function (path) { _this.validateMethods(errors.badPathMethod, path.methods, ['validate', 'read', 'write', 'index']); }); for (name in schema) { if (!util.arrayIncludes(builtinSchemaNames, name)) { this.validateMethods(errors.badSchemaMethod, schema[name].methods, ['validate', 'read', 'write']); } } if (paths.length === 0) { this.fatal(errors.noPaths); } paths.forEach(function (path) { return _this.updateRules(path); }); this.convertExpressions(this.rules); if (this.errorCount !== 0) { throw new Error(errors.generateFailed + this.errorCount + " errors."); } util.deletePropName(this.rules, '.scope'); util.pruneEmptyChildren(this.rules); return { rules: this.rules }; }; Generator.prototype.validateMethods = function (m, methods, allowed) { var _this = this; if (util.arrayIncludes(allowed, 'write')) { allowed = allowed.concat(Object.keys(writeAliases)); } for (var method in methods) { if (!util.arrayIncludes(allowed, method)) { logger_1.warn(m + util.quoteString(method) + " (allowed: " + allowed.map(util.quoteString).join(', ') + ")"); } } if ('write' in methods) { Object.keys(writeAliases).forEach(function (alias) { if (alias in methods) { _this.fatal(errors.badWriteAlias + alias); } }); } }; Generator.prototype.registerBuiltinSchema = function () { var self = this; var thisVar = ast.variable('this'); function registerAsCall(name, methodName) { self.symbols.registerSchema(name, ast.typeType('Any'), undefined, { validate: ast.method(['this'], ast.call(ast.reference(ast.cast(thisVar, 'Any'), ast.string(methodName)))) }); } this.symbols.registerSchema('Any', ast.typeType('Any'), undefined, { validate: ast.method(['this'], ast.boolean(true)) }); registerAsCall('Object', 'hasChildren'); // Because of the way firebase treats Null values, there is no way to // write a validation rule, that will EVER be called with this == null // (firebase allows values to be deleted no matter their validation rules). // So, comparing this == null will always return false -> that is what // we do here, which will be optimized away if ORed with other validations. this.symbols.registerSchema('Null', ast.typeType('Any'), undefined, { validate: ast.method(['this'], ast.boolean(false)) }); self.symbols.registerSchema('String', ast.typeType('Any'), undefined, { validate: ast.method(['this'], ast.call(ast.reference(ast.cast(thisVar, 'Any'), ast.string('isString')))), includes: ast.method(['this', 's'], ast.call(ast.reference(ast.value(thisVar), ast.string('contains')), [ast.value(ast.variable('s'))])), startsWith: ast.method(['this', 's'], ast.call(ast.reference(ast.value(thisVar), ast.string('beginsWith')), [ast.value(ast.variable('s'))])), endsWith: ast.method(['this', 's'], ast.call(ast.reference(ast.value(thisVar), ast.string('endsWith')), [ast.value(ast.variable('s'))])), replace: ast.method(['this', 's', 'r'], ast.call(ast.reference(ast.value(thisVar), ast.string('replace')), [ast.value(ast.variable('s')), ast.value(ast.variable('r'))])), test: ast.method(['this', 'r'], ast.call(ast.reference(ast.value(thisVar), ast.string('matches')), [ast.call(ast.variable('@RegExp'), [ast.variable('r')])])) }); registerAsCall('Number', 'isNumber'); registerAsCall('Boolean', 'isBoolean'); this.symbols.registerFunction('@RegExp', ['r'], ast.builtin(this.ensureType.bind(this, 'RegExp'))); var map = this.symbols.registerSchema('Map', ast.typeType('Any'), undefined, undefined, ['Key', 'Value']); map.getValidator = this.getMapValidator.bind(this); }; // type Map<Key, Value> => { // $key: { // '.validate': $key instanceof Key and this instanceof Value; // '.validate': 'newData.hasChildren()' // } // Key must derive from String Generator.prototype.getMapValidator = function (params) { var keyType = params[0]; var valueType = params[1]; if (keyType.type !== 'type' || !this.symbols.isDerivedFrom(keyType, 'String')) { throw new Error(errors.invalidMapKey + " (" + ast.decodeExpression(keyType) + " does not)"); } var validator = {}; var index = this.uniqueKey(); validator[index] = {}; extendValidator(validator, this.ensureValidator(ast.typeType('Object'))); // First validate the key (omit terminal String type validation). while (keyType.name !== 'String') { var schema = this.symbols.schema[keyType.name]; if (schema.methods['validate']) { var exp = this.partialEval(schema.methods['validate'].body, { 'this': ast.literal(index) }); extendValidator(validator[index], { '.validate': [exp] }); } keyType = schema.derivedFrom; } extendValidator(validator[index], this.ensureValidator(valueType)); return validator; }; Generator.prototype.uniqueKey = function () { this.keyIndex += 1; return '$key' + this.keyIndex; }; // Collection schema has exactly one $wildchild property Generator.prototype.isCollectionSchema = function (schema) { var props = Object.keys(schema.properties); var result = props.length === 1 && props[0][0] === '$'; return result; }; // Ensure we have a definition for a validator for the given schema. Generator.prototype.ensureValidator = function (type) { var key = ast.decodeExpression(type); if (!this.validators[key]) { this.validators[key] = { '.validate': ast.literal('***TYPE RECURSION***') }; var allowSave = this.allowUndefinedFunctions; this.allowUndefinedFunctions = true; this.validators[key] = this.createValidator(type); this.allowUndefinedFunctions = allowSave; } return this.validators[key]; }; Generator.prototype.createValidator = function (type) { var _this = this; switch (type.type) { case 'type': return this.createValidatorFromSchemaName(type.name); case 'union': var union_1 = {}; type.types.forEach(function (typePart) { // Make a copy var singleType = extendValidator({}, _this.ensureValidator(typePart)); mapValidator(singleType, ast.andArray); extendValidator(union_1, singleType); }); mapValidator(union_1, ast.orArray); return union_1; case 'generic': var genericType = type; return this.createValidatorFromGeneric(genericType.name, genericType.params); default: throw new Error(errors.application + "invalid internal type: " + type.type); } }; Generator.prototype.createValidatorFromGeneric = function (schemaName, params) { var schema = this.symbols.schema[schemaName]; if (schema === undefined || !ast.Schema.isGeneric(schema)) { throw new Error(errors.noSuchType + schemaName + " (generic)"); } var schemaParams = schema.params; if (params.length !== schemaParams.length) { throw new Error(errors.invalidGeneric + " expected <" + schemaParams.join(', ') + ">"); } // Call custom validator, if given. if (schema.getValidator) { return schema.getValidator(params); } var bindings = {}; for (var i = 0; i < params.length; i++) { bindings[schemaParams[i]] = params[i]; } // Expand generics and generate validator from schema. schema = this.replaceGenericsInSchema(schema, bindings); return this.createValidatorFromSchema(schema); }; Generator.prototype.replaceGenericsInSchema = function (schema, bindings) { var _this = this; var expandedSchema = { derivedFrom: this.replaceGenericsInExp(schema.derivedFrom, bindings), properties: {}, methods: {} }; var props = Object.keys(schema.properties); props.forEach(function (prop) { expandedSchema.properties[prop] = _this.replaceGenericsInExp(schema.properties[prop], bindings); }); var methods = Object.keys(schema.methods); methods.forEach(function (methodName) { expandedSchema.methods[methodName] = _this.replaceGenericsInMethod(schema.methods[methodName], bindings); }); return expandedSchema; }; Generator.prototype.replaceGenericsInExp = function (exp, bindings) { var self = this; function replaceGenericsInArray(exps) { return exps.map(function (expPart) { return self.replaceGenericsInExp(expPart, bindings); }); } switch (exp.type) { case 'op': case 'call': var opType = ast.copyExp(exp); opType.args = replaceGenericsInArray(opType.args); return opType; case 'type': var simpleType = exp; return bindings[simpleType.name] || simpleType; case 'union': var unionType = exp; return ast.unionType(replaceGenericsInArray(unionType.types)); case 'generic': var genericType = exp; return ast.genericType(genericType.name, replaceGenericsInArray(genericType.params)); default: return exp; } }; Generator.prototype.replaceGenericsInMethod = function (method, bindings) { var expandedMethod = { params: method.params, body: method.body }; expandedMethod.body = this.replaceGenericsInExp(method.body, bindings); return expandedMethod; }; Generator.prototype.createValidatorFromSchemaName = function (schemaName) { var schema = this.symbols.schema[schemaName]; if (!schema) { throw new Error(errors.noSuchType + schemaName); } if (ast.Schema.isGeneric(schema)) { throw new Error(errors.noSuchType + schemaName + " used as non-generic type."); } return this.createValidatorFromSchema(schema); }; Generator.prototype.createValidatorFromSchema = function (schema) { var _this = this; var hasProps = Object.keys(schema.properties).length > 0 && !this.isCollectionSchema(schema); if (hasProps && !this.symbols.isDerivedFrom(schema.derivedFrom, 'Object')) { this.fatal(errors.nonObject + " (is " + ast.decodeExpression(schema.derivedFrom) + ")"); return {}; } var validator = {}; if (!(schema.derivedFrom.type === 'type' && schema.derivedFrom.name === 'Any')) { extendValidator(validator, this.ensureValidator(schema.derivedFrom)); } var requiredProperties = []; var wildProperties = 0; Object.keys(schema.properties).forEach(function (propName) { if (propName[0] === '$') { wildProperties += 1; if (INVALID_KEY_REGEX.test(propName.slice(1))) { _this.fatal(errors.invalidPropertyName + propName); } } else { if (INVALID_KEY_REGEX.test(propName)) { _this.fatal(errors.invalidPropertyName + propName); } } if (!validator[propName]) { validator[propName] = {}; } var propType = schema.properties[propName]; if (propName[0] !== '$' && !_this.isNullableType(propType)) { requiredProperties.push(propName); } extendValidator(validator[propName], _this.ensureValidator(propType)); }); if (wildProperties > 1 || wildProperties === 1 && requiredProperties.length > 0) { this.fatal(errors.invalidWildChildren); } if (requiredProperties.length > 0) { // this.hasChildren(requiredProperties) extendValidator(validator, { '.validate': [hasChildrenExp(requiredProperties)] }); } // Disallow $other properties by default if (hasProps) { validator['$other'] = {}; extendValidator(validator['$other'], { '.validate': ast.boolean(false) }); } this.extendValidationMethods(validator, schema.methods); return validator; }; Generator.prototype.isNullableType = function (type) { var result = this.symbols.isDerivedFrom(type, 'Null') || this.symbols.isDerivedFrom(type, 'Map'); return result; }; // Update rules based on the given path expression. Generator.prototype.updateRules = function (path) { var i; var location = util.ensureObjectPath(this.rules, path.template.getLabels()); var exp; extendValidator(location, this.ensureValidator(path.isType)); location['.scope'] = path.template.getScope(); this.extendValidationMethods(location, path.methods); // Write indices if (path.methods['index']) { switch (path.methods['index'].body.type) { case 'String': exp = ast.array([path.methods['index'].body]); break; case 'Array': exp = path.methods['index'].body; break; default: this.fatal(errors.badIndex); return; } var indices = []; for (i = 0; i < exp.value.length; i++) { if (exp.value[i].type !== 'String') { this.fatal(errors.badIndex + " (not " + exp.value[i].type + ")"); } else { indices.push(exp.value[i].value); } } // TODO: Error check not over-writing index rules. location['.indexOn'] = indices; } }; Generator.prototype.extendValidationMethods = function (validator, methods) { var writeMethods = []; ['create', 'update', 'delete'].forEach(function (method) { if (method in methods) { writeMethods.push(ast.andArray([writeAliases[method], methods[method].body])); } }); if (writeMethods.length !== 0) { extendValidator(validator, { '.write': ast.orArray(writeMethods) }); } ['validate', 'read', 'write'].forEach(function (method) { if (method in methods) { var methodValidator = {}; methodValidator['.' + method] = methods[method].body; extendValidator(validator, methodValidator); } }); }; // Return union validator (||) over each schema Generator.prototype.unionValidators = function (schema) { var union = {}; schema.forEach(function (typeName) { // First and the validator terms for a single type // Todo extend to unions and generics var singleType = extendValidator({}, this.ensureValidator(typeName)); mapValidator(singleType, ast.andArray); extendValidator(union, singleType); }.bind(this)); mapValidator(union, ast.orArray); return union; }; // Convert expressions to text, and at the same time, apply pruning operations // to remove no-op rules. Generator.prototype.convertExpressions = function (validator) { var _this = this; var methodThisIs = { '.validate': 'newData', '.read': 'data', '.write': 'newData' }; function hasWildcardSibling(path) { var parts = path.getLabels(); var childPart = parts.pop(); var parent = util.deepLookup(validator, parts); if (parent === undefined) { return false; } for (var _i = 0, _a = Object.keys(parent); _i < _a.length; _i++) { var prop = _a[_i]; if (prop === childPart) { continue; } if (prop[0] === '$') { return true; } } return false; } mapValidator(validator, function (value, prop, scope, path) { if (prop in methodThisIs) { var result = _this.getExpressionText(ast.andArray(collapseHasChildren(value)), methodThisIs[prop], scope, path); // Remove no-op .read or .write rule if no sibling wildcard props. if ((prop === '.read' || prop === '.write') && result === 'false') { if (!hasWildcardSibling(path)) { return undefined; } } // Remove no-op .validate rule if no sibling wildcard props. if (prop === '.validate' && result === 'true') { if (!hasWildcardSibling(path)) { return undefined; } } return result; } return value; }); }; Generator.prototype.getExpressionText = function (exp, thisIs, scope, path) { if (!('type' in exp)) { throw new Error(errors.application + "Not an expression: " + util.prettyJSON(exp)); } // First evaluate w/o binding of this to specific location. this.allowUndefinedFunctions = true; scope = util.extend({}, scope, { 'this': ast.cast(ast.call(ast.variable('@getThis')), 'Snapshot') }); exp = this.partialEval(exp, scope); // Now re-evaluate the flattened expression. this.allowUndefinedFunctions = false; this.thisIs = thisIs; this.symbols.registerFunction('@getThis', [], ast.builtin(this.getThis.bind(this))); this.symbols.registerFunction('@root', [], ast.builtin(this.getRootReference.bind(this, path))); this.symbols.registerFunction('prior', ['exp'], ast.builtin(this.prior.bind(this))); this.symbols.registerFunction('key', [], ast.builtin(this.getKey.bind(this, path.length() === 0 ? '' : path.getPart(-1).label))); exp = this.partialEval(exp); delete this.symbols.functions['@getThis']; delete this.symbols.functions['@root']; delete this.symbols.functions['prior']; delete this.symbols.functions['key']; // Top level expressions should never be to a snapshot reference - should // always evaluate to a boolean. exp = ast.ensureBoolean(exp); return ast.decodeExpression(exp); }; /* * Wrapper for partialEval debugging. */ Generator.prototype.partialEval = function (exp, params, functionCalls) { if (params === void 0) { params = {}; } if (functionCalls === void 0) { functionCalls = {}; } // Wrap real call for debugging. var result = this.partialEvalReal(exp, params, functionCalls); // console.log(ast.decodeExpression(exp) + " => " + ast.decodeExpression(result)); return result; }; // Partial evaluation of expressions - copy of expression tree (immutable). // // - Expand inline function calls. // - Replace local and global variables with their values. // - Expand snapshot references using child('ref'). // - Coerce snapshot references to values as needed. Generator.prototype.partialEvalReal = function (exp, params, functionCalls) { if (params === void 0) { params = {}; } if (functionCalls === void 0) { functionCalls = {}; } var self = this; function subExpression(exp2) { return self.partialEval(exp2, params, functionCalls); } function valueExpression(exp2) { return ast.ensureValue(subExpression(exp2)); } function booleanExpression(exp2) { return ast.ensureBoolean(subExpression(exp2)); } function lookupVar(exp2) { // TODO: Unbound variable access should be a