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-2017 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 */ 43class 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 $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 $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 $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 $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