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