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