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\Entry\Attribute;
15use FreeDSx\Ldap\Entry\Dn;
16use FreeDSx\Ldap\Entry\Entry;
17use FreeDSx\Ldap\Entry\Option;
18use FreeDSx\Ldap\Exception\OperationException;
19use FreeDSx\Ldap\Exception\RuntimeException;
20use FreeDSx\Ldap\LdapClient;
21
22/**
23 * Provides simple helper APIs for retrieving ranged results for an entry attribute.
24 *
25 * @see https://docs.microsoft.com/en-us/windows/desktop/adsi/attribute-range-retrieval
26 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
27 */
28class RangeRetrieval
29{
30    /**
31     * @var LdapClient
32     */
33    protected $client;
34
35    /**
36     * @param LdapClient $client
37     */
38    public function __construct(LdapClient $client)
39    {
40        $this->client = $client;
41    }
42
43    /**
44     * Get a specific ranged attribute by name from an entry. If it does not exist it will return null.
45     *
46     * @param string|Attribute $attribute
47     */
48    public function getRanged(Entry $entry, $attribute): ?Attribute
49    {
50        $attribute = $attribute instanceof Attribute ? new Attribute($attribute->getName()) : new Attribute($attribute);
51
52        foreach ($this->getAllRanged($entry) as $rangedAttribute) {
53            if ($rangedAttribute->equals($attribute)) {
54                return $rangedAttribute;
55            }
56        }
57
58        return null;
59    }
60
61    /**
62     * Get all ranged attributes as an array from a entry.
63     *
64     * @param Entry $entry
65     * @return Attribute[]
66     * @psalm-return list<Attribute>
67     */
68    public function getAllRanged(Entry $entry): array
69    {
70        $ranged = [];
71
72        foreach ($entry->getAttributes() as $attribute) {
73            if (!$attribute->hasOptions()) {
74                continue;
75            }
76            /** @var Option $option */
77            foreach ($attribute->getOptions() as $option) {
78                if ($option->isRange()) {
79                    $ranged[] = $attribute;
80                    break;
81                }
82            }
83        }
84
85        return $ranged;
86    }
87
88    /**
89     * A simple check to determine if an entry contains any ranged attributes. Optionally pass an attribute
90     *
91     * @param Entry $entry
92     * @param Attribute|string|null $attribute
93     * @return bool
94     */
95    public function hasRanged(Entry $entry, $attribute = null): bool
96    {
97        return (bool) ($attribute !== null ? $this->getRanged($entry, $attribute) : $this->getAllRanged($entry));
98    }
99
100    /**
101     * Check if an attribute has more range values that can be queried.
102     *
103     * @param Attribute $attribute
104     * @return bool
105     */
106    public function hasMoreValues(Attribute $attribute): bool
107    {
108        if (($range = $this->getRangeOption($attribute)) === null) {
109            return false;
110        }
111
112        return $range->getHighRange() !== '*';
113    }
114
115    /**
116     * Given a specific Entry/DN and an attribute, get the next set of ranged values available. Optionally pass a third
117     * parameter to control how many values to grab next.
118     *
119     * @param Entry|Dn|string $entry
120     * @param Attribute $attribute
121     * @param string|int $amount
122     * @return Attribute
123     * @throws OperationException
124     */
125    public function getMoreValues($entry, Attribute $attribute, $amount = '*'): Attribute
126    {
127        if (($range = $this->getRangeOption($attribute)) === null || !$this->hasMoreValues($attribute)) {
128            return new Attribute($attribute->getName());
129        }
130        if ($amount !== '*') {
131            $amount = (int) $amount + (int) $range->getHighRange();
132        }
133        $attrReq = new Attribute($attribute->getName());
134        $startAt = (int) $range->getHighRange() + 1;
135        $attrReq->getOptions()->set(Option::fromRange((string) $startAt, (string) $amount));
136        $result = $this->client->readOrFail($entry, [$attrReq]);
137
138        $attrResult = $result->get($attribute->getName());
139        if ($attrResult === null) {
140            throw new RuntimeException(sprintf(
141                'The attribute %s was not returned from LDAP',
142                $attribute->getName()
143            ));
144        }
145        if (($range = $this->getRangeOption($attrResult)) === null) {
146            throw new RuntimeException(sprintf(
147                'No ranged option received for attribute "%s" on "%s".',
148                $attribute->getName(),
149                $result->getDn()->toString()
150            ));
151        }
152
153        return $attrResult;
154    }
155
156    /**
157     * Given a specific entry and attribute, range retrieve all values of the attribute.
158     *
159     * @param Entry|Dn|string $entry
160     * @param string|Attribute $attribute
161     * @return Attribute
162     * @throws OperationException
163     */
164    public function getAllValues($entry, $attribute): Attribute
165    {
166        $attrResult = $attribute instanceof Attribute ? new Attribute($attribute->getName()) : new Attribute($attribute);
167        $attrResult->getOptions()->set(Option::fromRange('0'));
168
169        $entry = $this->client->readOrFail($entry, [$attrResult]);
170        $attribute = $this->getRanged($entry, $attrResult);
171        if ($attribute === null) {
172            throw new RuntimeException(sprintf(
173                'No ranged result received for "%s" on entry "%s".',
174                $attrResult->getName(),
175                $entry->getDn()->toString()
176            ));
177        }
178
179        $attrResult->add(...$attribute->getValues());
180        while ($this->hasMoreValues($attribute)) {
181            $attribute = $this->getMoreValues($entry, $attribute);
182            $attrResult->add(...$attribute->getValues());
183        }
184
185        return $attrResult;
186    }
187
188    /**
189     * @param Attribute $attribute
190     * @return Option|null
191     */
192    protected function getRangeOption(Attribute $attribute): ?Option
193    {
194        /** @var Option $option */
195        foreach ($attribute->getOptions() as $option) {
196            if ($option->isRange()) {
197                return $option;
198            }
199        }
200
201        return null;
202    }
203}
204