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 289978 2009-10-27 09:56:41Z beni $
14 * @link http://pear.php.net/package/Net_LDAP2/
20 require_once 'PEAR.php';
21 require_once 'Util.php';
24 * Object representation of a part of a LDAP filter.
26 * This Class is not completely compatible to the PERL interface!
28 * The purpose of this class is, that users can easily build LDAP filters
29 * without having to worry about right escaping etc.
30 * A Filter is built using several independent filter objects
31 * which are combined afterwards. This object works in two
32 * modes, depending how the object is created.
33 * If the object is created using the {@link create()} method, then this is a leaf-object.
34 * If the object is created using the {@link combine()} method, then this is a container object.
36 * LDAP filters are defined in RFC-2254 and can be found under
37 * {@link http://www.ietf.org/rfc/rfc2254.txt}
39 * Here a quick copy&paste example:
41 * $filter0 = Net_LDAP2_Filter::create('stars', 'equals', '***');
42 * $filter_not0 = Net_LDAP2_Filter::combine('not', $filter0);
44 * $filter1 = Net_LDAP2_Filter::create('gn', 'begins', 'bar');
45 * $filter2 = Net_LDAP2_Filter::create('gn', 'ends', 'baz');
46 * $filter_comp = Net_LDAP2_Filter::combine('or',array($filter_not0, $filter1, $filter2));
48 * echo $filter_comp->asString();
49 * // This will output: (|(!(stars=\0x5c0x2a\0x5c0x2a\0x5c0x2a))(gn=bar*)(gn=*baz))
50 * // The stars in $filter0 are treaten as real stars unless you disable escaping.
55 * @author Benedikt Hallinger <beni@php.net>
56 * @license http://www.gnu.org/copyleft/lesser.html LGPL
57 * @link http://pear.php.net/package/Net_LDAP2/
59 class Net_LDAP2_Filter extends PEAR
62 * Storage for combination of filters
64 * This variable holds a array of filter objects
65 * that should be combined by this filter object.
70 protected $_subfilters = array();
73 * Match of this filter
75 * If this is a leaf filter, then a matching rule is stored,
76 * if it is a container, then it is a logical operator
86 * If we operate in leaf filter mode,
87 * then the constructing method stores
88 * the filter representation here
96 * Create a new Net_LDAP2_Filter object and parse $filter.
98 * This is for PERL Net::LDAP interface.
99 * Construction of Net_LDAP2_Filter objects should happen through either
100 * {@link create()} or {@link combine()} which give you more control.
101 * However, you may use the perl iterface if you already have generated filters.
103 * @param string $filter LDAP filter string
107 public function __construct($filter = false)
109 // The optional parameter must remain here, because otherwise create() crashes
110 if (false !== $filter) {
111 $filter_o = self::parse($filter);
112 if (PEAR::isError($filter_o)) {
113 $this->_filter = $filter_o; // assign error, so asString() can report it
115 $this->_filter = $filter_o->asString();
121 * Constructor of a new part of a LDAP filter.
123 * The following matching rules exists:
124 * - equals: One of the attributes values is exactly $value
125 * Please note that case sensitiviness is depends on the
126 * attributes syntax configured in the server.
127 * - begins: One of the attributes values must begin with $value
128 * - ends: One of the attributes values must end with $value
129 * - contains: One of the attributes values must contain $value
130 * - present | any: The attribute can contain any value but must be existent
131 * - greater: The attributes value is greater than $value
132 * - less: The attributes value is less than $value
133 * - greaterOrEqual: The attributes value is greater or equal than $value
134 * - lessOrEqual: The attributes value is less or equal than $value
135 * - approx: One of the attributes values is similar to $value
137 * If $escape is set to true (default) then $value will be escaped
138 * properly. If it is set to false then $value will be treaten as raw filter value string.
139 * You should escape yourself using {@link Net_LDAP2_Util::escape_filter_value()}!
143 * // This will find entries that contain an attribute "sn" that ends with "foobar":
144 * $filter = new Net_LDAP2_Filter('sn', 'ends', 'foobar');
146 * // This will find entries that contain an attribute "sn" that has any value set:
147 * $filter = new Net_LDAP2_Filter('sn', 'any');
150 * @param string $attr_name Name of the attribute the filter should apply to
151 * @param string $match Matching rule (equals, begins, ends, contains, greater, less, greaterOrEqual, lessOrEqual, approx, any)
152 * @param string $value (optional) if given, then this is used as a filter
153 * @param boolean $escape Should $value be escaped? (default: yes, see {@link Net_LDAP2_Util::escape_filter_value()} for detailed information)
155 * @return Net_LDAP2_Filter|Net_LDAP2_Error
157 public static function &create($attr_name, $match, $value = '', $escape = true)
159 $leaf_filter = new Net_LDAP2_Filter();
161 $array = Net_LDAP2_Util::escape_filter_value(array($value));
164 switch (strtolower($match)) {
166 $leaf_filter->_filter = '(' . $attr_name . '=' . $value . ')';
169 $leaf_filter->_filter = '(' . $attr_name . '=' . $value . '*)';
172 $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . ')';
175 $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . '*)';
178 $leaf_filter->_filter = '(' . $attr_name . '>' . $value . ')';
181 $leaf_filter->_filter = '(' . $attr_name . '<' . $value . ')';
183 case 'greaterorequal':
185 $leaf_filter->_filter = '(' . $attr_name . '>=' . $value . ')';
189 $leaf_filter->_filter = '(' . $attr_name . '<=' . $value . ')';
193 $leaf_filter->_filter = '(' . $attr_name . '~=' . $value . ')';
196 case 'present': // alias that may improve user code readability
197 $leaf_filter->_filter = '(' . $attr_name . '=*)';
200 return PEAR::raiseError('Net_LDAP2_Filter create error: matching rule "' . $match . '" not known!');
206 * Combine two or more filter objects using a logical operator
208 * This static method combines two or more filter objects and returns one single
209 * filter object that contains all the others.
210 * Call this method statically: $filter = Net_LDAP2_Filter('or', array($filter1, $filter2))
211 * If the array contains filter strings instead of filter objects, we will try to parse them.
213 * @param string $log_op The locicall operator. May be "and", "or", "not" or the subsequent logical equivalents "&", "|", "!"
214 * @param array|Net_LDAP2_Filter $filters array with Net_LDAP2_Filter objects
216 * @return Net_LDAP2_Filter|Net_LDAP2_Error
219 public static function &combine($log_op, $filters)
221 if (PEAR::isError($filters)) {
225 // substitude named operators to logical operators
226 if ($log_op == 'and') $log_op = '&';
227 if ($log_op == 'or') $log_op = '|';
228 if ($log_op == 'not') $log_op = '!';
230 // tests for sane operation
231 if ($log_op == '!') {
232 // Not-combination, here we only accept one filter object or filter string
233 if ($filters instanceof Net_LDAP2_Filter) {
234 $filters = array($filters); // force array
235 } elseif (is_string($filters)) {
236 $filter_o = self::parse($filters);
237 if (PEAR::isError($filter_o)) {
238 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: '.$filter_o->getMessage());
241 $filters = array($filter_o);
243 } elseif (is_array($filters)) {
244 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is an array!');
247 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is not a valid Net_LDAP2_Filter nor a filter string!');
250 } elseif ($log_op == '&' || $log_op == '|') {
251 if (!is_array($filters) || count($filters) < 2) {
252 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: parameter $filters is not an array or contains less than two Net_LDAP2_Filter objects!');
256 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: logical operator is not known!');
260 $combined_filter = new Net_LDAP2_Filter();
261 foreach ($filters as $key => $testfilter) { // check for errors
262 if (PEAR::isError($testfilter)) {
264 } elseif (is_string($testfilter)) {
265 // string found, try to parse into an filter object
266 $filter_o = self::parse($testfilter);
267 if (PEAR::isError($filter_o)) {
270 $filters[$key] = $filter_o;
272 } elseif (!$testfilter instanceof Net_LDAP2_Filter) {
273 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: invalid object passed in array $filters!');
278 $combined_filter->_subfilters = $filters;
279 $combined_filter->_match = $log_op;
280 return $combined_filter;
284 * Parse FILTER into a Net_LDAP2_Filter object
286 * This parses an filter string into Net_LDAP2_Filter objects.
288 * @param string $FILTER The filter string
291 * @return Net_LDAP2_Filter|Net_LDAP2_Error
292 * @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)
294 public static function parse($FILTER)
296 if (preg_match('/^\((.+?)\)$/', $FILTER, $matches)) {
297 if (in_array(substr($matches[1], 0, 1), array('!', '|', '&'))) {
298 // Subfilter processing: pass subfilters to parse() and combine
299 // the objects using the logical operator detected
300 // we have now something like "&(...)(...)(...)" but at least one part ("!(...)").
301 // Each subfilter could be an arbitary complex subfilter.
303 // extract logical operator and filter arguments
304 $log_op = substr($matches[1], 0, 1);
305 $remaining_component = substr($matches[1], 1);
307 // split $remaining_component into individual subfilters
308 // we cannot use split() for this, because we do not know the
309 // complexiness of the subfilter. Thus, we look trough the filter
310 // string and just recognize ending filters at the first level.
311 // We record the index number of the char and use that information
312 // later to split the string.
313 $sub_index_pos = array();
314 $prev_char = ''; // previous character looked at
315 $level = 0; // denotes the current bracket level we are,
316 // >1 is too deep, 1 is ok, 0 is outside any
318 for ($curpos = 0; $curpos < strlen($remaining_component); $curpos++) {
319 $cur_char = substr($remaining_component, $curpos, 1);
321 // rise/lower bracket level
322 if ($cur_char == '(' && $prev_char != '\\') {
324 } elseif ($cur_char == ')' && $prev_char != '\\') {
328 if ($cur_char == '(' && $prev_char == ')' && $level == 1) {
329 array_push($sub_index_pos, $curpos); // mark the position for splitting
331 $prev_char = $cur_char;
334 // now perform the splits. To get also the last part, we
335 // need to add the "END" index to the split array
336 array_push($sub_index_pos, strlen($remaining_component));
337 $subfilters = array();
339 foreach ($sub_index_pos as $s_pos) {
340 $str_part = substr($remaining_component, $oldpos, $s_pos - $oldpos);
341 array_push($subfilters, $str_part);
345 // some error checking...
346 if (count($subfilters) == 1) {
347 // only one subfilter found
348 } elseif (count($subfilters) > 1) {
349 // several subfilters found
350 if ($log_op == "!") {
351 return PEAR::raiseError("Filter parsing error: invalid filter syntax - NOT operator detected but several arguments given!");
354 // this should not happen unless the user specified a wrong filter
355 return PEAR::raiseError("Filter parsing error: invalid filter syntax - got operator '$log_op' but no argument!");
358 // Now parse the subfilters into objects and combine them using the operator
359 $subfilters_o = array();
360 foreach ($subfilters as $s_s) {
361 $o = self::parse($s_s);
362 if (PEAR::isError($o)) {
365 array_push($subfilters_o, self::parse($s_s));
369 $filter_o = self::combine($log_op, $subfilters_o);
373 // This is one leaf filter component, do some syntax checks, then escape and build filter_o
374 // $matches[1] should be now something like "foo=bar"
376 // detect multiple leaf components
377 // [TODO] Maybe this will make problems with filters containing brackets inside the value
378 if (stristr($matches[1], ')(')) {
379 return PEAR::raiseError("Filter parsing error: invalid filter syntax - multiple leaf components detected!");
381 $filter_parts = preg_split('/(?<!\\\\)(=|=~|>|<|>=|<=)/', $matches[1], 2, PREG_SPLIT_DELIM_CAPTURE);
382 if (count($filter_parts) != 3) {
383 return PEAR::raiseError("Filter parsing error: invalid filter syntax - unknown matching rule used");
385 $filter_o = new Net_LDAP2_Filter();
386 // [TODO]: Do we need to escape at all? what about *-chars user provide and that should remain special?
387 // I think, those prevent escaping! We need to check against PERL Net::LDAP!
388 // $value_arr = Net_LDAP2_Util::escape_filter_value(array($filter_parts[2]));
389 // $value = $value_arr[0];
390 $value = $filter_parts[2];
391 $filter_o->_filter = '('.$filter_parts[0].$filter_parts[1].$value.')';
397 // ERROR: Filter components must be enclosed in round brackets
398 return PEAR::raiseError("Filter parsing error: invalid filter syntax - filter components must be enclosed in round brackets");
403 * Get the string representation of this filter
405 * This method runs through all filter objects and creates
406 * the string representation of the filter. If this
407 * filter object is a leaf filter, then it will return
408 * the string representation of this filter.
410 * @return string|Net_LDAP2_Error
412 public function asString()
414 if ($this->isLeaf()) {
415 $return = $this->_filter;
418 foreach ($this->_subfilters as $filter) {
419 $return = $return.$filter->asString();
421 $return = '(' . $this->_match . $return . ')';
427 * Alias for perl interface as_string()
430 * @return string|Net_LDAP2_Error
432 public function as_string()
434 return $this->asString();
438 * Print the text representation of the filter to FH, or the currently selected output handle if FH is not given
440 * This method is only for compatibility to the perl interface.
441 * However, the original method was called "print" but due to PHP language restrictions,
442 * we cannot have a print() method.
444 * @param resource $FH (optional) A filehandle resource
446 * @return true|Net_LDAP2_Error
448 public function printMe($FH = false)
450 if (!is_resource($FH)) {
451 if (PEAR::isError($FH)) {
454 $filter_str = $this->asString();
455 if (PEAR::isError($filter_str)) {
461 $filter_str = $this->asString();
462 if (PEAR::isError($filter_str)) {
465 $res = @fwrite($FH, $this->asString());
467 return PEAR::raiseError("Unable to write filter string to filehandle \$FH!");
475 * This can be used to escape a string to provide a valid LDAP-Filter.
477 * LDAP will only recognise certain characters as the
478 * character istself if they are properly escaped. This is
479 * what this method does.
480 * The method can be called statically, so you can use it outside
481 * for your own purposes (eg for escaping only parts of strings)
483 * In fact, this is just a shorthand to {@link Net_LDAP2_Util::escape_filter_value()}.
484 * For upward compatibiliy reasons you are strongly encouraged to use the escape
485 * methods provided by the Net_LDAP2_Util class.
487 * @param string $value Any string who should be escaped
490 * @return string The string $string, but escaped
491 * @deprecated Do not use this method anymore, instead use Net_LDAP2_Util::escape_filter_value() directly
493 public static function escape($value)
495 $return = Net_LDAP2_Util::escape_filter_value(array($value));
500 * Is this a container or a leaf filter object?
505 protected function isLeaf()
507 if (count($this->_subfilters) > 0) {
508 return false; // Container!
510 return true; // Leaf!