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: