Overview

Packages

  • Ldap

Classes

  • Horde_Ldap
  • Horde_Ldap_Entry
  • Horde_Ldap_Exception
  • Horde_Ldap_Filter
  • Horde_Ldap_Ldif
  • Horde_Ldap_RootDse
  • Horde_Ldap_Schema
  • Horde_Ldap_Search
  • Horde_Ldap_Util
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Result set of an LDAP search
  4:  *
  5:  * Copyright 2009 Jan Wagner, Benedikt Hallinger
  6:  * Copyright 2010-2012 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:  */
 15: class 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:         /* Old Code, suitable and fast for single valued sorting. This code
215:          * should be used if we know that single valued sorting is desired, but
216:          * we need some method to get that knowledge... */
217:         /*
218:         $attrs = array_reverse($attrs);
219:         foreach ($attrs as $attribute) {
220:             if (!ldap_sort($this->_link, $this->_search, $attribute)) {
221:                 throw new Horde_Ldap_Exception('Sorting failed for attribute ' . $attribute);
222:             }
223:         }
224: 
225:         $results = ldap_get_entries($this->_link, $this->_search);
226: 
227:         unset($results['count']);
228:         if ($order) {
229:             return array_reverse($results);
230:         }
231:         return $results;
232:         */
233: 
234:         /* New code: complete "client side" sorting */
235:         // First some parameterchecks.
236:         if ($order != SORT_ASC && $order != SORT_DESC) {
237:             throw new Horde_Ldap_Exception('Sorting failed: sorting direction not understood! (neither constant SORT_ASC nor SORT_DESC)');
238:         }
239: 
240:         // Fetch the entries data.
241:         $entries = $this->asArray();
242: 
243:         // Now sort each entries attribute values.
244:         // This is neccessary because later we can only sort by one value, so
245:         // we need the highest or lowest attribute now, depending on the
246:         // selected ordering for that specific attribute.
247:         foreach ($entries as $dn => $entry) {
248:             foreach ($entry as $attr_name => $attr_values) {
249:                 sort($entries[$dn][$attr_name]);
250:                 if ($order == SORT_DESC) {
251:                     array_reverse($entries[$dn][$attr_name]);
252:                 }
253:             }
254:         }
255: 
256:         // Reformat entries array for later use with
257:         // array_multisort(). $to_sort will be a numeric array similar to
258:         // ldap_get_entries().
259:         $to_sort = array();
260:         foreach ($entries as $dn => $entry_attr) {
261:             $row = array('dn' => $dn);
262:             foreach ($entry_attr as $attr_name => $attr_values) {
263:                 $row[$attr_name] = $attr_values;
264:             }
265:             $to_sort[] = $row;
266:         }
267: 
268:         // Build columns for array_multisort(). Each requested attribute is one
269:         // row.
270:         $columns = array();
271:         foreach ($attrs as $attr_name) {
272:             foreach ($to_sort as $key => $row) {
273:                 $columns[$attr_name][$key] =& $to_sort[$key][$attr_name][0];
274:             }
275:         }
276: 
277:         // Sort the colums with array_multisort() if there is something to sort
278:         // and if we have requested sort columns.
279:         if (!empty($to_sort) && !empty($columns)) {
280:             $sort_params = '';
281:             foreach ($attrs as $attr_name) {
282:                 $sort_params .= '$columns[\'' . $attr_name . '\'], ' . $order . ', ';
283:             }
284:             eval("array_multisort($sort_params \$to_sort);");
285:         }
286: 
287:         return $to_sort;
288:     }
289: 
290:     /**
291:      * Returns entries sorted as objects.
292:      *
293:      * This returns a array with sorted Horde_Ldap_Entry objects. The sorting
294:      * is actually done with {@link sortedAsArray()}.
295:      *
296:      * Please note that attribute names are case sensitive!
297:      *
298:      * Also note that it is (depending on server capabilities) possible to let
299:      * the server sort your results. This happens through search controls and
300:      * is described in detail at {@link http://www.ietf.org/rfc/rfc2891.txt}
301:      *
302:      * Usage example:
303:      * <code>
304:      *   // To sort entries first by location, then by surname, but descending:
305:      *   $entries = $search->sorted(array('locality', 'sn'), SORT_DESC);
306:      * </code>
307:      *
308:      * @todo Entry object construction could be faster. Maybe we could use one
309:      *       of the factories instead of fetching the entry again.
310:      *
311:      * @param array   $attrs Attribute names as sort criteria.
312:      * @param integer $order Ordering direction, either constant SORT_ASC or
313:      *                       SORT_DESC
314:      *
315:      * @return array Sorted entries.
316:      * @throws Horde_Ldap_Exception
317:      */
318:     public function sorted($attrs = array('cn'), $order = SORT_ASC)
319:     {
320:         $return = array();
321:         $sorted = $this->sortedAsArray($attrs, $order);
322:         foreach ($sorted as $row) {
323:             $entry = $this->_ldap->getEntry($row['dn'], $this->searchedAttributes());
324:             array_push($return, $entry);
325:         }
326:         return $return;
327:     }
328: 
329:     /**
330:      * Returns entries as array.
331:      *
332:      * The first array level contains all found entries where the keys are the
333:      * DNs of the entries. The second level arrays contian the entries
334:      * attributes such that the keys is the lowercased name of the attribute
335:      * and the values are stored in another indexed array. Note that the
336:      * attribute values are stored in an array even if there is no or just one
337:      * value.
338:      *
339:      * The array has the following structure:
340:      * <code>
341:      * array(
342:      *     'cn=foo,dc=example,dc=com' => array(
343:      *         'sn'       => array('foo'),
344:      *         'multival' => array('val1', 'val2', 'valN')),
345:      *     'cn=bar,dc=example,dc=com' => array(
346:      *         'sn'       => array('bar'),
347:      *         'multival' => array('val1', 'valN')))
348:      * </code>
349:      *
350:      * @return array Associative result array as described above.
351:      * @throws Horde_Ldap_Exception
352:      */
353:     public function asArray()
354:     {
355:         $return  = array();
356:         $entries = $this->entries();
357:         foreach ($entries as $entry) {
358:             $attrs            = array();
359:             $entry_attributes = $entry->attributes();
360:             foreach ($entry_attributes as $attr_name) {
361:                 $attr_values = $entry->getValue($attr_name, 'all');
362:                 if (!is_array($attr_values)) {
363:                     $attr_values = array($attr_values);
364:                 }
365:                 $attrs[$attr_name] = $attr_values;
366:             }
367:             $return[$entry->dn()] = $attrs;
368:         }
369:         return $return;
370:     }
371: 
372:     /**
373:      * Sets the search objects resource link
374:      *
375:      * @param resource $search Search result identifier.
376:      */
377:     public function setSearch($search)
378:     {
379:         $this->_search = $search;
380:     }
381: 
382:     /**
383:      * Sets the LDAP resource link.
384:      *
385:      * @param resource $link LDAP link identifier.
386:      */
387:     public function setLink($link)
388:     {
389:         $this->_link = $link;
390:     }
391: 
392:     /**
393:      * Returns the number of entries in the search result.
394:      *
395:      * @return integer Number of found entries.
396:      */
397:     public function count()
398:     {
399:         // This catches the situation where OL returned errno 32 = no such
400:         // object!
401:         if (!$this->_search) {
402:             return 0;
403:         }
404:         return @ldap_count_entries($this->_link, $this->_search);
405:     }
406: 
407:     /**
408:      * Returns the errorcode from the search.
409:      *
410:      * @return integer The LDAP error number.
411:      */
412:     public function getErrorCode()
413:     {
414:         return $this->_errorCode;
415:     }
416: 
417:     /**
418:      * Returns the attribute names this search selected.
419:      *
420:      * @see $_searchedAttrs
421:      *
422:      * @return array
423:      */
424:     protected function searchedAttributes()
425:     {
426:         return $this->_searchedAttrs;
427:     }
428: 
429:     /**
430:      * Returns wheter this search exceeded a sizelimit.
431:      *
432:      * @return boolean  True if the size limit was exceeded.
433:      */
434:     public function sizeLimitExceeded()
435:     {
436:         return $this->getErrorCode() == 4;
437:     }
438: 
439:     /* SPL Iterator interface methods. This interface allows to use
440:      * Horde_Ldap_Search objects directly inside a foreach loop. */
441: 
442:     /**
443:      * SPL Iterator interface: Returns the current element.
444:      *
445:      * The SPL Iterator interface allows you to fetch entries inside
446:      * a foreach() loop: <code>foreach ($search as $dn => $entry) { ...</code>
447:      *
448:      * Of course, you may call {@link current()}, {@link key()}, {@link next()},
449:      * {@link rewind()} and {@link valid()} yourself.
450:      *
451:      * If the search throwed an error, it returns false. False is also
452:      * returned, if the end is reached.
453:      *
454:      * In case no call to next() was made, we will issue one, thus returning
455:      * the first entry.
456:      *
457:      * @return Horde_Ldap_Entry|false
458:      * @throws Horde_Ldap_Exception
459:      */
460:     public function current()
461:     {
462:         if (count($this->_iteratorCache) == 0) {
463:             $this->next();
464:             reset($this->_iteratorCache);
465:         }
466:         $entry = current($this->_iteratorCache);
467:         return $entry instanceof Horde_Ldap_Entry ? $entry : false;
468:     }
469: 
470:     /**
471:      * SPL Iterator interface: Returns the identifying key (DN) of the current
472:      * entry.
473:      *
474:      * @see current()
475:      * @return string|false DN of the current entry; false in case no entry is
476:      *                      returned by current().
477:      */
478:     public function key()
479:     {
480:         $entry = $this->current();
481:         return $entry instanceof Horde_Ldap_Entry ? $entry->dn() :false;
482:     }
483: 
484:     /**
485:      * SPL Iterator interface: Moves forward to next entry.
486:      *
487:      * After a call to {@link next()}, {@link current()} will return the next
488:      * entry in the result set.
489:      *
490:      * @see current()
491:      * @throws Horde_Ldap_Exception
492:      */
493:     public function next()
494:     {
495:         // Fetch next entry. If we have no entries anymore, we add false (which
496:         // is returned by shiftEntry()) so current() will complain.
497:         if (count($this->_iteratorCache) - 1 <= $this->count()) {
498:             $this->_iteratorCache[] = $this->shiftEntry();
499:         }
500: 
501:         // Move array pointer to current element.  Even if we have added all
502:         // entries, this will ensure proper operation in case we rewind().
503:         next($this->_iteratorCache);
504:     }
505: 
506:     /**
507:      * SPL Iterator interface: Checks if there is a current element after calls
508:      * to {@link rewind()} or {@link next()}.
509:      *
510:      * Used to check if we've iterated to the end of the collection.
511:      *
512:      * @see current()
513:      * @return boolean False if there's nothing more to iterate over.
514:      */
515:     public function valid()
516:     {
517:         return $this->current() instanceof Horde_Ldap_Entry;
518:     }
519: 
520:     /**
521:      * SPL Iterator interface: Rewinds the Iterator to the first element.
522:      *
523:      * After rewinding, {@link current()} will return the first entry in the
524:      * result set.
525:      *
526:      * @see current()
527:      */
528:     public function rewind()
529:     {
530:         reset($this->_iteratorCache);
531:     }
532: }
533: 
API documentation generated by ApiGen