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