]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/Net/LDAP2/Filter.php
Revert "* [Cc]an't -> [Cc]annot"
[quix0rs-gnu-social.git] / extlib / Net / LDAP2 / Filter.php
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4: */
3 /**
4 * File containing the Net_LDAP2_Filter interface class.
5 *
6 * PHP version 5
7 *
8 * @category  Net
9 * @package   Net_LDAP2
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/
15 */
16
17 /**
18 * Includes
19 */
20 require_once 'PEAR.php';
21 require_once 'Util.php';
22
23 /**
24 * Object representation of a part of a LDAP filter.
25 *
26 * This Class is not completely compatible to the PERL interface!
27 *
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.
35 *
36 * LDAP filters are defined in RFC-2254 and can be found under
37 * {@link http://www.ietf.org/rfc/rfc2254.txt}
38 *
39 * Here a quick copy&paste example:
40 * <code>
41 * $filter0 = Net_LDAP2_Filter::create('stars', 'equals', '***');
42 * $filter_not0 = Net_LDAP2_Filter::combine('not', $filter0);
43 *
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));
47 *
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.
51 * </code>
52 *
53 * @category Net
54 * @package  Net_LDAP2
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/
58 */
59 class Net_LDAP2_Filter extends PEAR
60 {
61     /**
62     * Storage for combination of filters
63     *
64     * This variable holds a array of filter objects
65     * that should be combined by this filter object.
66     *
67     * @access protected
68     * @var array
69     */
70     protected $_subfilters = array();
71
72     /**
73     * Match of this filter
74     *
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
77     *
78     * @access protected
79     * @var string
80     */
81     protected $_match;
82
83     /**
84     * Single filter
85     *
86     * If we operate in leaf filter mode,
87     * then the constructing method stores
88     * the filter representation here
89     *
90     * @acces private
91     * @var string
92     */
93     protected $_filter;
94
95     /**
96     * Create a new Net_LDAP2_Filter object and parse $filter.
97     *
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.
102     *
103     * @param string $filter LDAP filter string
104     *
105     * @see parse()
106     */
107     public function __construct($filter = false)
108     {
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
114             } else {
115                 $this->_filter = $filter_o->asString();
116             }
117         }
118     }
119
120     /**
121     * Constructor of a new part of a LDAP filter.
122     *
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
136     *
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()}!
140     *
141     * Examples:
142     * <code>
143     *   // This will find entries that contain an attribute "sn" that ends with "foobar":
144     *   $filter = new Net_LDAP2_Filter('sn', 'ends', 'foobar');
145     *
146     *   // This will find entries that contain an attribute "sn" that has any value set:
147     *   $filter = new Net_LDAP2_Filter('sn', 'any');
148     * </code>
149     *
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)
154     *
155     * @return Net_LDAP2_Filter|Net_LDAP2_Error
156     */
157     public static function &create($attr_name, $match, $value = '', $escape = true)
158     {
159         $leaf_filter = new Net_LDAP2_Filter();
160         if ($escape) {
161             $array = Net_LDAP2_Util::escape_filter_value(array($value));
162             $value = $array[0];
163         }
164         switch (strtolower($match)) {
165         case 'equals':
166             $leaf_filter->_filter = '(' . $attr_name . '=' . $value . ')';
167             break;
168         case 'begins':
169             $leaf_filter->_filter = '(' . $attr_name . '=' . $value . '*)';
170             break;
171         case 'ends':
172             $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . ')';
173             break;
174         case 'contains':
175             $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . '*)';
176             break;
177         case 'greater':
178             $leaf_filter->_filter = '(' . $attr_name . '>' . $value . ')';
179             break;
180         case 'less':
181             $leaf_filter->_filter = '(' . $attr_name . '<' . $value . ')';
182             break;
183         case 'greaterorequal':
184         case '>=':
185             $leaf_filter->_filter = '(' . $attr_name . '>=' . $value . ')';
186             break;
187         case 'lessorequal':
188         case '<=':
189             $leaf_filter->_filter = '(' . $attr_name . '<=' . $value . ')';
190             break;
191         case 'approx':
192         case '~=':
193             $leaf_filter->_filter = '(' . $attr_name . '~=' . $value . ')';
194             break;
195         case 'any':
196         case 'present': // alias that may improve user code readability
197             $leaf_filter->_filter = '(' . $attr_name . '=*)';
198             break;
199         default:
200             return PEAR::raiseError('Net_LDAP2_Filter create error: matching rule "' . $match . '" not known!');
201         }
202         return $leaf_filter;
203     }
204
205     /**
206     * Combine two or more filter objects using a logical operator
207     *
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.
212     *
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
215     *
216     * @return Net_LDAP2_Filter|Net_LDAP2_Error
217     * @static
218     */
219     public static function &combine($log_op, $filters)
220     {
221         if (PEAR::isError($filters)) {
222             return $filters;
223         }
224
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 = '!';
229
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());
239                     return $err;
240                 } else {
241                     $filters = array($filter_o);
242                 }
243             } elseif (is_array($filters)) {
244                 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: operator is "not" but $filter is an array!');
245                 return $err;
246             } else {
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!');
248                 return $err;
249             }
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!');
253                 return $err;
254             }
255         } else {
256             $err = PEAR::raiseError('Net_LDAP2_Filter combine error: logical operator is not known!');
257             return $err;
258         }
259
260         $combined_filter = new Net_LDAP2_Filter();
261         foreach ($filters as $key => $testfilter) {     // check for errors
262             if (PEAR::isError($testfilter)) {
263                 return $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)) {
268                     return $filter_o;
269                 } else {
270                     $filters[$key] = $filter_o;
271                 }
272             } elseif (!$testfilter instanceof Net_LDAP2_Filter) {
273                 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: invalid object passed in array $filters!');
274                 return $err;
275             }
276         }
277
278         $combined_filter->_subfilters = $filters;
279         $combined_filter->_match      = $log_op;
280         return $combined_filter;
281     }
282
283     /**
284     * Parse FILTER into a Net_LDAP2_Filter object
285     *
286     * This parses an filter string into Net_LDAP2_Filter objects.
287     *
288     * @param string $FILTER The filter string
289     *
290     * @access static
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)
293     */
294     public static function parse($FILTER)
295     {
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.
302
303                 // extract logical operator and filter arguments
304                 $log_op              = substr($matches[1], 0, 1);
305                 $remaining_component = substr($matches[1], 1);
306
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
317                                      //   subcomponent
318                 for ($curpos = 0; $curpos < strlen($remaining_component); $curpos++) {
319                     $cur_char = substr($remaining_component, $curpos, 1);
320
321                     // rise/lower bracket level
322                     if ($cur_char == '(' && $prev_char != '\\') {
323                         $level++;
324                     } elseif  ($cur_char == ')' && $prev_char != '\\') {
325                         $level--;
326                     }
327
328                     if ($cur_char == '(' && $prev_char == ')' && $level == 1) {
329                         array_push($sub_index_pos, $curpos); // mark the position for splitting
330                     }
331                     $prev_char = $cur_char;
332                 }
333
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();
338                 $oldpos = 0;
339                 foreach ($sub_index_pos as $s_pos) {
340                     $str_part = substr($remaining_component, $oldpos, $s_pos - $oldpos);
341                     array_push($subfilters, $str_part);
342                     $oldpos = $s_pos;
343                 }
344
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!");
352                     }
353                 } else {
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!");
356                 }
357
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)) {
363                         return $o;
364                     } else {
365                         array_push($subfilters_o, self::parse($s_s));
366                     }
367                 }
368
369                 $filter_o = self::combine($log_op, $subfilters_o);
370                 return $filter_o;
371
372             } else {
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"
375
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!");
380                 } else {
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");
384                     } else {
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.')';
392                         return $filter_o;
393                     }
394                 }
395             }
396         } else {
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");
399         }
400     }
401
402     /**
403     * Get the string representation of this filter
404     *
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.
409     *
410     * @return string|Net_LDAP2_Error
411     */
412     public function asString()
413     {
414         if ($this->isLeaf()) {
415             $return = $this->_filter;
416         } else {
417             $return = '';
418             foreach ($this->_subfilters as $filter) {
419                 $return = $return.$filter->asString();
420             }
421             $return = '(' . $this->_match . $return . ')';
422         }
423         return $return;
424     }
425
426     /**
427     * Alias for perl interface as_string()
428     *
429     * @see asString()
430     * @return string|Net_LDAP2_Error
431     */
432     public function as_string()
433     {
434         return $this->asString();
435     }
436
437     /**
438     * Print the text representation of the filter to FH, or the currently selected output handle if FH is not given
439     *
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 can't have a print() method.
443     *
444     * @param resource $FH (optional) A filehandle resource
445     *
446     * @return true|Net_LDAP2_Error
447     */
448     public function printMe($FH = false)
449     {
450         if (!is_resource($FH)) {
451             if (PEAR::isError($FH)) {
452                 return $FH;
453             }
454             $filter_str = $this->asString();
455             if (PEAR::isError($filter_str)) {
456                 return $filter_str;
457             } else {
458                 print($filter_str);
459             }
460         } else {
461             $filter_str = $this->asString();
462             if (PEAR::isError($filter_str)) {
463                 return $filter_str;
464             } else {
465                 $res = @fwrite($FH, $this->asString());
466                 if ($res == false) {
467                     return PEAR::raiseError("Unable to write filter string to filehandle \$FH!");
468                 }
469             }
470         }
471         return true;
472     }
473
474     /**
475     * This can be used to escape a string to provide a valid LDAP-Filter.
476     *
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)
482     *
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.
486     *
487     * @param string $value Any string who should be escaped
488     *
489     * @static
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
492     */
493     public static function escape($value)
494     {
495         $return = Net_LDAP2_Util::escape_filter_value(array($value));
496         return $return[0];
497     }
498
499     /**
500     * Is this a container or a leaf filter object?
501     *
502     * @access protected
503     * @return boolean
504     */
505     protected function isLeaf()
506     {
507         if (count($this->_subfilters) > 0) {
508             return false; // Container!
509         } else {
510             return true; // Leaf!
511         }
512     }
513 }
514 ?>