1<?php
2/**
3 * This file is part of the FreeDSx LDAP package.
4 *
5 * (c) Chad Sikorra <Chad.Sikorra@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11namespace FreeDSx\Ldap\Search;
12
13use FreeDSx\Ldap\Control\Control;
14use FreeDSx\Ldap\Control\PagingControl;
15use FreeDSx\Ldap\Controls;
16use FreeDSx\Ldap\Entry\Entries;
17use FreeDSx\Ldap\Exception\ProtocolException;
18use FreeDSx\Ldap\LdapClient;
19use FreeDSx\Ldap\Operation\Request\SearchRequest;
20use FreeDSx\Ldap\Operation\Response\SearchResponse;
21
22/**
23 * Provides a simple wrapper around paging a search operation.
24 *
25 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
26 */
27class Paging
28{
29    /**
30     * @var PagingControl|null
31     */
32    protected $control;
33
34    /**
35     * @var LdapClient
36     */
37    protected $client;
38
39    /**
40     * @var int
41     */
42    protected $size;
43
44    /**
45     * @var SearchRequest
46     */
47    protected $search;
48
49    /**
50     * @var bool
51     */
52    protected $ended = false;
53
54    /**
55     * @param LdapClient $client
56     * @param SearchRequest $search
57     * @param int $size
58     */
59    public function __construct(LdapClient $client, SearchRequest $search, int $size = 1000)
60    {
61        $this->search = $search;
62        $this->client = $client;
63        $this->size = $size;
64    }
65
66    /**
67     * Start a new paging operation with a search request. This must be called first if you reuse the paging object.
68     *
69     * @param SearchRequest $search
70     * @param int|null $size
71     */
72    public function start(SearchRequest $search, ?int $size = null): void
73    {
74        $this->size = $size ?? $this->size;
75        $this->search = $search;
76        $this->control = null;
77        $this->ended = false;
78    }
79
80    /**
81     * End the paging operation. This can be triggered at any time.
82     *
83     * @return $this
84     */
85    public function end()
86    {
87        $this->send(0);
88        $this->ended = true;
89
90        return $this;
91    }
92
93    /**
94     * Get the next set of entries of results.
95     *
96     * @param int|null $size
97     * @return Entries
98     */
99    public function getEntries(?int $size = null): Entries
100    {
101        return $this->send($size);
102    }
103
104    /**
105     * @return bool
106     */
107    public function hasEntries()
108    {
109        if ($this->ended) {
110            return false;
111        }
112
113        return $this->control === null || !($this->control->getCookie() === '');
114    }
115
116    /**
117     * The size may be set to the server's estimate of the total number of entries in the entire result set. Servers
118     * that cannot provide such an estimate may set this size to zero.
119     *
120     * @return int|null
121     */
122    public function sizeEstimate(): ?int
123    {
124        return ($this->control !== null) ? $this->control->getSize() : null;
125    }
126
127    /**
128     * @param int|null $size
129     * @return Entries
130     * @throws ProtocolException
131     */
132    protected function send(?int $size = null)
133    {
134        $cookie = ($this->control !== null) ? $this->control->getCookie() : '';
135        $message = $this->client->sendAndReceive($this->search, Controls::paging($size ?? $this->size, $cookie));
136        $control = $message->controls()->get(Control::OID_PAGING);
137        if ($control !== null && !$control instanceof PagingControl) {
138            throw new ProtocolException(sprintf(
139                'Expected a paging control, but received: %s.',
140                get_class($control)
141            ));
142        }
143        # OpenLDAP returns no paging control in response to an abandon request. However, other LDAP implementations do;
144        # such as Active Directory. It's not clear from the paging RFC which is correct.
145        if ($control === null && $size !== 0) {
146            throw new ProtocolException('Expected a paging control, but received none.');
147        }
148        $this->control = $control;
149        /** @var SearchResponse $response */
150        $response = $message->getResponse();
151
152        return $response->getEntries();
153    }
154}
155