diff --git a/.gitignore b/.gitignore
index 6a5c7ef..3086bbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,6 @@ lib/api-functions.js
lib/cwmp.js
lib/db.js
lib/nbi.js
-lib/query.js
lib/session.js
node_modules
debug
diff --git a/lib/query.coffee b/lib/query.coffee
deleted file mode 100644
index 07739d9..0000000
--- a/lib/query.coffee
+++ /dev/null
@@ -1,372 +0,0 @@
-###
-# Copyright 2013-2017 Zaid Abdulla
-#
-# This file is part of GenieACS.
-#
-# GenieACS is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# GenieACS is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with GenieACS. If not, see .
-#
-# This file incorporates work covered by the following copyright and
-# permission notice:
-#
-# Copyright 2013 Fanoos Telecom
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-###
-
-common = require './common'
-mongodb = require 'mongodb'
-
-
-stringToRegexp = (input, flags) ->
- if (input.indexOf('*') == -1)
- return false
-
- output = input.replace(/[\[\]\\\^\$\.\|\?\+\(\)]/, "\\$&")
- if output[0] == '*'
- output = output.replace(/^\*+/g, '')
- else
- output = '^' + output
-
- if output[output.length - 1] == '*'
- output = output.replace(/\*+$/g, '')
- else
- output = output + '$'
-
- output = output.replace(/[\*]/, '.*')
-
- return new RegExp(output, flags)
-
-
-normalize = (input) ->
- if common.typeOf(input) is common.STRING_TYPE
- vals = [input]
- if (m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input))
- vals.push({'$regex' : new RegExp(m[1], m[2])})
-
- if +input == parseFloat(input)
- vals.push(+input)
-
- d = new Date(input)
- if input.length >= 8 and d.getFullYear() > 1983
- vals.push(d)
-
- r = stringToRegexp(input)
- if r isnt false
- vals.push({'$regex' : r})
-
- return vals
- return input
-
-
-expandValue = (value) ->
- if common.typeOf(value) is common.ARRAY_TYPE
- a = []
- for j in value
- a = a.concat(expandValue(j))
- return [a]
- else if common.typeOf(value) isnt common.OBJECT_TYPE
- n = normalize(value)
- if common.typeOf(n) isnt common.ARRAY_TYPE
- return [n]
- else
- return n
-
- objs = []
- indices = []
- keys = []
- values = []
- for k,v of value
- keys.push(k)
- values.push(expandValue(v))
- indices.push(0)
-
- i = 0
- while i < indices.length
- obj = {}
- for i in [0...keys.length]
- obj[keys[i]] = values[i][indices[i]]
- objs.push(obj)
-
- for i in [0...indices.length]
- indices[i] += 1
- if indices[i] < values[i].length
- break
- indices[i] = 0
- return objs
-
-
-permute = (param, val) ->
- conditions = []
-
- values = expandValue(val)
- if param[param.lastIndexOf('.') + 1] != '_'
- param += '._value'
-
- for v in values
- obj = {}
- obj[param] = v
- conditions.push(obj)
-
- return conditions
-
-
-expand = (query) ->
- new_query = {}
- for k,v of query
- if k[0] == '$' # operator
- expressions = []
- for e in v
- expressions.push(expand(e))
- new_query[k] = expressions
- else
- conditions = permute(k, v)
- if conditions.length > 1
- new_query['$and'] ?= []
- if v?['$ne']? or v?['$not']?
- if Object.keys(v) > 1
- throw new Error("Cannot mix $ne or $not with other operators")
- for c in conditions
- new_query['$and'].push(c)
- else
- new_query['$and'].push({'$or' : conditions})
- else
- Object.assign(new_query, conditions[0])
-
- return new_query
-
-
-# Generate parameter projection from a query
-# If second arg is given, it's edited and returned
-queryProjection = (query, proj) ->
- proj ?= {}
- for k,v of query
- if k.charAt(0) == '$' # this is a logical operator
- for q in v
- queryProjection(q, proj)
- else
- proj[k] = 1
- return proj
-
-
-testExpressions = (params, expressions, lop) ->
- for f in expressions
- res = test(params, f)
-
- switch lop
- when '$and'
- return false if not res
- when '$or'
- return true if res
- when '$nor'
- return false if res
-
- return switch lop
- when '$and' then true
- when '$or' then false
- when '$nor' then true
- else throw new Error('Unknown logical operator')
-
-
-test = (params, query) ->
- for k,v of query
- if k.charAt(0) == '$' # this is a logical operator
- res = testExpressions(params, v, k)
- else
- value = params[k]
-
- if common.typeOf(v) isnt common.OBJECT_TYPE
- # TODO comparing array to regex, array to array, and object to object
- if common.typeOf(value) is common.ARRAY_TYPE
- res = value.indexOf(v) != -1
- else
- if common.typeOf(v) is common.REGEXP_TYPE
- res = v.test(value)
- else
- res = v == value
- else
- for k2,v2 of v
- switch k2
- when '$ne'
- if common.typeOf(value) is common.ARRAY_TYPE
- res = value.indexOf(v2) == -1
- else
- res = value != v2
- when '$lt'
- res = value < v2
- when '$lte'
- res = value <= v2
- when '$gt'
- res = value > v2
- when '$gte'
- res = value >= v2
- when '$regex'
- res = v2.test(value)
- when '$in'
- throw new Error('Operator not supported')
- when '$nin'
- throw new Error('Operator not supported')
- when '$all'
- throw new Error('Operator not supported')
- when '$exists'
- throw new Error('Operator not supported')
- else
- throw new Error('Operator not supported')
-
- if not res
- return false
-
- return true
-
-
-matchType = (src, dst) ->
- if typeof src is 'string'
- return String(dst)
- else if typeof src is 'number'
- if +dst == parseFloat(dst)
- return +dst
- else if not isNaN(Date.parse(dst))
- return Date.parse(dst)
- else if typeof src is 'boolean'
- v = String(dst).trim()
- if v is 'true' or v is 'TRUE' or v is 'True' or v is '1'
- return true
- else if v is 'false' or v is 'FALSE' or v is 'False' or v is '0'
- return false
-
- return dst
-
-
-testFilter = (obj, filter) ->
- for k, v of filter
- [param, op] = k.split(/([^a-zA-Z0-9\-\_\.].*)/, 2)
- val = matchType(obj[param], v)
- switch op
- when '=', undefined
- return false if obj[param] != val
- when '>'
- return false if not (obj[param] > val)
- when '>='
- return false if not (obj[param] >= val)
- when '<'
- return false if not (obj[param] < val)
- when '<='
- return false if not (obj[param] <= val)
- when '!'
- return false if obj[param] == val
- else
- throw new Error("Unrecognized operator #{op}")
-
- return true
-
-
-convertMongoQueryToFilters = (query, filters) ->
- filters ?= {}
- for k, v of query
- if k[0] == '$'
- if k == '$and'
- for vv in v
- convertMongoQueryToFilters(vv, filters)
- else
- throw new Error("Operator #{k} not supported")
- else if k == '_tags'
- if common.typeOf(v) is common.OBJECT_TYPE
- for kk, vv of v
- vv = vv.replace(/[^a-zA-Z0-9\-]+/g, '_')
- switch kk
- when '$ne'
- filters["Tags.#{vv}!"] = true
- else
- throw new Error("Operator #{kk} not supported")
- else
- v = v.replace(/[^a-zA-Z0-9\-]+/g, '_')
- filters["Tags.#{v}"] = true
- else
- switch k
- when '_id' then k = 'DeviceID.ID'
- when '_deviceId._Manufacturer' then k = 'DeviceID.Manufacturer'
- when '_deviceId._OUI' then k = 'DeviceID.OUI'
- when '_deviceId._ProductClass' then k = 'DeviceID.ProductClass'
- when '_deviceId._SerialNumber' then k = 'DeviceID.SerialNumber'
- when '_lastInform' then k = 'Events.Inform'
- when '_lastBootstrap' then k = 'Events.0_BOOTSTRAP'
- when '_lastBoot' then k = 'Events.1_BOOT'
- when '_registered' then k = 'Events.Registered'
-
- if common.typeOf(v) is common.OBJECT_TYPE
- for kk, vv of v
- switch kk
- when '$eq'
- filters[k] = vv
- when '$ne'
- filters["#{k}!"] = vv
- when '$lt'
- filters["#{k}<"] = vv
- when '$lte'
- filters["#{k}<="] = vv
- when '$gt'
- filters["#{k}>"] = vv
- when '$gte'
- filters["#{k}>="] = vv
- else
- throw new Error("Oprator #{kk} not supported")
- else
- filters[k] = v
-
- return filters
-
-
-sanitizeQueryTypes = (query, types) ->
- for k, v of query
- if k[0] == '$' # logical operator
- sanitizeQueryTypes(vv, types) for vv in v
- else if k of types
- if common.typeOf(v) is common.OBJECT_TYPE
- for kk, vv of v
- switch kk # operator
- when '$in', '$nin'
- vv[i] = types[k](vv[i]) for i in [0...vv.length]
- when '$eq', '$gt', '$gte', '$lt', '$lte', '$ne'
- v[kk] = types[k](vv)
- when '$exists', '$type' then
- # ignore
- else
- throw new Error('Operator not supported')
- else
- query[k] = types[k](query[k])
-
- return query
-
-
-exports.expand = expand
-exports.queryProjection = queryProjection
-exports.test = test
-exports.sanitizeQueryTypes = sanitizeQueryTypes
-exports.convertMongoQueryToFilters = convertMongoQueryToFilters
-exports.testFilter = testFilter
diff --git a/lib/query.js b/lib/query.js
new file mode 100644
index 0000000..8f82222
--- /dev/null
+++ b/lib/query.js
@@ -0,0 +1,427 @@
+/**
+ * Copyright 2013-2018 Zaid Abdulla
+ *
+ * This file is part of GenieACS.
+ *
+ * GenieACS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * GenieACS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with GenieACS. If not, see .
+ */
+"use strict";
+
+const common = require("./common");
+
+function stringToRegexp(input, flags) {
+ if (input.indexOf("*") === -1)
+ return false;
+
+ let output = input.replace(/[\[\]\\\^\$\.\|\?\+\(\)]/, "\\$&");
+ if (output[0] === "*")
+ output = output.replace(/^\*+/g, "");
+ else
+ output = "^" + output;
+
+ if (output[output.length - 1] === "*")
+ output = output.replace(/\*+$/g, "");
+ else
+ output = output + "$";
+
+ output = output.replace(/[\*]/, ".*");
+ return new RegExp(output, flags);
+}
+
+function normalize(input) {
+ if (common.typeOf(input) === common.STRING_TYPE) {
+ const vals = [input];
+ let m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input);
+ if (m)
+ vals.push({"$regex": new RegExp(m[1], m[2])});
+
+ if (+input === parseFloat(input))
+ vals.push(+input);
+
+ let d = new Date(input);
+ if (input.length >= 8 && d.getFullYear() > 1983)
+ vals.push(d);
+
+ let r = stringToRegexp(input);
+ if (r !== false)
+ vals.push({"$regex": r});
+
+ return vals;
+ }
+ return input;
+}
+
+function expandValue(value) {
+ if (common.typeOf(value) === common.ARRAY_TYPE) {
+ const a = [];
+ for (let j of value)
+ a = a.conact(expandValue(j));
+ return [a];
+ } else if (common.typeOf(value) !== common.OBJECT_TYPE) {
+ const n = normalize(value);
+ if (common.typeOf(n) !== common.ARRAY_TYPE)
+ return [n];
+ else
+ return n;
+ }
+
+ const objs = [];
+ const indices = [];
+ const keys = [];
+ const values = [];
+ for (let [k, v] of Object.entries(value)) {
+ keys.push(k);
+ values.push(expandValue(v));
+ indices.push(0);
+ }
+
+ let i = 0;
+ while (i < indices.length) {
+ const obj = {};
+ for (let j = 0; j < keys.length; ++ j)
+ obj[keys[j]] = values[j][indices[j]];
+ objs.push(obj);
+
+ for (i = 0; i < indices.length; ++ i) {
+ indices[i] += 1;
+ if (indices[i] < values[i].length)
+ break;
+ indices[i] = 0;
+ }
+ }
+ return objs;
+}
+
+function permute(param, val) {
+ const conditions = [];
+ const values = expandValue(val);
+
+ if (param[param.lastIndexOf(".") + 1] !== "_")
+ param += "._value";
+
+ for (let v of values) {
+ const obj = {}
+ obj[param] = v;
+ conditions.push(obj);
+ }
+
+ return conditions;
+}
+
+function expand(query) {
+ const newQuery = {};
+ for (let [k, v] of Object.entries(query)) {
+ if (k[0] === "$") { // Operator
+ newQuery[k] = v.map(e => expand(e));
+ } else {
+ const conditions = permute(k, v);
+ if (conditions.length > 1) {
+ newQuery["$and"] = newQuery["$and"] || [];
+ if (v && (v["$ne"] != null || v["$not"] != null)) {
+ if (Object.keys(v).length > 1)
+ throw new Error("Cannot mix $ne or $not with other operators");
+ for (let c of conditions)
+ newQuery["$an"].push(c);
+ } else {
+ newQuery["$and"].push({"$or": conditions});
+ }
+ } else {
+ Object.assign(newQuery, conditions[0]);
+ }
+ }
+ }
+ return newQuery;
+}
+
+function testExpressions(params, expressions, lop) {
+ for (const f of expressions) {
+ const res = test(params, f);
+ switch (lop) {
+ case "$and":
+ if (!res)
+ return false;
+ break;
+ case "$or":
+ if (res)
+ return true;
+ break;
+ case "$nor":
+ if (res)
+ return false;
+ }
+ }
+
+ switch (lop) {
+ case "$and":
+ return true;
+ case "$or":
+ return false;
+ case "$nor":
+ return true;
+ default:
+ throw new Error("Unknown logical operator");
+ }
+}
+
+function test(params, query) {
+ let res;
+ for (let [k, v] of Object.entries(query)) {
+ if (k.charAt(0) === "$") { // Logical operator
+ res = testExpressions(params, v, k);
+ } else {
+ const value = params[k];
+ if (common.typeOf(v) !== common.OBJECT_TYPE) {
+ // TODO comparing array to regex, array to array, and object to object
+ if (common.typeOf(value) === common.ARRAY_TYPE) {
+ res = value.indexOf(v) !== -1;
+ } else {
+ if (common.typeOf(v) === common.REGEXP_TYPE) {
+ res = v.test(value);
+ } else {
+ res = v === value;
+ }
+ }
+ } else {
+ for (let [k2, v2] of Object.entries(v)) {
+ switch (k2) {
+ case "$ne":
+ if (common.typeOf(value) === common.ARRAY_TYPE)
+ res = value.indexOf(v2) === -1;
+ else
+ res = value !== v2;
+ break;
+ case "$lt":
+ res = value < v2;
+ break;
+ case "$lte":
+ res = value <= v2;
+ break;
+ case "$gt":
+ res = value > v2;
+ break;
+ case "$gte":
+ res = value >= v2;
+ break;
+ case "$regex":
+ res = v2.test(value);
+ break;
+ case "$in":
+ throw new Error("Operator not supported");
+ break;
+ case "$nin":
+ throw new Error("Operator not supported");
+ break;
+ case "$all":
+ throw new Error("Operator not supported");
+ break;
+ case "$exists":
+ throw new Error("Operator not supported");
+ break;
+ default:
+ throw new Error("Operator not supported");
+ }
+ }
+ }
+ }
+
+ if (!res)
+ return false;
+ }
+ return true;
+}
+
+function matchType(src, dst) {
+ const type = typeof src;
+ if (type === "string") {
+ return String(dst);
+ } else if (type === "number") {
+ if (+dst === parseFloat(dst))
+ return +dst;
+ else if (!isNaN(Date.parse(dst)))
+ return Date.parse(dst);
+ } else if (type === "boolean") {
+ const v = String(dst).trim().toLowerCase();
+ if (v === "true" || v === "1")
+ return true;
+ else if (v === "false" || v === "0")
+ return false;
+ }
+ return dst;
+}
+
+function testFilter(obj, filter) {
+ for (let [k, v] of Object.entries(filter)) {
+ const [param, op] = k.split(/([^a-zA-Z0-9\-\_\.].*)/, 2)
+ const val = matchType(obj[param], v);
+ switch (op) {
+ case "=":
+ case undefined:
+ if (obj[param] !== val)
+ return false;
+ break;
+ case ">":
+ if (!(obj[param] > val))
+ return false;
+ break;
+ case ">=":
+ if (!(obj[param] >= val))
+ return false;
+ break;
+ case "<":
+ if (!(obj[param] < val))
+ return false;
+ break;
+ case "<=":
+ if (!(obj[param] <= val))
+ return false;
+ break;
+ case "!":
+ if (obj[param] === val)
+ return false;
+ break;
+ default:
+ throw new Error(`Unrecognized operator ${op}`);
+ }
+ }
+ return true;
+}
+
+function convertMongoQueryToFilters(query, filters) {
+ filters = filters || {}
+ for (let [k, v] of Object.entries(query)) {
+ if (k[0] === "$") {
+ if (k === "$and")
+ for (let vv of v)
+ convertMongoQueryToFilters(vv, filters);
+ else
+ throw new Error(`Operator ${k} not supported`);
+ } else if (k === "_tags") {
+ if (common.typeOf(v) === common.OBJECT_TYPE) {
+ for (let [kk, vv] of Object.entries(v)) {
+ vv = vv.replace(/[^a-zA-Z0-9\-]+/g, "_");
+ if (kk === "$ne")
+ filters[`Tags.${vv}!`] = true;
+ else
+ throw new Error(`Operator ${kk} not supported`);
+ }
+ } else {
+ v = v.replace(/[^a-zA-Z0-9\-]+/g, "_");
+ filters[`Tags.${v}`] = true;
+ }
+ } else {
+ switch (k) {
+ case "_id":
+ k = "DeviceID.ID";
+ break;
+ case "_deviceId._Manufacturer":
+ k = "DeviceID.Manufacturer";
+ break;
+ case "_deviceId._OUI":
+ k = "DeviceID.OUI";
+ break;
+ case "_deviceId._ProductClass":
+ k = "DeviceID.ProductClass";
+ break;
+ case "_deviceId._SerialNumber":
+ k = "DeviceID.SerialNumber";
+ break;
+ case "_lastInform":
+ k = "Events.Inform";
+ break;
+ case "_lastBootstrap":
+ k = "Events.0_BOOTSTRAP";
+ break;
+ case "_lastBoot":
+ k = "Events.1_BOOT";
+ break;
+ case "_registered":
+ k = "Events.Registered";
+ }
+
+ if (common.typeOf(v) === common.OBJECT_TYPE) {
+ for (let [kk, vv] of Object.entries(v)) {
+ switch (kk) {
+ case "$eq":
+ filters[k] = vv;
+ break;
+ case "$ne":
+ filters[`${k}!`] = vv;
+ break;
+ case "$lt":
+ filters[`${k}<`] = vv;
+ break;
+ case "$lte":
+ filters[`${k}<=`] = vv;
+ break;
+ case "$gt":
+ filters[`${k}>`] = vv;
+ break;
+ case "$gte":
+ filters[`${k}>=`] = vv;
+ break;
+ default:
+ throw new Error(`Oprator ${kk} not supported`);
+ }
+ }
+ } else {
+ filters[k] = v;
+ }
+ }
+ }
+ return filters;
+}
+
+function sanitizeQueryTypes(query, types) {
+ for (let [k, v] of Object.entries(query)) {
+ if (k[0] === "$") { // Logical operator
+ for (let vv of v)
+ sanitizeQueryTypes(vv, types);
+ } else if (k in types) {
+ if (common.typeOf(v) === common.OBJECT_TYPE) {
+ for (let [kk, vv] of Object.entries(v)) {
+ switch (kk) {
+ case "$in":
+ case "$nin":
+ for (let i = 0; i < vv.length; ++ i)
+ vv[i] = types[k](vv[i]);
+ break;
+ case "$eq":
+ case "$gt":
+ case "$gte":
+ case "$lt":
+ case "$lte":
+ case "$ne":
+ v[kk] = types[k](vv);
+ break;
+ case "$exists":
+ case "$type":
+ // Ignore
+ break;
+ default:
+ throw new Error("Operator not supported");
+ }
+ }
+ } else {
+ query[k] = types[k](query[k]);
+ }
+ }
+ }
+ return query;
+}
+
+exports.expand = expand;
+exports.test = test;
+exports.sanitizeQueryTypes = sanitizeQueryTypes;
+exports.convertMongoQueryToFilters = convertMongoQueryToFilters;
+exports.testFilter = testFilter;