1<?php 2/** 3 * Result set of an LDAP search 4 * 5 * Copyright 2009 Jan Wagner, Benedikt Hallinger 6 * Copyright 2010-2017 Horde LLC (http://www.horde.org/) 7 * 8 * @category Horde 9 * @package Ldap 10 * @author Tarjej Huse <tarjei@bergfald.no> 11 * @author Benedikt Hallinger <beni@php.net> 12 * @author Jan Schneider <jan@horde.org> 13 * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0 14 */ 15class Horde_Ldap_Search implements Iterator 16{ 17 /** 18 * Search result identifier. 19 * 20 * @var resource 21 */ 22 protected $_search; 23 24 /** 25 * LDAP resource link. 26 * 27 * @var resource 28 */ 29 protected $_link; 30 31 /** 32 * Horde_Ldap object. 33 * 34 * A reference of the Horde_Ldap object for passing to Horde_Ldap_Entry. 35 * 36 * @var Horde_Ldap 37 */ 38 protected $_ldap; 39 40 /** 41 * Result entry identifier. 42 * 43 * @var resource 44 */ 45 protected $_entry; 46 47 /** 48 * The errorcode from the search. 49 * 50 * Some errorcodes might be of interest that should not be considered 51 * errors, for example: 52 * - 4: LDAP_SIZELIMIT_EXCEEDED - indicates a huge search. Incomplete 53 * results are returned. If you just want to check if there is 54 * anything returned by the search at all, this could be catched. 55 * - 32: no such object - search here returns a count of 0. 56 * 57 * @var integer 58 */ 59 protected $_errorCode = 0; 60 61 /** 62 * Cache for all entries already fetched from iterator interface. 63 * 64 * @var array 65 */ 66 protected $_iteratorCache = array(); 67 68 /** 69 * Attributes we searched for. 70 * 71 * This variable gets set from the constructor and can be retrieved through 72 * {@link searchedAttributes()}. 73 * 74 * @var array 75 */ 76 protected $_searchedAttrs = array(); 77 78 /** 79 * Cache variable for storing entries fetched internally. 80 * 81 * This currently is only used by {@link pop_entry()}. 82 * 83 * @var array 84 */ 85 protected $_entry_cache = false; 86 87 /** 88 * Constructor. 89 * 90 * @param resource $search Search result identifier. 91 * @param Horde_Ldap|resource $ldap Horde_Ldap object or a LDAP link 92 * resource 93 * @param array $attributes The searched attribute names, 94 * see {@link $_searchedAttrs}. 95 */ 96 public function __construct($search, $ldap, $attributes = array()) 97 { 98 $this->setSearch($search); 99 100 if ($ldap instanceof Horde_Ldap) { 101 $this->_ldap = $ldap; 102 $this->setLink($this->_ldap->getLink()); 103 } else { 104 $this->setLink($ldap); 105 } 106 107 $this->_errorCode = @ldap_errno($this->_link); 108 109 if (is_array($attributes) && !empty($attributes)) { 110 $this->_searchedAttrs = $attributes; 111 } 112 } 113 114 /** 115 * Destructor. 116 */ 117 public function __destruct() 118 { 119 @ldap_free_result($this->_search); 120 } 121 122 /** 123 * Returns all entries from the search result. 124 * 125 * @return array All entries. 126 * @throws Horde_Ldap_Exception 127 */ 128 public function entries() 129 { 130 $entries = array(); 131 while ($entry = $this->shiftEntry()) { 132 $entries[] = $entry; 133 } 134 return $entries; 135 } 136 137 /** 138 * Get the next entry from the search result. 139 * 140 * This will return a valid Horde_Ldap_Entry object or false, so you can 141 * use this method to easily iterate over the entries inside a while loop. 142 * 143 * @return Horde_Ldap_Entry|false Reference to Horde_Ldap_Entry object or 144 * false if no more entries exist. 145 * @throws Horde_Ldap_Exception 146 */ 147 public function shiftEntry() 148 { 149 if (is_null($this->_entry)) { 150 if (!$this->_entry = @ldap_first_entry($this->_link, $this->_search)) { 151 return false; 152 } 153 $entry = Horde_Ldap_Entry::createConnected($this->_ldap, $this->_entry); 154 } else { 155 if (!$this->_entry = @ldap_next_entry($this->_link, $this->_entry)) { 156 return false; 157 } 158 $entry = Horde_Ldap_Entry::createConnected($this->_ldap, $this->_entry); 159 } 160 161 return $entry; 162 } 163 164 /** 165 * Retrieve the next entry in the search result, but starting from last 166 * entry. 167 * 168 * This is the opposite to {@link shiftEntry()} and is also very useful to 169 * be used inside a while loop. 170 * 171 * @return Horde_Ldap_Entry|false 172 * @throws Horde_Ldap_Exception 173 */ 174 public function popEntry() 175 { 176 if (false === $this->_entry_cache) { 177 // Fetch entries into cache if not done so far. 178 $this->_entry_cache = $this->entries(); 179 } 180 181 return count($this->_entry_cache) ? array_pop($this->_entry_cache) : false; 182 } 183 184 /** 185 * Return entries sorted as array. 186 * 187 * This returns a array with sorted entries and the values. Sorting is done 188 * with PHPs {@link array_multisort()}. 189 * 190 * This method relies on {@link asArray()} to fetch the raw data of the 191 * entries. 192 * 193 * Please note that attribute names are case sensitive! 194 * 195 * Usage example: 196 * <code> 197 * // To sort entries first by location, then by surname, but descending: 198 * $entries = $search->sortedAsArray(array('locality', 'sn'), SORT_DESC); 199 * </code> 200 * 201 * @todo what about server side sorting as specified in 202 * http://www.ietf.org/rfc/rfc2891.txt? 203 * @todo Nuke evil eval(). 204 * 205 * @param array $attrs Attribute names as sort criteria. 206 * @param integer $order Ordering direction, either constant SORT_ASC or 207 * SORT_DESC 208 * 209 * @return array Sorted entries. 210 * @throws Horde_Ldap_Exception 211 */ 212 public function sortedAsArray(array $attrs = array('cn'), $order = SORT_ASC) 213 { 214 /* New code: complete "client side" sorting */ 215 // First some parameterchecks. 216 if ($order != SORT_ASC && $order != SORT_DESC) { 217 throw new Horde_Ldap_Exception('Sorting failed: sorting direction not understood! (neither constant SORT_ASC nor SORT_DESC)'); 218 } 219 220 // Fetch the entries data. 221 $entries = $this->asArray(); 222 223 // Now sort each entries attribute values. 224 // This is neccessary because later we can only sort by one value, so 225 // we need the highest or lowest attribute now, depending on the 226 // selected ordering for that specific attribute. 227 foreach ($entries as $dn => $entry) { 228 foreach ($entry as $attr_name => $attr_values) { 229 sort($entries[$dn][$attr_name]); 230 if ($order == SORT_DESC) { 231 array_reverse($entries[$dn][$attr_name]); 232 } 233 } 234 } 235 236 // Reformat entries array for later use with 237 // array_multisort(). $to_sort will be a numeric array similar to 238 // ldap_get_entries(). 239 $to_sort = array(); 240 foreach ($entries as $dn => $entry_attr) { 241 $row = array('dn' => $dn); 242 foreach ($entry_attr as $attr_name => $attr_values) { 243 $row[$attr_name] = $attr_values; 244 } 245 $to_sort[] = $row; 246 } 247 248 // Build columns for array_multisort(). Each requested attribute is one 249 // row. 250 $columns = array(); 251 foreach ($attrs as $attr_name) { 252 foreach ($to_sort as $key => $row) { 253 $columns[$attr_name][$key] =& $to_sort[$key][$attr_name][0]; 254 } 255 } 256 257 // Sort the colums with array_multisort() if there is something to sort 258 // and if we have requested sort columns. 259 if (!empty($to_sort) && !empty($columns)) { 260 $sort_params = ''; 261 foreach ($attrs as $attr_name) { 262 $sort_params .= '$columns[\'' . $attr_name . '\'], ' . $order . ', '; 263 } 264 eval("array_multisort($sort_params \$to_sort);"); 265 } 266 267 return $to_sort; 268 } 269 270 /** 271 * Returns entries sorted as objects. 272 * 273 * This returns a array with sorted Horde_Ldap_Entry objects. The sorting 274 * is actually done with {@link sortedAsArray()}. 275 * 276 * Please note that attribute names are case sensitive! 277 * 278 * Also note that it is (depending on server capabilities) possible to let 279 * the server sort your results. This happens through search controls and 280 * is described in detail at {@link http://www.ietf.org/rfc/rfc2891.txt} 281 * 282 * Usage example: 283 * <code> 284 * // To sort entries first by location, then by surname, but descending: 285 * $entries = $search->sorted(array('locality', 'sn'), SORT_DESC); 286 * </code> 287 * 288 * @todo Entry object construction could be faster. Maybe we could use one 289 * of the factories instead of fetching the entry again. 290 * 291 * @param array $attrs Attribute names as sort criteria. 292 * @param integer $order Ordering direction, either constant SORT_ASC or 293 * SORT_DESC 294 * 295 * @return array Sorted entries. 296 * @throws Horde_Ldap_Exception 297 */ 298 public function sorted($attrs = array('cn'), $order = SORT_ASC) 299 { 300 $return = array(); 301 $sorted = $this->sortedAsArray($attrs, $order); 302 foreach ($sorted as $row) { 303 $entry = $this->_ldap->getEntry($row['dn'], $this->searchedAttributes()); 304 $return[] = $entry; 305 } 306 return $return; 307 } 308 309 /** 310 * Returns entries as array. 311 * 312 * The first array level contains all found entries where the keys are the 313 * DNs of the entries. The second level arrays contian the entries 314 * attributes such that the keys is the lowercased name of the attribute 315 * and the values are stored in another indexed array. Note that the 316 * attribute values are stored in an array even if there is no or just one 317 * value. 318 * 319 * The array has the following structure: 320 * <code> 321 * array( 322 * 'cn=foo,dc=example,dc=com' => array( 323 * 'sn' => array('foo'), 324 * 'multival' => array('val1', 'val2', 'valN')), 325 * 'cn=bar,dc=example,dc=com' => array( 326 * 'sn' => array('bar'), 327 * 'multival' => array('val1', 'valN'))) 328 * </code> 329 * 330 * @return array Associative result array as described above. 331 * @throws Horde_Ldap_Exception 332 */ 333 public function asArray() 334 { 335 $return = array(); 336 $entries = $this->entries(); 337 foreach ($entries as $entry) { 338 $attrs = array(); 339 $entry_attributes = $entry->attributes(); 340 foreach ($entry_attributes as $attr_name) { 341 $attr_values = $entry->getValue($attr_name, 'all'); 342 if (!is_array($attr_values)) { 343 $attr_values = array($attr_values); 344 } 345 $attrs[$attr_name] = $attr_values; 346 } 347 $return[$entry->dn()] = $attrs; 348 } 349 return $return; 350 } 351 352 /** 353 * Sets the search objects resource link 354 * 355 * @param resource $search Search result identifier. 356 */ 357 public function setSearch($search) 358 { 359 $this->_search = $search; 360 } 361 362 /** 363 * Sets the LDAP resource link. 364 * 365 * @param resource $link LDAP link identifier. 366 */ 367 public function setLink($link) 368 { 369 $this->_link = $link; 370 } 371 372 /** 373 * Returns the number of entries in the search result. 374 * 375 * @return integer Number of found entries. 376 */ 377 public function count() 378 { 379 // This catches the situation where OL returned errno 32 = no such 380 // object! 381 if (!$this->_search) { 382 return 0; 383 } 384 return @ldap_count_entries($this->_link, $this->_search); 385 } 386 387 /** 388 * Returns the errorcode from the search. 389 * 390 * @return integer The LDAP error number. 391 */ 392 public function getErrorCode() 393 { 394 return $this->_errorCode; 395 } 396 397 /** 398 * Returns the attribute names this search selected. 399 * 400 * @see $_searchedAttrs 401 * 402 * @return array 403 */ 404 protected function searchedAttributes() 405 { 406 return $this->_searchedAttrs; 407 } 408 409 /** 410 * Returns wheter this search exceeded a sizelimit. 411 * 412 * @return boolean True if the size limit was exceeded. 413 */ 414 public function sizeLimitExceeded() 415 { 416 return $this->getErrorCode() == 4; 417 } 418 419 /* SPL Iterator interface methods. This interface allows to use 420 * Horde_Ldap_Search objects directly inside a foreach loop. */ 421 422 /** 423 * SPL Iterator interface: Returns the current element. 424 * 425 * The SPL Iterator interface allows you to fetch entries inside 426 * a foreach() loop: <code>foreach ($search as $dn => $entry) { ...</code> 427 * 428 * Of course, you may call {@link current()}, {@link key()}, {@link next()}, 429 * {@link rewind()} and {@link valid()} yourself. 430 * 431 * If the search throwed an error, it returns false. False is also 432 * returned, if the end is reached. 433 * 434 * In case no call to next() was made, we will issue one, thus returning 435 * the first entry. 436 * 437 * @return Horde_Ldap_Entry|false 438 * @throws Horde_Ldap_Exception 439 */ 440 public function current() 441 { 442 if (count($this->_iteratorCache) == 0) { 443 $this->next(); 444 reset($this->_iteratorCache); 445 } 446 $entry = current($this->_iteratorCache); 447 return $entry instanceof Horde_Ldap_Entry ? $entry : false; 448 } 449 450 /** 451 * SPL Iterator interface: Returns the identifying key (DN) of the current 452 * entry. 453 * 454 * @see current() 455 * @return string|false DN of the current entry; false in case no entry is 456 * returned by current(). 457 */ 458 public function key() 459 { 460 $entry = $this->current(); 461 return $entry instanceof Horde_Ldap_Entry ? $entry->dn() :false; 462 } 463 464 /** 465 * SPL Iterator interface: Moves forward to next entry. 466 * 467 * After a call to {@link next()}, {@link current()} will return the next 468 * entry in the result set. 469 * 470 * @see current() 471 * @throws Horde_Ldap_Exception 472 */ 473 public function next() 474 { 475 // Fetch next entry. If we have no entries anymore, we add false (which 476 // is returned by shiftEntry()) so current() will complain. 477 if (count($this->_iteratorCache) - 1 <= $this->count()) { 478 $this->_iteratorCache[] = $this->shiftEntry(); 479 } 480 481 // Move array pointer to current element. Even if we have added all 482 // entries, this will ensure proper operation in case we rewind(). 483 next($this->_iteratorCache); 484 } 485 486 /** 487 * SPL Iterator interface: Checks if there is a current element after calls 488 * to {@link rewind()} or {@link next()}. 489 * 490 * Used to check if we've iterated to the end of the collection. 491 * 492 * @see current() 493 * @return boolean False if there's nothing more to iterate over. 494 */ 495 public function valid() 496 { 497 return $this->current() instanceof Horde_Ldap_Entry; 498 } 499 500 /** 501 * SPL Iterator interface: Rewinds the Iterator to the first element. 502 * 503 * After rewinding, {@link current()} will return the first entry in the 504 * result set. 505 * 506 * @see current() 507 */ 508 public function rewind() 509 { 510 reset($this->_iteratorCache); 511 } 512} 513