123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- // Copyright 2014 Mark Cavage, Inc. All rights reserved.
- // Copyright 2014 Patrick Mooney. All rights reserved.
-
- var assert = require('assert-plus');
-
- var helpers = require('./helpers.js');
-
- var AndFilter = require('./and_filter');
- var ApproximateFilter = require('./approx_filter');
- var EqualityFilter = require('./equality_filter');
- var ExtensibleFilter = require('./ext_filter');
- var GreaterThanEqualsFilter = require('./ge_filter');
- var LessThanEqualsFilter = require('./le_filter');
- var NotFilter = require('./not_filter');
- var OrFilter = require('./or_filter');
- var PresenceFilter = require('./presence_filter');
- var SubstringFilter = require('./substr_filter');
-
-
-
- ///--- Internal Parsers
-
- // expression parsing
- // returns the index of the closing parenthesis matching the open paren
- // specified by openParenIndex
- function matchParens(str, openParenIndex) {
- var stack = [];
- var esc = false;
- for (var i = openParenIndex || 0; i < str.length; i++) {
- var c = str[i];
-
- if (c === '\\') {
- if (!esc)
- esc = true;
- continue;
- } else if (c === '(' && !esc) {
- stack.push(1);
- } else if (c === ')' && !esc) {
- stack.pop();
- if (stack.length === 0)
- return i;
- }
-
- esc = false;
- }
-
- var ndx = str.length - 1;
- if (str.charAt(ndx) !== ')')
- throw new Error(str + ' has unbalanced parentheses');
-
- return ndx;
- }
-
-
- function parse_substr(tree) {
- // Effectively a hand-rolled .shift() to support \* sequences
- var clean = true;
- var esc = false;
- var obj = {};
- var split = [];
- var substrNdx = 0;
-
- split[substrNdx] = '';
-
- for (var i = 0; i < tree.value.length; i++) {
- var c = tree.value[i];
- if (esc) {
- split[substrNdx] += c;
- esc = false;
- } else if (c === '*') {
- split[++substrNdx] = '';
- } else if (c === '\\') {
- esc = true;
- } else {
- split[substrNdx] += c;
- }
- }
-
- if (split.length > 1) {
- obj.tag = 'substrings';
- clean = true;
-
- // if the value string doesn't start with a * then theres no initial
- // value else split will have an empty string in its first array
- // index...
- // we need to remove that empty string
- if (tree.value.indexOf('*') !== 0) {
- obj.initial = split.shift();
- } else {
- split.shift();
- }
-
- // if the value string doesn't end with a * then theres no final
- // value also same split stuff as the initial stuff above
- if (tree.value.lastIndexOf('*') !== tree.value.length - 1) {
- obj.final = split.pop();
- } else {
- split.pop();
- }
- obj.any = split;
- } else {
- obj.value = split[0]; // pick up the cleaned version
- }
-
- obj.clean = clean;
- obj.esc = esc;
-
- return obj;
- }
-
- // recursive function that builds a filter tree from a string expression
- // the filter tree is an intermediary step between the incoming expression and
- // the outgoing Filter Class structure.
- function _buildFilterTree(expr) {
- var c;
- var child;
- var clean = false;
- var endParen;
- var esc = false;
- var i = 0;
- var obj;
- var tree = {};
- var split;
- var val = '';
-
- if (expr.length === 0)
- return tree;
-
- // Chop the parens (the call to matchParens below gets rid of the trailer)
- if (expr.charAt(0) == '(')
- expr = expr.substring(1, expr.length - 1);
-
- //store prefix operator
- if (expr.charAt(0) === '&') {
- tree.op = 'and';
- expr = expr.substring(1);
- } else if (expr.charAt(0) === '|') {
- tree.op = 'or';
- expr = expr.substring(1);
- } else if (expr.charAt(0) === '!') {
- tree.op = 'not';
- expr = expr.substring(1);
- } else if (expr.charAt(0) === '(') {
- throw new Error('invalid nested parens');
- } else {
- tree.op = 'expr';
- }
-
- if (tree.op != 'expr') {
- tree.children = [];
-
- // logical operators are k-ary, so we go until our expression string runs
- // out (at least for this recursion level)
- while (expr.length !== 0) {
- endParen = matchParens(expr);
-
- if (endParen == expr.length - 1) {
- tree.children[i] = _buildFilterTree(expr);
- expr = '';
- } else {
- child = expr.slice(0, endParen + 1);
- expr = expr.substring(endParen + 1);
- tree.children[i] = _buildFilterTree(child);
- }
- i++;
- }
- } else {
- //else its some sort of non-logical expression, parse and return as such
- var operatorStr = '';
- tree.name = '';
- tree.value = '';
-
-
- // This parses and enforces filter syntax, which is an AttributeDescription
- // plus a filter operator, followed by (for ldapjs), anything. Note
- // that ldapjs additionally allows the '_' character in the AD, as many
- // users rely on it, even though it's non-standard
- //
- // From 4.1.5 of RFC251
- //
- // AttributeDescription ::= LDAPString
- //
- // A value of AttributeDescription is based on the following BNF:
- //
- // <AttributeDescription> ::= <AttributeType> [ ";" <options> ]
- //
- // <options> ::= <option> | <option> ";" <options>
- //
- // <option> ::= <opt-char> <opt-char>*
- //
- // <opt-char> ::= ASCII-equivalent letters, numbers and hyphen
- //
- // Examples of valid AttributeDescription:
- //
- // cn
- // userCertificate;binary
-
- /* JSSTYLED */
- if (!/[a-zA-Z0-9;_\-]+[~><:]?=.+/.test(expr))
- throw new Error(expr + ' is invalid');
-
- if (expr.indexOf('~=') !== -1) {
- operatorStr = '~=';
- tree.tag = 'approxMatch';
- } else if (expr.indexOf('>=') !== -1) {
- operatorStr = '>=';
- tree.tag = 'greaterOrEqual';
- } else if (expr.indexOf('<=') !== -1) {
- operatorStr = '<=';
- tree.tag = 'lessOrEqual';
- } else if (expr.indexOf(':=') !== -1) {
- operatorStr = ':=';
- tree.tag = 'extensibleMatch';
- } else if (expr.indexOf('=') !== -1) {
- operatorStr = '=';
- tree.tag = 'equalityMatch';
- } else {
- // tree.tag = 'present';
- throw new Error('invalid filter syntax');
- }
-
- if (operatorStr === '') {
- tree.name = expr;
- } else {
- // pull out lhs and rhs of equality operator
- var splitAry = expr.split(operatorStr);
- tree.name = splitAry.shift();
- tree.value = splitAry.join(operatorStr);
-
- // substrings fall into the equality bin in the
- // switch above so we need more processing here
- if (tree.tag === 'equalityMatch') {
- if (tree.value === '*') {
- tree.tag = 'present';
- } else {
- obj = parse_substr(tree);
- tree.initial = obj.initial;
- tree.any = obj.any;
- tree.final = obj.final;
- tree.tag = obj.tag || tree.tag;
- tree.value = obj.value;
- esc = obj.esc;
- clean = obj.clean;
- }
- } else if (tree.tag == 'extensibleMatch') {
- split = tree.name.split(':');
- tree.extensible = {
- matchType: split[0],
- value: tree.value
- };
-
- switch (split.length) {
- case 1:
- break;
- case 2:
- if (split[1].toLowerCase() === 'dn') {
- tree.extensible.dnAttributes = true;
- } else {
- tree.extensible.rule = split[1];
- }
- break;
- case 3:
- tree.extensible.dnAttributes = true;
- tree.extensible.rule = split[2];
- break;
- default:
- throw new Error('Invalid extensible filter');
- }
-
- switch (tree.extensible.rule) {
- case '2.5.13.4':
- case 'caseIgnoreSubstringsMatch':
- tree.extensible.attribute = tree.extensible.matchType;
- obj = parse_substr(tree);
- tree.extensible.initial = obj.initial;
- tree.extensible.any = obj.any;
- tree.extensible.final = obj.final;
- tree.value = obj.value;
- esc = obj.esc;
- clean = obj.clean;
- break;
-
- case '2.5.13.2':
- case 'caseIgnoreMatch':
- tree.extensible.attribute = tree.extensible.matchType;
- break;
- default:
- // noop
- break;
- }
- }
- }
-
- // Cleanup any escape sequences
- if (!clean) {
-
- for (i = 0; i < tree.value.length; i++) {
- c = tree.value[i];
- if (esc) {
- val += c;
- esc = false;
- } else if (c === '\\') {
- esc = true;
- } else {
- val += c;
- }
- }
- tree.value = val;
- }
- }
-
- return tree;
- }
-
-
- function serializeTree(tree, filter) {
- if (tree === undefined || tree.length === 0)
- return;
-
- // if the current tree object is not an expression then its a logical
- // operator (ie an internal node in the tree)
- var current = null;
- if (tree.op !== 'expr') {
- switch (tree.op) {
- case 'and':
- current = new AndFilter();
- break;
- case 'or':
- current = new OrFilter();
- break;
- case 'not':
- current = new NotFilter();
- break;
- default:
- break;
- }
-
- filter.addFilter(current || filter);
- if (current || tree.children.length) {
- tree.children.forEach(function (child) {
- serializeTree(child, current);
- });
- }
- } else {
- // else its a leaf node in the tree, and represents some type of
- // non-logical expression
- var tmp;
-
- // convert the tag name to a filter class type
- switch (tree.tag) {
- case 'approxMatch':
- tmp = new ApproximateFilter({
- attribute: tree.name,
- value: tree.value
- });
- break;
- case 'extensibleMatch':
- tmp = new ExtensibleFilter(tree.extensible);
- break;
- case 'greaterOrEqual':
- tmp = new GreaterThanEqualsFilter({
- attribute: tree.name,
- value: tree.value
- });
- break;
- case 'lessOrEqual':
- tmp = new LessThanEqualsFilter({
- attribute: tree.name,
- value: tree.value
- });
- break;
- case 'equalityMatch':
- tmp = new EqualityFilter({
- attribute: tree.name,
- value: tree.value
- });
- break;
- case 'substrings':
- tmp = new SubstringFilter({
- attribute: tree.name,
- initial: tree.initial,
- any: tree.any,
- final: tree.final
- });
- break;
- case 'present':
- tmp = new PresenceFilter({
- attribute: tree.name
- });
- break;
- default:
- break;
- }
- if (tmp)
- filter.addFilter(tmp);
- }
- }
-
-
- function _parseString(str) {
- assert.ok(str);
-
- // create a blank object to pass into treeToObjs
- // since its recursive we have to prime it ourselves.
- // this gets stripped off before the filter structure is returned
- // at the bottom of this function.
- var filterObj = new AndFilter({
- filters: []
- });
-
- serializeTree(_buildFilterTree(str), filterObj);
- return filterObj.filters[0];
- }
-
-
- ///--- Exports
-
- module.exports = {
- parse: function (filter) {
- assert.string(filter);
-
- return _parseString(filter);
- },
-
- // Helper utilties for writing custom matchers
- testValues: helpers.testValues,
- getAttrValue: helpers.getAttrValue,
-
- // Filter definitions
- AndFilter: AndFilter,
- ApproximateFilter: ApproximateFilter,
- EqualityFilter: EqualityFilter,
- ExtensibleFilter: ExtensibleFilter,
- GreaterThanEqualsFilter: GreaterThanEqualsFilter,
- LessThanEqualsFilter: LessThanEqualsFilter,
- NotFilter: NotFilter,
- OrFilter: OrFilter,
- PresenceFilter: PresenceFilter,
- SubstringFilter: SubstringFilter
- };
|