2 /* vim: set expandtab tabstop=4 shiftwidth=4: */
4 * File containing the Net_LDAP2_Filter interface class.
10 * @author Benedikt Hallinger <beni@php.net>
11 * @copyright 2009 Benedikt Hallinger
12 * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
13 * @version SVN: $Id: Filter.php 332305 2013-12-09 08:51:41Z beni $
14 * @link http://pear.php.net/package/Net_LDAP2/
20 require_once 'PEAR.php';
21 require_once 'Net/LDAP2/Util.php';
22 require_once 'Net/LDAP2/Entry.php';
25 * Object representation of a part of a LDAP filter.
27 * This Class is not completely compatible to the PERL interface!
29 * The purpose of this class is, that users can easily build LDAP filters
30 * without having to worry about right escaping etc.
31 * A Filter is built using several independent filter objects
32 * which are combined afterwards. This object works in two
33 * modes, depending how the object is created.
34 * If the object is created using the {@link create()} method, then this is a leaf-object.
35 * If the object is created using the {@link combine()} method, then this is a container object.
37 * LDAP filters are defined in RFC-2254 and can be found under
38 * {@link http://www.ietf.org/rfc/rfc2254.txt}
40 * Here a quick copy&paste example:
42 * $filter0 = Net_LDAP2_Filter::create('stars', 'equals', '***');
43 * $filter_not0 = Net_LDAP2_Filter::combine('not', $filter0);
45 * $filter1 = Net_LDAP2_Filter::create('gn', 'begins', 'bar');
46 * $filter2 = Net_LDAP2_Filter::create('gn', 'ends', 'baz');
47 * $filter_comp = Net_LDAP2_Filter::combine('or',array($filter_not0, $filter1, $filter2));
49 * echo $filter_comp->asString();
50 * // This will output: (|(!(stars=\0x5c0x2a\0x5c0x2a\0x5c0x2a))(gn=bar*)(gn=*baz))
51 * // The stars in $filter0 are treaten as real stars unless you disable escaping.
56 * @author Benedikt Hallinger <beni@php.net>
57 * @license http://www.gnu.org/copyleft/lesser.html LGPL
58 * @link http://pear.php.net/package/Net_LDAP2/
60 class Net_LDAP2_Filter extends PEAR
63 * Storage for combination of filters
65 * This variable holds a array of filter objects
66 * that should be combined by this filter object.
71 protected $_subfilters = array();
74 * Match of this filter
76 * If this is a leaf filter, then a matching rule is stored,
77 * if it is a container, then it is a logical operator
87 * If we operate in leaf filter mode,
88 * then the constructing method stores
89 * the filter representation here
97 * Create a new Net_LDAP2_Filter object and parse $filter.
99 * This is for PERL Net::LDAP interface.
100 * Construction of Net_LDAP2_Filter objects should happen through either
101 * {@link create()} or {@link combine()} which give you more control.
102 * However, you may use the perl iterface if you already have generated filters.
104 * @param string $filter LDAP filter string
108 public function __construct($filter = false)
110 // The optional parameter must remain here, because otherwise create() crashes
111 if (false !== $filter) {
112 $filter_o = self::parse($filter);
113 if (PEAR::isError($filter_o)) {
114 $this->_filter = $filter_o; // assign error, so asString() can report it
116 $this->_filter = $filter_o->asString();
122 * Constructor of a new part of a LDAP filter.
124 * The following matching rules exists:
125 * - equals: One of the attributes values is exactly $value
126 * Please note that case sensitiviness is depends on the
127 * attributes syntax configured in the server.
128 * - begins: One of the attributes values must begin with $value
129 * - ends: One of the attributes values must end with $value
130 * - contains: One of the attributes values must contain $value
131 * - present | any: The attribute can contain any value but must be existent
132 * - greater: The attributes value is greater than $value
133 * - less: The attributes value is less than $value
134 * - greaterOrEqual: The attributes value is greater or equal than $value
135 * - lessOrEqual: The attributes value is less or equal than $value
136 * - approx: One of the attributes values is similar to $value
138 * Negation ("not") can be done by prepending the above operators with the
139 * "not" or "!" keyword, see example below.
141 * If $escape is set to true (default) then $value will be escaped
142 * properly. If it is set to false then $value will be treaten as raw filter value string.
143 * You should escape yourself using {@link Net_LDAP2_Util::escape_filter_value()}!
147 * // This will find entries that contain an attribute "sn" that ends with "foobar":
148 * $filter = Net_LDAP2_Filter::create('sn', 'ends', 'foobar');
150 * // This will find entries that contain an attribute "sn" that has any value set:
151 * $filter = Net_LDAP2_Filter::create('sn', 'any');
153 * // This will build a negated equals filter:
154 * $filter = Net_LDAP2_Filter::create('sn', 'not equals', 'foobar');
157 * @param string $attr_name Name of the attribute the filter should apply to
158 * @param string $match Matching rule (equals, begins, ends, contains, greater, less, greaterOrEqual, lessOrEqual, approx, any)
159 * @param string $value (optional) if given, then this is used as a filter
160 * @param boolean $escape Should $value be escaped? (default: yes, see {@link Net_LDAP2_Util::escape_filter_value()} for detailed information)
162 * @return Net_LDAP2_Filter|Net_LDAP2_Error
164 public static function &create($attr_name, $match, $value = '', $escape = true)
166 $leaf_filter = new Net_LDAP2_Filter();
168 $array = Net_LDAP2_Util::escape_filter_value(array($value));
172 $match = strtolower($match);
175 $neg_matches = array();
176 $negate_filter = false;
177 if (preg_match('/^(?:not|!)[\s_-](.+)/', $match, $neg_matches)) {
178 $negate_filter = true;
179 $match = $neg_matches[1];
182 // build basic filter
187 $leaf_filter->_filter = '(' . $attr_name . '=' . $value . ')';
190 $leaf_filter->_filter = '(' . $attr_name . '=' . $value . '*)';
193 $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . ')';
196 $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . '*)';
200 $leaf_filter->_filter = '(' . $attr_name . '>' . $value . ')';
204 $leaf_filter->_filter = '(' . $attr_name . '<' . $value . ')';
206 case 'greaterorequal':
208 $leaf_filter->_filter = '(' . $attr_name . '>=' . $value . ')';
212 $leaf_filter->_filter = '(' . $attr_name . '<=' . $value . ')';
216 $leaf_filter->_filter = '(' . $attr_name . '~=' . $value . ')';
219 case 'present': // alias that may improve user code readability
220 $leaf_filter->_filter = '(' . $attr_name . '=*)';
223 return PEAR::raiseError('Net_LDAP2_Filter create error: matching rule "' . $match . '" not known!');
226 // negate if requested
227 if ($negate_filter) {
228 $leaf_filter = Net_LDAP2_Filter::combine('!', $leaf_filter);
235 * Combine two or more filter objects using a logical operator
237 * This static method combines two or more filter objects and returns one single
238 * filter object that contains all the others.
239 * Call this method statically: $filter = Net_LDAP2_Filter::combine('or', array($filter1, $filter2))
240 * If the array contains filter strings instead of filter objects, we will try to parse them.
242 * @param string $log_op The locical operator. May be "and", "or", "not" or the subsequent logical equivalents "&", "|", "!"
243 * @param array|Net_LDAP2_Filter $filters array with Net_LDAP2_Filter objects
245 * @return Net_LDAP2_Filter|Net_LDAP2_Error
248 public static function &combine($log_op, $filters)
250 if (PEAR::isError($filters)) {
254 // substitude named operators to logical operators
255 if ($log_op == 'and') $log_op = '&';
256 if ($log_op == 'or') $log_op = '|';
257 if ($log_op == 'not') $log_op = '!';
259 // tests for sane operation
260 if ($log_op == '!') {
261 // Not-combination, here we only accept one filter object or filter string
262 if ($filters instanceof Net_LDAP2_Filter) {
263 $filters = array($filters); // force array
264 } elseif (is_string($filters)) {
265 $filter_o = self::parse($filters);
266 if (PEAR::isError($filter_o)) {
267 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: '.$filter_o->getMessage());
270 $filters = array($filter_o);
272 } elseif (is_array($filters)) {
273 if (count($filters) != 1) {
274 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is an array!');
276 } elseif (!($filters[0] instanceof Net_LDAP2_Filter)) {
277 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is not a valid Net_LDAP2_Filter nor a filter string!');
281 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is not a valid Net_LDAP2_Filter nor a filter string!');
284 } elseif ($log_op == '&' || $log_op == '|') {
285 if (!is_array($filters) || count($filters) < 2) {
286 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: parameter $filters is not an array or contains less than two Net_LDAP2_Filter objects!');
290 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: logical operator is not known!');
294 $combined_filter = new Net_LDAP2_Filter();
295 foreach ($filters as $key => $testfilter) { // check for errors
296 if (PEAR::isError($testfilter)) {
298 } elseif (is_string($testfilter)) {
299 // string found, try to parse into an filter object
300 $filter_o = self::parse($testfilter);
301 if (PEAR::isError($filter_o)) {
304 $filters[$key] = $filter_o;
306 } elseif (!$testfilter instanceof Net_LDAP2_Filter) {
307 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: invalid object passed in array $filters!');
312 $combined_filter->_subfilters = $filters;
313 $combined_filter->_match = $log_op;
314 return $combined_filter;
318 * Parse FILTER into a Net_LDAP2_Filter object
320 * This parses an filter string into Net_LDAP2_Filter objects.
322 * @param string $FILTER The filter string
325 * @return Net_LDAP2_Filter|Net_LDAP2_Error
326 * @todo Leaf-mode: Do we need to escape at all? what about *-chars?check for the need of encoding values, tackle problems (see code comments)
328 public static function parse($FILTER)
330 if (preg_match('/^\((.+?)\)$/', $FILTER, $matches)) {
331 // Check for right bracket syntax: count of unescaped opening
332 // brackets must match count of unescaped closing brackets.
333 // At this stage we may have:
334 // 1. one filter component with already removed outer brackets
335 // 2. one or more subfilter components
336 $c_openbracks = preg_match_all('/(?<!\\\\)\(/' , $matches[1], $notrelevant);
337 $c_closebracks = preg_match_all('/(?<!\\\\)\)/' , $matches[1], $notrelevant);
338 if ($c_openbracks != $c_closebracks) {
339 return PEAR::raiseError("Filter parsing error: invalid filter syntax - opening brackets do not match close brackets!");
342 if (in_array(substr($matches[1], 0, 1), array('!', '|', '&'))) {
343 // Subfilter processing: pass subfilters to parse() and combine
344 // the objects using the logical operator detected
345 // we have now something like "&(...)(...)(...)" but at least one part ("!(...)").
346 // Each subfilter could be an arbitary complex subfilter.
348 // extract logical operator and filter arguments
349 $log_op = substr($matches[1], 0, 1);
350 $remaining_component = substr($matches[1], 1);
352 // split $remaining_component into individual subfilters
353 // we cannot use split() for this, because we do not know the
354 // complexiness of the subfilter. Thus, we look trough the filter
355 // string and just recognize ending filters at the first level.
356 // We record the index number of the char and use that information
357 // later to split the string.
358 $sub_index_pos = array();
359 $prev_char = ''; // previous character looked at
360 $level = 0; // denotes the current bracket level we are,
361 // >1 is too deep, 1 is ok, 0 is outside any
363 for ($curpos = 0; $curpos < strlen($remaining_component); $curpos++) {
364 $cur_char = substr($remaining_component, $curpos, 1);
366 // rise/lower bracket level
367 if ($cur_char == '(' && $prev_char != '\\') {
369 } elseif ($cur_char == ')' && $prev_char != '\\') {
373 if ($cur_char == '(' && $prev_char == ')' && $level == 1) {
374 array_push($sub_index_pos, $curpos); // mark the position for splitting
376 $prev_char = $cur_char;
379 // now perform the splits. To get also the last part, we
380 // need to add the "END" index to the split array
381 array_push($sub_index_pos, strlen($remaining_component));
382 $subfilters = array();
384 foreach ($sub_index_pos as $s_pos) {
385 $str_part = substr($remaining_component, $oldpos, $s_pos - $oldpos);
386 array_push($subfilters, $str_part);
390 // some error checking...
391 if (count($subfilters) == 1) {
392 // only one subfilter found
393 } elseif (count($subfilters) > 1) {
394 // several subfilters found
395 if ($log_op == "!") {
396 return PEAR::raiseError("Filter parsing error: invalid filter syntax - NOT operator detected but several arguments given!");
399 // this should not happen unless the user specified a wrong filter
400 return PEAR::raiseError("Filter parsing error: invalid filter syntax - got operator '$log_op' but no argument!");
403 // Now parse the subfilters into objects and combine them using the operator
404 $subfilters_o = array();
405 foreach ($subfilters as $s_s) {
406 $o = self::parse($s_s);
407 if (PEAR::isError($o)) {
410 array_push($subfilters_o, self::parse($s_s));
414 $filter_o = self::combine($log_op, $subfilters_o);
418 // This is one leaf filter component, do some syntax checks, then escape and build filter_o
419 // $matches[1] should be now something like "foo=bar"
421 // detect multiple leaf components
422 // [TODO] Maybe this will make problems with filters containing brackets inside the value
423 if (stristr($matches[1], ')(')) {
424 return PEAR::raiseError("Filter parsing error: invalid filter syntax - multiple leaf components detected!");
426 $filter_parts = Net_LDAP2_Util::split_attribute_string($matches[1], true, true);
427 if (count($filter_parts) != 3) {
428 return PEAR::raiseError("Filter parsing error: invalid filter syntax - unknown matching rule used");
430 $filter_o = new Net_LDAP2_Filter();
431 // [TODO]: Do we need to escape at all? what about *-chars user provide and that should remain special?
432 // I think, those prevent escaping! We need to check against PERL Net::LDAP!
433 // $value_arr = Net_LDAP2_Util::escape_filter_value(array($filter_parts[2]));
434 // $value = $value_arr[0];
435 $value = $filter_parts[2];
436 $filter_o->_filter = '('.$filter_parts[0].$filter_parts[1].$value.')';
442 // ERROR: Filter components must be enclosed in round brackets
443 return PEAR::raiseError("Filter parsing error: invalid filter syntax - filter components must be enclosed in round brackets");
448 * Get the string representation of this filter
450 * This method runs through all filter objects and creates
451 * the string representation of the filter. If this
452 * filter object is a leaf filter, then it will return
453 * the string representation of this filter.
455 * @return string|Net_LDAP2_Error
457 public function asString()
459 if ($this->isLeaf()) {
460 $return = $this->_filter;
463 foreach ($this->_subfilters as $filter) {
464 $return = $return.$filter->asString();
466 $return = '(' . $this->_match . $return . ')';
472 * Alias for perl interface as_string()
475 * @return string|Net_LDAP2_Error
477 public function as_string()
479 return $this->asString();
483 * Print the text representation of the filter to FH, or the currently selected output handle if FH is not given
485 * This method is only for compatibility to the perl interface.
486 * However, the original method was called "print" but due to PHP language restrictions,
487 * we can't have a print() method.
489 * @param resource $FH (optional) A filehandle resource
491 * @return true|Net_LDAP2_Error
493 public function printMe($FH = false)
495 if (!is_resource($FH)) {
496 if (PEAR::isError($FH)) {
499 $filter_str = $this->asString();
500 if (PEAR::isError($filter_str)) {
506 $filter_str = $this->asString();
507 if (PEAR::isError($filter_str)) {
510 $res = @fwrite($FH, $this->asString());
512 return PEAR::raiseError("Unable to write filter string to filehandle \$FH!");
520 * This can be used to escape a string to provide a valid LDAP-Filter.
522 * LDAP will only recognise certain characters as the
523 * character istself if they are properly escaped. This is
524 * what this method does.
525 * The method can be called statically, so you can use it outside
526 * for your own purposes (eg for escaping only parts of strings)
528 * In fact, this is just a shorthand to {@link Net_LDAP2_Util::escape_filter_value()}.
529 * For upward compatibiliy reasons you are strongly encouraged to use the escape
530 * methods provided by the Net_LDAP2_Util class.
532 * @param string $value Any string who should be escaped
535 * @return string The string $string, but escaped
536 * @deprecated Do not use this method anymore, instead use Net_LDAP2_Util::escape_filter_value() directly
538 public static function escape($value)
540 $return = Net_LDAP2_Util::escape_filter_value(array($value));
545 * Is this a container or a leaf filter object?
550 protected function isLeaf()
552 if (count($this->_subfilters) > 0) {
553 return false; // Container!
555 return true; // Leaf!
560 * Filter entries using this filter or see if a filter matches
562 * @todo Currently slow and naive implementation with preg_match, could be optimized (esp. begins, ends filters etc)
563 * @todo Currently only "="-based matches (equals, begins, ends, contains, any) implemented; Implement all the stuff!
564 * @todo Implement expert code with schema checks in case $entry is connected to a directory
565 * @param array|Net_LDAP2_Entry The entry (or array with entries) to check
566 * @param array If given, the array will be appended with entries who matched the filter. Return value is true if any entry matched.
567 * @return int|Net_LDAP2_Error Returns the number of matched entries or error
569 function matches(&$entries, &$results=array()) {
572 if (!is_array($entries)) {
573 $all_entries = array(&$entries);
575 $all_entries = &$entries;
578 foreach ($all_entries as $entry) {
579 // look at the current entry and see if filter matches
581 $entry_matched = false;
582 // if this is not a single component, do calculate all subfilters,
583 // then assert the partial results with the given combination modifier
584 if (!$this->isLeaf()) {
586 // get partial results from subfilters
587 $partial_results = array();
588 foreach ($this->_subfilters as $filter) {
589 $partial_results[] = $filter->matches($entry);
592 // evaluate partial results using this filters combination rule
593 switch ($this->_match) {
595 // result is the neagtive result of the assertion
596 $entry_matched = !$partial_results[0];
600 // all partial results have to be boolean-true
601 $entry_matched = !in_array(false, $partial_results);
605 // at least one partial result has to be true
606 $entry_matched = in_array(true, $partial_results);
611 // Leaf filter: assert given entry
612 // [TODO]: Could be optimized to avoid preg_match especially with "ends", "begins" etc
614 // Translate the LDAP-match to some preg_match expression and evaluate it
615 list($attribute, $match, $assertValue) = $this->getComponents();
618 $regexp = '/^'.str_replace('*', '.*', $assertValue).'$/i'; // not case sensitive unless specified by schema
619 $entry_matched = $entry->pregMatch($regexp, $attribute);
622 // -------------------------------------
623 // [TODO]: implement <, >, <=, >= and =~
624 // -------------------------------------
627 $err = PEAR::raiseError("Net_LDAP2_Filter match error: unsupported match rule '$match'!");
633 // process filter matching result
634 if ($entry_matched) {
641 return $numOfMatches;
646 * Retrieve this leaf-filters attribute, match and value component.
648 * For leaf filters, this returns array(attr, match, value).
649 * Match is be the logical operator, not the text representation,
650 * eg "=" instead of "equals". Note that some operators are really
651 * a combination of operator+value with wildcard, like
652 * "begins": That will return "=" with the value "value*"!
654 * For non-leaf filters this will drop an error.
656 * @todo $this->_match is not always available and thus not usable here; it would be great if it would set in the factory methods and constructor.
657 * @return array|Net_LDAP2_Error
659 function getComponents() {
660 if ($this->isLeaf()) {
661 $raw_filter = preg_replace('/^\(|\)$/', '', $this->_filter);
662 $parts = Net_LDAP2_Util::split_attribute_string($raw_filter, true, true);
663 if (count($parts) != 3) {
664 return PEAR::raiseError("Net_LDAP2_Filter getComponents() error: invalid filter syntax - unknown matching rule used");
669 return PEAR::raiseError('Net_LDAP2_Filter getComponents() call is invalid for non-leaf filters!');