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\Sorting\SortingControl;
15use FreeDSx\Ldap\Control\Vlv\VlvControl;
16use FreeDSx\Ldap\Control\Vlv\VlvResponseControl;
17use FreeDSx\Ldap\Controls;
18use FreeDSx\Ldap\Entry\Entries;
19use FreeDSx\Ldap\Exception\ProtocolException;
20use FreeDSx\Ldap\LdapClient;
21use FreeDSx\Ldap\Operation\Request\SearchRequest;
22use FreeDSx\Ldap\Operation\Response\SearchResponse;
23use FreeDSx\Ldap\Search\Filter\GreaterThanOrEqualFilter;
24
25/**
26 * Provides a simple wrapper around a VLV (Virtual List View) search operation.
27 *
28 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
29 */
30class Vlv
31{
32    /**
33     * @var LdapClient
34     */
35    protected $client;
36
37    /**
38     * @var SearchRequest
39     */
40    protected $search;
41
42    /**
43     * @var VlvResponseControl|null
44     */
45    protected $control;
46
47    /**
48     * @var int
49     */
50    protected $before;
51
52    /**
53     * @var int
54     */
55    protected $after;
56
57    /**
58     * @var int
59     */
60    protected $offset = 1;
61
62    /**
63     * @var GreaterThanOrEqualFilter
64     */
65    protected $filter;
66
67    /**
68     * @var SortingControl
69     */
70    protected $sort;
71
72    /**
73     * @var bool
74     */
75    protected $asPercentage = false;
76
77    /**
78     * @param LdapClient $client
79     * @param SearchRequest $search
80     * @param SortingControl|\FreeDSx\Ldap\Control\Sorting\SortKey|string $sort
81     * @param int $before
82     * @param int $after
83     */
84    public function __construct(LdapClient $client, SearchRequest $search, $sort, int $after = 100, int $before = 0)
85    {
86        $this->client = $client;
87        $this->search = $search;
88        $this->sort = $sort instanceof SortingControl ? $sort : Controls::sort($sort);
89        $this->before = $before;
90        $this->after = $after;
91    }
92
93    /**
94     * As a percentage the moveTo, moveForward, moveBackward, and position methods work with numbers 0 - 100 and should
95     * be interpreted as percentages.
96     *
97     * @param bool $asPercentage
98     * @return $this
99     */
100    public function asPercentage(bool $asPercentage = true)
101    {
102        $this->asPercentage = $asPercentage;
103
104        return $this;
105    }
106
107    /**
108     * Request to start at a specific offset/percentage of entries.
109     *
110     * @param int $offset
111     * @return $this
112     */
113    public function startAt(int $offset)
114    {
115        $this->offset = $offset;
116
117        return $this;
118    }
119
120    /**
121     * Move backward the specified number or percentage from the current position.
122     *
123     * @param int $size
124     * @return $this
125     */
126    public function moveBackward(int $size)
127    {
128        $this->offset = ($this->offset - $size < 0) ? 0 : $this->offset - $size;
129
130        return $this;
131    }
132
133    /**
134     * Move forward the specified number or percentage from the current position.
135     *
136     * @param int $size
137     * @return $this
138     */
139    public function moveForward(int $size)
140    {
141        $this->offset = ($this->asPercentage && ($this->offset + $size) > 100) ? 100 : $this->offset + $size;
142
143        return $this;
144    }
145
146    /**
147     * Moves the starting entry of the list to a specific position/percentage of the total list. An alias for startAt().
148     *
149     * @param int $position
150     * @return Vlv
151     */
152    public function moveTo(int $position)
153    {
154        return $this->startAt($position);
155    }
156
157    /**
158     * Retrieve the following number of entries after the position specified.
159     *
160     * @param int $after
161     * @return $this
162     */
163    public function afterPosition(int $after)
164    {
165        $this->after = $after;
166
167        return $this;
168    }
169
170    /**
171     * Retrieve the following number of entries before the position specified.
172     *
173     * @param int $before
174     * @return $this
175     */
176    public function beforePosition(int $before)
177    {
178        $this->before = $before;
179
180        return $this;
181    }
182
183    /**
184     * Get the servers entry offset of the current list.
185     *
186     * @return int|null
187     */
188    public function listOffset(): ?int
189    {
190        return ($this->control !== null) ? $this->control->getOffset() : null;
191    }
192
193    /**
194     * Get the severs estimate, from the last request, that indicates how many entries are in the list.
195     *
196     * @return int|null
197     */
198    public function listSize(): ?int
199    {
200        return ($this->control !== null) ? $this->control->getCount() : null;
201    }
202
203    /**
204     * Get the current position in the list. When as percentage was specified this will be expressed as a percentage.
205     * Use listOffset for a specific entry offset position.
206     *
207     * @return int|null
208     */
209    public function position(): ?int
210    {
211        $control = $this->control;
212        $pos = $control === null ? null : $control->getOffset();
213        if ($control === null || $pos === null) {
214            return null;
215        }
216
217        if ($this->asPercentage) {
218            return (int) round($pos / ((int) $control->getCount() / 100));
219        } else {
220            return $control->getOffset();
221        }
222    }
223
224    /**
225     * Whether or not we are at the end of the list.
226     *
227     * @return bool
228     */
229    public function isAtEndOfList(): bool
230    {
231        if ($this->control === null) {
232            return false;
233        }
234
235        $control = $this->control;
236        if ((((int) $control->getOffset() + $this->after) >= (int) $control->getCount())) {
237            return true;
238        }
239
240        return $control->getOffset() === $control->getCount();
241    }
242
243    /**
244     * Whether or not we are currently at the start of the list.
245     *
246     * @return bool
247     */
248    public function isAtStartOfList(): bool
249    {
250        if ($this->control === null) {
251            return false;
252        }
253        $control = $this->control;
254        if ($this->before !== 0 && ((int) $control->getOffset() - $this->before) <= 1) {
255            return true;
256        }
257
258        return $control->getOffset() === 1;
259    }
260
261    /**
262     * @return Entries
263     * @throws ProtocolException
264     */
265    public function getEntries(): Entries
266    {
267        return $this->send();
268    }
269
270    /**
271     * @throws ProtocolException
272     */
273    protected function send(): Entries
274    {
275        $contextId = ($this->control !== null) ? $this->control->getContextId() : null;
276        $message = $this->client->sendAndReceive($this->search, $this->createVlvControl($contextId), $this->sort);
277        $control = $message->controls()->get(Control::OID_VLV_RESPONSE);
278        if ($control === null || !$control instanceof VlvResponseControl) {
279            throw new ProtocolException('Expected a VLV response control, but received none.');
280        }
281        $this->control = $control;
282        /** @var SearchResponse $response */
283        $response = $message->getResponse();
284
285        return $response->getEntries();
286    }
287
288    /**
289     * @return VlvControl
290     */
291    protected function createVlvControl(?string $contextId): VlvControl
292    {
293        if ($this->filter !== null) {
294            return Controls::vlvFilter($this->before, $this->after, $this->filter, $contextId);
295        }
296        # An offset of 1 and a content size of zero starts from the beginning entry (server uses its assumed count).
297        $count = ($this->control !== null) ? (int) $this->control->getCount() : 0;
298
299        # In percentage mode start off with an assumed count of 100, as the formula the server uses should give us the
300        # expected result
301        if ($this->control === null && $this->asPercentage) {
302            $count = 100;
303        }
304
305        $offset = $this->offset;
306        # Final checks to make sure if we are using a percentage that valid values are used.
307        if ($this->asPercentage && $this->offset > 100) {
308            $offset = 100;
309        } elseif ($this->asPercentage && $this->offset < 0) {
310            $offset = 0;
311        }
312
313        if ($this->asPercentage && $this->control !== null) {
314            $offset = (int) round(((int) $this->control->getCount() / 100) * $offset);
315        }
316
317        return Controls::vlv($this->before, $this->after, $offset, $count, $contextId);
318    }
319}
320