From 520cc439728a03ef32a0737823b34373d75d0312 Mon Sep 17 00:00:00 2001 From: Shweelan Samson Date: Mon, 28 Jan 2019 17:51:32 +0300 Subject: [PATCH] Add a check for filter to be valid query --- lib/ui/api-functions.js | 58 ---------- lib/ui/mongodb-functions.js | 216 ++++++++++++++++++++++-------------- ui/filter-component.js | 24 +++- 3 files changed, 152 insertions(+), 146 deletions(-) diff --git a/lib/ui/api-functions.js b/lib/ui/api-functions.js index e6947d5..918f7c2 100644 --- a/lib/ui/api-functions.js +++ b/lib/ui/api-functions.js @@ -8,64 +8,6 @@ import * as db from "./db"; import * as cache from "../cache"; import * as mongodbFunctions from "./mongodb-functions"; -export function filterToMongoQuery(filter, negate = false, res = {}) { - const op = filter[0]; - - if ((!negate && op === "AND") || (negate && op === "OR")) { - res["$and"] = res["$and"] || []; - for (let i = 1; i < filter.length; ++i) - res["$and"].push(filterToMongoQuery(filter[i], negate)); - } else if ((!negate && op === "OR") || (negate && op === "AND")) { - res["$or"] = res["$or"] || []; - - for (let i = 1; i < filter.length; ++i) - res["$or"].push(filterToMongoQuery(filter[i], negate)); - } else if (op === "NOT") { - filterToMongoQuery(filter[1], !negate, res); - } else if (op === "=") { - const param = filter[1][1]; - const p = (res[param] = res[param] || {}); - if (negate) p["$ne"] = filter[2]; - else p["$eq"] = filter[2]; - } else if (op === "<>") { - const param = filter[1][1]; - let p = (res[param] = res[param] || {}); - if (negate) p = p["$not"] = p["$not"] || {}; - p["$ne"] = filter[2]; - p["$exists"] = true; - } else if (op === ">") { - const param = filter[1][1]; - let p = (res[param] = res[param] || {}); - if (negate) p = p["$not"] = p["$not"] || {}; - p["$gt"] = filter[2]; - } else if (op === ">=") { - const param = filter[1][1]; - let p = (res[param] = res[param] || {}); - if (negate) p = p["$not"] = p["$not"] || {}; - p["$gte"] = filter[2]; - } else if (op === "<") { - const param = filter[1][1]; - let p = (res[param] = res[param] || {}); - if (negate) p = p["$not"] = p["$not"] || {}; - p["$lt"] = filter[2]; - } else if (op === "<=") { - const param = filter[1][1]; - let p = (res[param] = res[param] || {}); - if (negate) p = p["$not"] = p["$not"] || {}; - p["$lte"] = filter[2]; - } else if (op === "IS NULL") { - const param = filter[1][1]; - res[param] = { $exists: negate }; - } else if (op === "IS NOT NULL") { - const param = filter[1][1]; - res[param] = { $exists: !negate }; - } else { - throw new Error(`Unrecognized operator ${op}`); - } - - return res; -} - export function deleteResource(resource, id) { return new Promise((resolve, reject) => { if (resource === "config") { diff --git a/lib/ui/mongodb-functions.js b/lib/ui/mongodb-functions.js index 3a72d89..a6bc095 100644 --- a/lib/ui/mongodb-functions.js +++ b/lib/ui/mongodb-functions.js @@ -1,11 +1,13 @@ "use strict"; import { ObjectID } from "mongodb"; import { map, parse, stringify } from "../common/expression-parser"; -import { likePatternToRegExp } from "../common/expression"; +import { likePatternToRegExp, evaluate } from "../common/expression"; + +const isArray = Array.isArray; export function processDeviceFilter(filter) { return map(filter, exp => { - if (!Array.isArray(exp)) return exp; + if (!isArray(exp)) return exp; if (exp[0] === "PARAM") { const p = exp[1]; @@ -20,7 +22,7 @@ export function processDeviceFilter(filter) { else if (p === "Events.0_BOOTSTRAP") return ["PARAM", "_lastBootstrap"]; else if (p === "Events.1_BOOT") return ["PARAM", "_lastBoot"]; } else if ( - Array.isArray(exp[1]) && + isArray(exp[1]) && exp[1][0] === "PARAM" && exp[1][1].startsWith("Tags.") ) { @@ -34,7 +36,7 @@ export function processDeviceFilter(filter) { ) { let e = map(exp, ee => { if ( - Array.isArray(ee) && + isArray(ee) && ee[0] === "PARAM" && typeof ee[1] === "string" && !ee[1].startsWith("_") @@ -56,7 +58,7 @@ export function processDeviceFilter(filter) { export function processTasksFilter(filter) { return map(filter, exp => { - if (!Array.isArray(exp)) return exp; + if (!isArray(exp)) return exp; if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { const e = exp.slice(); if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectID(e[2]); @@ -72,7 +74,7 @@ export function processTasksFilter(filter) { export function processFaultsFilter(filter) { return map(filter, exp => { - if (!Array.isArray(exp)) return exp; + if (!isArray(exp)) return exp; if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { const e = exp.slice(); if (e[1][0] === "PARAM" && e[1][1] === "timestamp") e[2] = new Date(e[2]); @@ -84,89 +86,137 @@ export function processFaultsFilter(filter) { }); } -export function filterToMongoQuery(filter, negate = false, res = {}) { - const op = filter[0]; +export function filterToMongoQuery(exp) { + const ops = { + OR: 0, + AND: 0, + NOT: 0, + "=": 1, + "<>": 1, + ">": 1, + ">=": 1, + "<": 1, + "<=": 1, + LIKE: 1, + "NOT LIKE": 1, + "IS NULL": 1, + "IS NOT NULL": 1 + }; - if ((!negate && op === "AND") || (negate && op === "OR")) { - res["$and"] = res["$and"] || []; - for (let i = 1; i < filter.length; ++i) - res["$and"].push(filterToMongoQuery(filter[i], negate)); - } else if ((!negate && op === "OR") || (negate && op === "AND")) { - res["$or"] = res["$or"] || []; + function recursive(filter, negate, res = {}) { + const op = filter[0]; - for (let i = 1; i < filter.length; ++i) - res["$or"].push(filterToMongoQuery(filter[i], negate)); - } else if (op === "NOT") { - filterToMongoQuery(filter[1], !negate, res); - } else if (op === "=") { - const param = filter[1][1]; - const p = (res[param] = res[param] || {}); - if (negate) p["$ne"] = filter[2]; - else p["$eq"] = filter[2]; - } else if (op === "<>") { - const param = filter[1][1]; - let p = (res[param] = res[param] || {}); - if (negate) p = p["$not"] = p["$not"] || {}; - p["$ne"] = filter[2]; - p["$exists"] = true; - } else if (op === ">") { - const param = filter[1][1]; - let p = (res[param] = res[param] || {}); - if (negate) p = p["$not"] = p["$not"] || {}; - p["$gt"] = filter[2]; - } else if (op === ">=") { - const param = filter[1][1]; - let p = (res[param] = res[param] || {}); - if (negate) p = p["$not"] = p["$not"] || {}; - p["$gte"] = filter[2]; - } else if (op === "<") { - const param = filter[1][1]; - let p = (res[param] = res[param] || {}); - if (negate) p = p["$not"] = p["$not"] || {}; - p["$lt"] = filter[2]; - } else if (op === "<=") { - const param = filter[1][1]; - let p = (res[param] = res[param] || {}); - if (negate) p = p["$not"] = p["$not"] || {}; - p["$lte"] = filter[2]; - } else if (op === "IS NULL") { - const param = filter[1][1]; - res[param] = { $exists: negate }; - } else if (op === "IS NOT NULL") { - const param = filter[1][1]; - res[param] = { $exists: !negate }; - } else if (op === "LIKE" || op === "NOT LIKE") { - if (op === "NOT LIKE") negate = !negate; - let param; - let flags; - if (filter[1][0] === "FUNC" && filter[1][1] === "UPPER") { - if (filter[2] !== filter[2].toUpperCase()) { - throw new Error( - "Cannot compare UPPER() against non upper case pattern" - ); + if (ops[op] === 0) { + for (let i = 1; i < filter.length; ++i) { + if (!isArray(filter[i]) || ops[filter[i][0]] == null) + throw new Error(`Invalid expression in ${op} clause`); } - param = filter[1][2][1]; - flags = "i"; - } else if (filter[1][0] === "FUNC" && filter[1][1] === "LOWER") { - if (filter[2] !== filter[2].toLowerCase()) { - throw new Error( - "Cannot compare LOWER() against non lower case pattern" - ); + + if ((!negate && op === "AND") || (negate && op === "OR")) { + res["$and"] = res["$and"] || []; + for (let i = 1; i < filter.length; ++i) + res["$and"].push(recursive(filter[i], negate)); + } else if ((!negate && op === "OR") || (negate && op === "AND")) { + res["$or"] = res["$or"] || []; + + for (let i = 1; i < filter.length; ++i) + res["$or"].push(recursive(filter[i], negate)); + } else if (op === "NOT") { + recursive(filter[1], !negate, res); + } + } else if (ops[op] === 1) { + if (isArray(filter[2])) { + throw new Error(`Right operand of ${op} clause is not a primitive`); + } else if ( + ["LIKE", "NOT LIKE"].includes(op) && + isArray(filter[1]) && + filter[1][0] === "FUNC" && + ["UPPER", "LOWER"].includes(filter[1][1]) + ) { + if ( + (filter[1][1] === "UPPER" && filter[2] !== filter[2].toUpperCase()) || + (filter[1][1] === "LOWER" && filter[2] !== filter[2].toLowerCase()) + ) { + throw new Error( + `Cannot compare ${ + filter[1][1] + }() against non-${filter[1][1].toLowerCase()}case pattern` + ); + } + } else if (!isArray(filter[1]) || filter[1][0] !== "PARAM") { + throw new Error(`Left operand of ${op} clause is not a parameter`); + } + + if (op === "=") { + const param = filter[1][1]; + const p = (res[param] = res[param] || {}); + if (negate) p["$ne"] = filter[2]; + else p["$eq"] = filter[2]; + } else if (op === "<>") { + const param = filter[1][1]; + let p = (res[param] = res[param] || {}); + if (negate) p = p["$not"] = p["$not"] || {}; + p["$ne"] = filter[2]; + p["$exists"] = true; + } else if (op === ">") { + const param = filter[1][1]; + let p = (res[param] = res[param] || {}); + if (negate) p = p["$not"] = p["$not"] || {}; + p["$gt"] = filter[2]; + } else if (op === ">=") { + const param = filter[1][1]; + let p = (res[param] = res[param] || {}); + if (negate) p = p["$not"] = p["$not"] || {}; + p["$gte"] = filter[2]; + } else if (op === "<") { + const param = filter[1][1]; + let p = (res[param] = res[param] || {}); + if (negate) p = p["$not"] = p["$not"] || {}; + p["$lt"] = filter[2]; + } else if (op === "<=") { + const param = filter[1][1]; + let p = (res[param] = res[param] || {}); + if (negate) p = p["$not"] = p["$not"] || {}; + p["$lte"] = filter[2]; + } else if (op === "IS NULL") { + const param = filter[1][1]; + res[param] = { $exists: negate }; + } else if (op === "IS NOT NULL") { + const param = filter[1][1]; + res[param] = { $exists: !negate }; + } else { + if (op === "NOT LIKE") negate = !negate; + let param; + let flags; + if (filter[1][0] === "FUNC" && filter[1][1] === "UPPER") { + param = filter[1][2][1]; + flags = "i"; + } else if (filter[1][0] === "FUNC" && filter[1][1] === "LOWER") { + param = filter[1][2][1]; + flags = "i"; + } else { + param = filter[1][1]; + flags = ""; + } + const r = likePatternToRegExp(filter[2], filter[3], flags); + if (negate) res[param] = { $not: r }; + else res[param] = r; } - param = filter[1][2][1]; - flags = "i"; } else { - param = filter[1][1]; - flags = ""; + throw new Error(`Unrecognized operator ${op}`); } - const r = likePatternToRegExp(filter[2], filter[3], flags); - if (negate) res[param] = { $not: r }; - else res[param] = r; - } else { - throw new Error(`Unrecognized operator ${op}`); + + return res; } - return res; + const _exp = evaluate(exp, null, Date.now()); + + if (!isArray(_exp)) { + if (_exp === true) return {}; + throw new Error("Primitives are not valid queries"); + } + + return recursive(_exp, false); } export function processDeviceProjection(projection) { @@ -384,7 +434,7 @@ function mongoQueryToFilter(query) { else throw new Error(`Operator ${k} not supported`); } else if (k === "_tags") { if (typeof v === "object") { - if (Array.isArray(v)) throw new Error(`Invalid type`); + if (isArray(v)) throw new Error(`Invalid type`); for (const [kk, vv] of Object.entries(v)) { if (kk === "$ne") @@ -400,7 +450,7 @@ function mongoQueryToFilter(query) { ["PARAM", k] ]); } else if (typeof v === "object") { - if (Array.isArray(v)) throw new Error(`Invalid type`); + if (isArray(v)) throw new Error(`Invalid type`); for (const [kk, vv] of Object.entries(v)) { let op; diff --git a/ui/filter-component.js b/ui/filter-component.js index e98117d..5c28ae6 100644 --- a/ui/filter-component.js +++ b/ui/filter-component.js @@ -1,10 +1,11 @@ "use strict"; import m from "mithril"; -import { parse, stringify } from "../lib/common/expression-parser"; +import { parse, stringify, map } from "../lib/common/expression-parser"; import memoize from "../lib/common/memoize"; import Autocomplete from "./autocomplete-compnent"; import * as smartQuery from "./smart-query"; +import { filterToMongoQuery } from "../lib/ui/mongodb-functions"; const getAutocomplete = memoize(resource => { const labels = smartQuery.getLabels(resource); @@ -15,13 +16,26 @@ const getAutocomplete = memoize(resource => { return autocomplete; }); -function parseFilter(f) { +function parseFilter(resource, f) { + let exp; if (/^[\s0-9a-zA-Z]+:/.test(f)) { const k = f.split(":", 1)[0]; const v = f.slice(k.length + 1).trim(); - return ["FUNC", "Q", k.trim(), v]; + exp = ["FUNC", "Q", k.trim(), v]; + } else { + exp = parse(f); } - return parse(f); + + // Throw exception if invalid Mongo query + filterToMongoQuery( + map(exp, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack(resource, e[2], e[3]); + return e; + }) + ); + + return exp; } function stringifyFilter(f) { @@ -55,7 +69,7 @@ const component = { vnode.state.filterList = vnode.state.filterList.filter(f => f); let filter = vnode.state.filterList.map((f, idx) => { try { - return parseFilter(f); + return parseFilter(vnode.attrs.resource, f); } catch (err) { vnode.state.filterInvalid |= 1 << idx; }