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