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$ 14* @link http://pear.php.net/package/Net_LDAP2/ 15*/ 16 17/** 18* Includes 19*/ 20require_once 'PEAR.php'; 21require_once 'Net/LDAP2/Util.php'; 22require_once 'Net/LDAP2/Entry.php'; 23 24/** 25* Object representation of a part of a LDAP filter. 26* 27* This Class is not completely compatible to the PERL interface! 28* 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. 36* 37* LDAP filters are defined in RFC-2254 and can be found under 38* {@link http://www.ietf.org/rfc/rfc2254.txt} 39* 40* Here a quick copy&paste example: 41* <code> 42* $filter0 = Net_LDAP2_Filter::create('stars', 'equals', '***'); 43* $filter_not0 = Net_LDAP2_Filter::combine('not', $filter0); 44* 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)); 48* 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. 52* </code> 53* 54* @category Net 55* @package Net_LDAP2 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/ 59*/ 60class Net_LDAP2_Filter extends PEAR 61{ 62 /** 63 * Storage for combination of filters 64 * 65 * This variable holds a array of filter objects 66 * that should be combined by this filter object. 67 * 68 * @access protected 69 * @var array 70 */ 71 protected $_subfilters = array(); 72 73 /** 74 * Match of this filter 75 * 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 78 * 79 * @access protected 80 * @var string 81 */ 82 protected $_match; 83 84 /** 85 * Single filter 86 * 87 * If we operate in leaf filter mode, 88 * then the constructing method stores 89 * the filter representation here 90 * 91 * @acces private 92 * @var string 93 */ 94 protected $_filter; 95 96 /** 97 * Create a new Net_LDAP2_Filter object and parse $filter. 98 * 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. 103 * 104 * @param string $filter LDAP filter string 105 * 106 * @see parse() 107 */ 108 public function __construct($filter = false) 109 { 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 115 } else { 116 $this->_filter = $filter_o->asString(); 117 } 118 } 119 } 120 121 /** 122 * Constructor of a new part of a LDAP filter. 123 * 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 137 * 138 * Negation ("not") can be done by prepending the above operators with the 139 * "not" or "!" keyword, see example below. 140 * 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()}! 144 * 145 * Examples: 146 * <code> 147 * // This will find entries that contain an attribute "sn" that ends with "foobar": 148 * $filter = Net_LDAP2_Filter::create('sn', 'ends', 'foobar'); 149 * 150 * // This will find entries that contain an attribute "sn" that has any value set: 151 * $filter = Net_LDAP2_Filter::create('sn', 'any'); 152 * 153 * // This will build a negated equals filter: 154 * $filter = Net_LDAP2_Filter::create('sn', 'not equals', 'foobar'); 155 * </code> 156 * 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) 161 * 162 * @return Net_LDAP2_Filter|Net_LDAP2_Error 163 */ 164 public static function create($attr_name, $match, $value = '', $escape = true) 165 { 166 $leaf_filter = new Net_LDAP2_Filter(); 167 if ($escape) { 168 $array = Net_LDAP2_Util::escape_filter_value(array($value)); 169 $value = $array[0]; 170 } 171 172 $match = strtolower($match); 173 174 // detect negation 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]; 180 } 181 182 // build basic filter 183 switch ($match) { 184 case 'equals': 185 case '=': 186 case '==': 187 $leaf_filter->_filter = '(' . $attr_name . '=' . $value . ')'; 188 break; 189 case 'begins': 190 $leaf_filter->_filter = '(' . $attr_name . '=' . $value . '*)'; 191 break; 192 case 'ends': 193 $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . ')'; 194 break; 195 case 'contains': 196 $leaf_filter->_filter = '(' . $attr_name . '=*' . $value . '*)'; 197 break; 198 case 'greater': 199 case '>': 200 $leaf_filter->_filter = '(' . $attr_name . '>' . $value . ')'; 201 break; 202 case 'less': 203 case '<': 204 $leaf_filter->_filter = '(' . $attr_name . '<' . $value . ')'; 205 break; 206 case 'greaterorequal': 207 case '>=': 208 $leaf_filter->_filter = '(' . $attr_name . '>=' . $value . ')'; 209 break; 210 case 'lessorequal': 211 case '<=': 212 $leaf_filter->_filter = '(' . $attr_name . '<=' . $value . ')'; 213 break; 214 case 'approx': 215 case '~=': 216 $leaf_filter->_filter = '(' . $attr_name . '~=' . $value . ')'; 217 break; 218 case 'any': 219 case 'present': // alias that may improve user code readability 220 $leaf_filter->_filter = '(' . $attr_name . '=*)'; 221 break; 222 default: 223 return PEAR::raiseError('Net_LDAP2_Filter create error: matching rule "' . $match . '" not known!'); 224 } 225 226 // negate if requested 227 if ($negate_filter) { 228 $leaf_filter = Net_LDAP2_Filter::combine('!', $leaf_filter); 229 } 230 231 return $leaf_filter; 232 } 233 234 /** 235 * Combine two or more filter objects using a logical operator 236 * 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. 241 * 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 244 * 245 * @return Net_LDAP2_Filter|Net_LDAP2_Error 246 * @static 247 */ 248 public static function &combine($log_op, $filters) 249 { 250 if (PEAR::isError($filters)) { 251 return $filters; 252 } 253 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 = '!'; 258 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()); 268 return $err; 269 } else { 270 $filters = array($filter_o); 271 } 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!'); 275 return $err; 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!'); 278 return $err; 279 } 280 } else { 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!'); 282 return $err; 283 } 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!'); 287 return $err; 288 } 289 } else { 290 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: logical operator is not known!'); 291 return $err; 292 } 293 294 $combined_filter = new Net_LDAP2_Filter(); 295 foreach ($filters as $key => $testfilter) { // check for errors 296 if (PEAR::isError($testfilter)) { 297 return $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)) { 302 return $filter_o; 303 } else { 304 $filters[$key] = $filter_o; 305 } 306 } elseif (!$testfilter instanceof Net_LDAP2_Filter) { 307 $err = PEAR::raiseError('Net_LDAP2_Filter combine error: invalid object passed in array $filters!'); 308 return $err; 309 } 310 } 311 312 $combined_filter->_subfilters = $filters; 313 $combined_filter->_match = $log_op; 314 return $combined_filter; 315 } 316 317 /** 318 * Parse FILTER into a Net_LDAP2_Filter object 319 * 320 * This parses an filter string into Net_LDAP2_Filter objects. 321 * 322 * @param string $FILTER The filter string 323 * 324 * @access static 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) 327 */ 328 public static function parse($FILTER) 329 { 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!"); 340 } 341 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. 347 348 // extract logical operator and filter arguments 349 $log_op = substr($matches[1], 0, 1); 350 $remaining_component = substr($matches[1], 1); 351 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 362 // subcomponent 363 for ($curpos = 0; $curpos < strlen($remaining_component); $curpos++) { 364 $cur_char = substr($remaining_component, $curpos, 1); 365 366 // rise/lower bracket level 367 if ($cur_char == '(' && $prev_char != '\\') { 368 $level++; 369 } elseif ($cur_char == ')' && $prev_char != '\\') { 370 $level--; 371 } 372 373 if ($cur_char == '(' && $prev_char == ')' && $level == 1) { 374 array_push($sub_index_pos, $curpos); // mark the position for splitting 375 } 376 $prev_char = $cur_char; 377 } 378 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(); 383 $oldpos = 0; 384 foreach ($sub_index_pos as $s_pos) { 385 $str_part = substr($remaining_component, $oldpos, $s_pos - $oldpos); 386 array_push($subfilters, $str_part); 387 $oldpos = $s_pos; 388 } 389 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!"); 397 } 398 } else { 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!"); 401 } 402 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)) { 408 return $o; 409 } else { 410 array_push($subfilters_o, self::parse($s_s)); 411 } 412 } 413 414 $filter_o = self::combine($log_op, $subfilters_o); 415 return $filter_o; 416 417 } else { 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" 420 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!"); 425 } else { 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"); 429 } else { 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.')'; 437 return $filter_o; 438 } 439 } 440 } 441 } else { 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"); 444 } 445 } 446 447 /** 448 * Get the string representation of this filter 449 * 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. 454 * 455 * @return string|Net_LDAP2_Error 456 */ 457 public function asString() 458 { 459 if ($this->isLeaf()) { 460 $return = $this->_filter; 461 } else { 462 $return = ''; 463 foreach ($this->_subfilters as $filter) { 464 $return = $return.$filter->asString(); 465 } 466 $return = '(' . $this->_match . $return . ')'; 467 } 468 return $return; 469 } 470 471 /** 472 * Alias for perl interface as_string() 473 * 474 * @see asString() 475 * @return string|Net_LDAP2_Error 476 */ 477 public function as_string() 478 { 479 return $this->asString(); 480 } 481 482 /** 483 * Print the text representation of the filter to FH, or the currently selected output handle if FH is not given 484 * 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. 488 * 489 * @param resource $FH (optional) A filehandle resource 490 * 491 * @return true|Net_LDAP2_Error 492 */ 493 public function printMe($FH = false) 494 { 495 if (!is_resource($FH)) { 496 if (PEAR::isError($FH)) { 497 return $FH; 498 } 499 $filter_str = $this->asString(); 500 if (PEAR::isError($filter_str)) { 501 return $filter_str; 502 } else { 503 print($filter_str); 504 } 505 } else { 506 $filter_str = $this->asString(); 507 if (PEAR::isError($filter_str)) { 508 return $filter_str; 509 } else { 510 $res = @fwrite($FH, $this->asString()); 511 if ($res == false) { 512 return PEAR::raiseError("Unable to write filter string to filehandle \$FH!"); 513 } 514 } 515 } 516 return true; 517 } 518 519 /** 520 * This can be used to escape a string to provide a valid LDAP-Filter. 521 * 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) 527 * 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. 531 * 532 * @param string $value Any string who should be escaped 533 * 534 * @static 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 537 */ 538 public static function escape($value) 539 { 540 $return = Net_LDAP2_Util::escape_filter_value(array($value)); 541 return $return[0]; 542 } 543 544 /** 545 * Is this a container or a leaf filter object? 546 * 547 * @access protected 548 * @return boolean 549 */ 550 protected function isLeaf() 551 { 552 if (count($this->_subfilters) > 0) { 553 return false; // Container! 554 } else { 555 return true; // Leaf! 556 } 557 } 558 559 /** 560 * Filter entries using this filter or see if a filter matches 561 * 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 568 */ 569 function matches(&$entries, &$results=array()) { 570 $numOfMatches = 0; 571 572 if (!is_array($entries)) { 573 $all_entries = array(&$entries); 574 } else { 575 $all_entries = &$entries; 576 } 577 578 foreach ($all_entries as $entry) { 579 // look at the current entry and see if filter matches 580 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()) { 585 586 // get partial results from subfilters 587 $partial_results = array(); 588 foreach ($this->_subfilters as $filter) { 589 $partial_results[] = $filter->matches($entry); 590 } 591 592 // evaluate partial results using this filters combination rule 593 switch ($this->_match) { 594 case '!': 595 // result is the neagtive result of the assertion 596 $entry_matched = !$partial_results[0]; 597 break; 598 599 case '&': 600 // all partial results have to be boolean-true 601 $entry_matched = !in_array(false, $partial_results); 602 break; 603 604 case '|': 605 // at least one partial result has to be true 606 $entry_matched = in_array(true, $partial_results); 607 break; 608 } 609 610 } else { 611 // Leaf filter: assert given entry 612 // [TODO]: Could be optimized to avoid preg_match especially with "ends", "begins" etc 613 614 // Translate the LDAP-match to some preg_match expression and evaluate it 615 list($attribute, $match, $assertValue) = $this->getComponents(); 616 switch ($match) { 617 case '=': 618 $regexp = '/^'.str_replace('*', '.*', $assertValue).'$/i'; // not case sensitive unless specified by schema 619 $entry_matched = $entry->pregMatch($regexp, $attribute); 620 break; 621 622 // ------------------------------------- 623 // [TODO]: implement <, >, <=, >= and =~ 624 // ------------------------------------- 625 626 default: 627 $err = PEAR::raiseError("Net_LDAP2_Filter match error: unsupported match rule '$match'!"); 628 return $err; 629 } 630 631 } 632 633 // process filter matching result 634 if ($entry_matched) { 635 $numOfMatches++; 636 $results[] = $entry; 637 } 638 639 } 640 641 return $numOfMatches; 642 } 643 644 645 /** 646 * Retrieve this leaf-filters attribute, match and value component. 647 * 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*"! 653 * 654 * For non-leaf filters this will drop an error. 655 * 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 658 */ 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"); 665 } else { 666 return $parts; 667 } 668 } else { 669 return PEAR::raiseError('Net_LDAP2_Filter getComponents() call is invalid for non-leaf filters!'); 670 } 671 } 672 673 674} 675?> 676