1: <?php
2: /**
3: * Object representation of a part of a LDAP filter.
4: *
5: * The purpose of this class is to easily build LDAP filters without having to
6: * worry about correct escaping etc.
7: *
8: * A filter is built using several independent filter objects which are
9: * combined afterwards. This object works in two modes, depending how the
10: * object is created.
11: *
12: * If the object is created using the {@link create()} method, then this is a
13: * leaf-object. If the object is created using the {@link combine()} method,
14: * then this is a container object.
15: *
16: * LDAP filters are defined in RFC 2254.
17: *
18: * @see http://www.ietf.org/rfc/rfc2254.txt
19: *
20: * A short example:
21: * <code>
22: * $filter0 = Horde_Ldap_Filter::create('stars', 'equals', '***');
23: * $filter_not0 = Horde_Ldap_Filter::combine('not', $filter0);
24: *
25: * $filter1 = Horde_Ldap_Filter::create('gn', 'begins', 'bar');
26: * $filter2 = Horde_Ldap_Filter::create('gn', 'ends', 'baz');
27: * $filter_comp = Horde_Ldap_Filter::combine('or', array($filter_not0, $filter1, $filter2));
28: *
29: * echo (string)$filter_comp;
30: * // This will output: (|(!(stars=\0x5c0x2a\0x5c0x2a\0x5c0x2a))(gn=bar*)(gn=*baz))
31: * // The stars in $filter0 are treaten as real stars unless you disable escaping.
32: * </code>
33: *
34: * Copyright 2009 Benedikt Hallinger
35: * Copyright 2010-2012 Horde LLC (http://www.horde.org/)
36: *
37: * @category Horde
38: * @package Ldap
39: * @author Benedikt Hallinger <beni@php.net>
40: * @author Jan Schneider <jan@horde.org>
41: * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0
42: */
43: class Horde_Ldap_Filter
44: {
45: /**
46: * Storage for combination of filters.
47: *
48: * This variable holds a array of filter objects that should be combined by
49: * this filter object.
50: *
51: * @var array
52: */
53: protected $_filters = array();
54:
55: /**
56: * Operator for sub-filters.
57: *
58: * @var string
59: */
60: protected $_operator;
61:
62: /**
63: * Single filter.
64: *
65: * If this is a leaf filter, the filter representation is store here.
66: *
67: * @var string
68: */
69: protected $_filter;
70:
71: /**
72: * Constructor.
73: *
74: * Construction of Horde_Ldap_Filter objects should happen through either
75: * {@link create()} or {@link combine()} which give you more control.
76: * However, you may use the constructor if you already have generated
77: * filters.
78: *
79: * @param array $params List of object parameters
80: */
81: protected function __construct(array $params)
82: {
83: foreach ($params as $param => $value) {
84: if (in_array($param, array('filter', 'filters', 'operator'))) {
85: $this->{'_' . $param} = $value;
86: }
87: }
88: }
89:
90: /**
91: * Creates a new part of an LDAP filter.
92: *
93: * The following matching rules exists:
94: * - equals: One of the attributes values is exactly $value.
95: * Please note that case sensitiviness depends on the
96: * attributes syntax configured in the server.
97: * - begins: One of the attributes values must begin with $value.
98: * - ends: One of the attributes values must end with $value.
99: * - contains: One of the attributes values must contain $value.
100: * - present | any: The attribute can contain any value but must exist.
101: * - greater: The attributes value is greater than $value.
102: * - less: The attributes value is less than $value.
103: * - greaterOrEqual: The attributes value is greater or equal than $value.
104: * - lessOrEqual: The attributes value is less or equal than $value.
105: * - approx: One of the attributes values is similar to $value.
106: *
107: * If $escape is set to true then $value will be escaped. If set to false
108: * then $value will be treaten as a raw filter value string. You should
109: * then escape it yourself using {@link
110: * Horde_Ldap_Util::escapeFilterValue()}.
111: *
112: * Examples:
113: * <code>
114: * // This will find entries that contain an attribute "sn" that ends with
115: * // "foobar":
116: * $filter = Horde_Ldap_Filter::create('sn', 'ends', 'foobar');
117: *
118: * // This will find entries that contain an attribute "sn" that has any
119: * // value set:
120: * $filter = Horde_Ldap_Filter::create('sn', 'any');
121: * </code>
122: *
123: * @param string $attribute Name of the attribute the filter should apply
124: * to.
125: * @param string $match Matching rule (equals, begins, ends, contains,
126: * greater, less, greaterOrEqual, lessOrEqual,
127: * approx, any).
128: * @param string $value If given, then this is used as a filter value.
129: * @param boolean $escape Should $value be escaped?
130: *
131: * @return Horde_Ldap_Filter
132: * @throws Horde_Ldap_Exception
133: */
134: public static function create($attribute, $match, $value = '',
135: $escape = true)
136: {
137: if ($escape) {
138: $array = Horde_Ldap_Util::escapeFilterValue(array($value));
139: $value = $array[0];
140: }
141:
142: switch (Horde_String::lower($match)) {
143: case 'equals':
144: case '=':
145: $filter = '(' . $attribute . '=' . $value . ')';
146: break;
147: case 'begins':
148: $filter = '(' . $attribute . '=' . $value . '*)';
149: break;
150: case 'ends':
151: $filter = '(' . $attribute . '=*' . $value . ')';
152: break;
153: case 'contains':
154: $filter = '(' . $attribute . '=*' . $value . '*)';
155: break;
156: case 'greater':
157: case '>':
158: $filter = '(' . $attribute . '>' . $value . ')';
159: break;
160: case 'less':
161: case '>':
162: $filter = '(' . $attribute . '<' . $value . ')';
163: break;
164: case 'greaterorequal':
165: case '>=':
166: $filter = '(' . $attribute . '>=' . $value . ')';
167: break;
168: case 'lessorequal':
169: case '<=':
170: $filter = '(' . $attribute . '<=' . $value . ')';
171: break;
172: case 'approx':
173: case '~=':
174: $filter = '(' . $attribute . '~=' . $value . ')';
175: break;
176: case 'any':
177: case 'present':
178: $filter = '(' . $attribute . '=*)';
179: break;
180: default:
181: throw new Horde_Ldap_Exception('Matching rule "' . $match . '" unknown');
182: }
183:
184: return new Horde_Ldap_Filter(array('filter' => $filter));
185:
186: }
187:
188: /**
189: * Combines two or more filter objects using a logical operator.
190: *
191: * Example:
192: * <code>
193: * $filter = Horde_Ldap_Filter::combine('or', array($filter1, $filter2));
194: * </code>
195: *
196: * If the array contains filter strings instead of filter objects, they
197: * will be parsed.
198: *
199: * @param string $operator
200: * The logical operator, either "and", "or", "not" or the logical
201: * equivalents "&", "|", "!".
202: * @param array|Horde_Ldap_Filter|string $filters
203: * Array with Horde_Ldap_Filter objects and/or strings or a single
204: * filter when using the "not" operator.
205: *
206: * @return Horde_Ldap_Filter
207: * @throws Horde_Ldap_Exception
208: */
209: public static function combine($operator, $filters)
210: {
211: // Substitute named operators with logical operators.
212: switch ($operator) {
213: case 'and':
214: $operator = '&';
215: break;
216: case 'or':
217: $operator = '|';
218: break;
219: case 'not':
220: $operator = '!';
221: break;
222: }
223:
224: // Tests for sane operation.
225: switch ($operator) {
226: case '!':
227: // Not-combination, here we only accept one filter object or filter
228: // string.
229: if ($filters instanceof Horde_Ldap_Filter) {
230: $filters = array($filters); // force array
231: } elseif (is_string($filters)) {
232: $filters = array(self::parse($filters));
233: } elseif (is_array($filters)) {
234: throw new Horde_Ldap_Exception('Operator is "not" but $filter is an array');
235: } else {
236: throw new Horde_Ldap_Exception('Operator is "not" but $filter is not a valid Horde_Ldap_Filter nor a filter string');
237: }
238: break;
239:
240: case '&':
241: case '|':
242: if (!is_array($filters) || count($filters) < 2) {
243: throw new Horde_Ldap_Exception('Parameter $filters is not an array or contains less than two Horde_Ldap_Filter objects');
244: }
245: break;
246:
247: default:
248: throw new Horde_Ldap_Exception('Logical operator is unknown');
249: }
250:
251: foreach ($filters as $key => $testfilter) {
252: // Check for errors.
253: if (is_string($testfilter)) {
254: // String found, try to parse into an filter object.
255: $filters[$key] = self::parse($testfilter);
256: } elseif (!($testfilter instanceof Horde_Ldap_Filter)) {
257: throw new Horde_Ldap_Exception('Invalid object passed in array $filters!');
258: }
259: }
260:
261: return new Horde_Ldap_Filter(array('filters' => $filters,
262: 'operator' => $operator));
263: }
264:
265: /**
266: * Builds a filter (commonly for objectClass attributes) from different
267: * configuration options.
268: *
269: * @param array $params Hash with configuration options that build the
270: * search filter. Possible hash keys:
271: * - 'filter': An LDAP filter string.
272: * - 'objectclass' (string): An objectClass name.
273: * - 'objectclass' (array): A list of objectClass
274: * names.
275: * @param string $operator How to combine mutliple 'objectclass' entries.
276: * 'and' or 'or'.
277: *
278: * @return Horde_Ldap_Filter A filter matching the specified criteria.
279: * @throws Horde_Ldap_Exception
280: */
281: public static function build(array $params, $operator = 'and')
282: {
283: if (!empty($params['filter'])) {
284: return self::parse($params['filter']);
285: }
286: if (!is_array($params['objectclass'])) {
287: return self::create('objectclass', 'equals', $params['objectclass']);
288: }
289: $filters = array();
290: foreach ($params['objectclass'] as $objectclass) {
291: $filters[] = self::create('objectclass', 'equals', $objectclass);
292: }
293: if (count($filters) == 1) {
294: return $filters[0];
295: }
296: return self::combine($operator, $filters);
297: }
298:
299: /**
300: * Parses a string into a Horde_Ldap_Filter object.
301: *
302: * @todo Leaf-mode: Do we need to escape at all? what about *-chars? Check
303: * for the need of encoding values, tackle problems (see code comments).
304: *
305: * @param string $filter An LDAP filter string.
306: *
307: * @return Horde_Ldap_Filter
308: * @throws Horde_Ldap_Exception
309: */
310: public static function parse($filter)
311: {
312: if (!preg_match('/^\((.+?)\)$/', $filter, $matches)) {
313: throw new Horde_Ldap_Exception('Invalid filter syntax, filter components must be enclosed in round brackets');
314: }
315:
316: if (in_array(substr($matches[1], 0, 1), array('!', '|', '&'))) {
317: return self::_parseCombination($matches[1]);
318: } else {
319: return self::_parseLeaf($matches[1]);
320: }
321: }
322:
323: /**
324: * Parses combined subfilter strings.
325: *
326: * Passes subfilters to parse() and combines the objects using the logical
327: * operator detected. Each subfilter could be an arbitary complex
328: * subfilter.
329: *
330: * @param string $filter An LDAP filter string.
331: *
332: * @return Horde_Ldap_Filter
333: * @throws Horde_Ldap_Exception
334: */
335: protected static function _parseCombination($filter)
336: {
337: // Extract logical operator and filter arguments.
338: $operator = substr($filter, 0, 1);
339: $filter = substr($filter, 1);
340:
341: // Split $filter into individual subfilters. We cannot use split() for
342: // this, because we do not know the complexiness of the
343: // subfilter. Thus, we look trough the filter string and just recognize
344: // ending filters at the first level. We record the index number of the
345: // char and use that information later to split the string.
346: $sub_index_pos = array();
347: // Previous character looked at.
348: $prev_char = '';
349: // Denotes the current bracket level we are, >1 is too deep, 1 is ok, 0
350: // is outside any subcomponent.
351: $level = 0;
352: for ($curpos = 0, $len = strlen($filter); $curpos < $len; $curpos++) {
353: $cur_char = $filter{$curpos};
354:
355: // Rise/lower bracket level.
356: if ($cur_char == '(' && $prev_char != '\\') {
357: $level++;
358: } elseif ($cur_char == ')' && $prev_char != '\\') {
359: $level--;
360: }
361:
362: if ($cur_char == '(' && $prev_char == ')' && $level == 1) {
363: // Mark the position for splitting.
364: array_push($sub_index_pos, $curpos);
365: }
366: $prev_char = $cur_char;
367: }
368:
369: // Now perform the splits. To get the last part too, we need to add the
370: // "END" index to the split array.
371: array_push($sub_index_pos, strlen($filter));
372: $subfilters = array();
373: $oldpos = 0;
374: foreach ($sub_index_pos as $s_pos) {
375: $str_part = substr($filter, $oldpos, $s_pos - $oldpos);
376: array_push($subfilters, $str_part);
377: $oldpos = $s_pos;
378: }
379:
380: if (count($subfilters) > 1) {
381: // Several subfilters found.
382: if ($operator == '!') {
383: throw new Horde_Ldap_Exception('Invalid filter syntax: NOT operator detected but several arguments given');
384: }
385: } elseif (!count($subfilters)) {
386: // This should not happen unless the user specified a wrong filter.
387: throw new Horde_Ldap_Exception('Invalid filter syntax: got operator ' . $operator . ' but no argument');
388: }
389:
390: // Now parse the subfilters into objects and combine them using the
391: // operator.
392: $subfilters_o = array();
393: foreach ($subfilters as $s_s) {
394: array_push($subfilters_o, self::parse($s_s));
395: }
396: if (count($subfilters_o) == 1) {
397: $subfilters_o = $subfilters_o[0];
398: }
399:
400: return self::combine($operator, $subfilters_o);
401: }
402:
403: /**
404: * Parses a single leaf component.
405: *
406: * @param string $filter An LDAP filter string.
407: *
408: * @return Horde_Ldap_Filter
409: * @throws Horde_Ldap_Exception
410: */
411: protected static function _parseLeaf($filter)
412: {
413: // Detect multiple leaf components.
414: // [TODO] Maybe this will make problems with filters containing
415: // brackets inside the value.
416: if (strpos($filter, ')(')) {
417: throw new Horde_Ldap_Exception('Invalid filter syntax: multiple leaf components detected');
418: }
419:
420: $filter_parts = preg_split('/(?<!\\\\)(=|=~|>|<|>=|<=)/', $filter, 2, PREG_SPLIT_DELIM_CAPTURE);
421: if (count($filter_parts) != 3) {
422: throw new Horde_Ldap_Exception('Invalid filter syntax: unknown matching rule used');
423: }
424:
425: // [TODO]: Do we need to escape at all? what about *-chars user provide
426: // and that should remain special? I think, those prevent
427: // escaping! We need to check against PERL Net::LDAP!
428: // $value_arr = Horde_Ldap_Util::escapeFilterValue(array($filter_parts[2]));
429: // $value = $value_arr[0];
430:
431: return new Horde_Ldap_Filter(array('filter' => '(' . $filter_parts[0] . $filter_parts[1] . $filter_parts[2] . ')'));
432: }
433:
434: /**
435: * Returns the string representation of this filter.
436: *
437: * This method runs through all filter objects and creates the string
438: * representation of the filter.
439: *
440: * @return string
441: */
442: public function __toString()
443: {
444: if (!count($this->_filters)) {
445: return $this->_filter;
446: }
447:
448: $return = '';
449: foreach ($this->_filters as $filter) {
450: $return .= (string)$filter;
451: }
452:
453: return '(' . $this->_operator . $return . ')';
454: }
455: }
456: