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\Operation\Request;
13
14use FreeDSx\Asn1\Asn1;
15use FreeDSx\Asn1\Type\AbstractType;
16use FreeDSx\Asn1\Type\BooleanType;
17use FreeDSx\Asn1\Type\EnumeratedType;
18use FreeDSx\Asn1\Type\IntegerType;
19use FreeDSx\Asn1\Type\OctetStringType;
20use FreeDSx\Asn1\Type\SequenceType;
21use FreeDSx\Ldap\Entry\Attribute;
22use FreeDSx\Ldap\Entry\Dn;
23use FreeDSx\Ldap\Exception\ProtocolException;
24use FreeDSx\Ldap\Exception\RuntimeException;
25use FreeDSx\Ldap\Protocol\Factory\FilterFactory;
26use FreeDSx\Ldap\Search\Filter\FilterInterface;
27use function array_map;
28
29/**
30 * A Search Request. RFC 4511, 4.5.1.
31 *
32 * SearchRequest ::= [APPLICATION 3] SEQUENCE {
33 *     baseObject      LDAPDN,
34 *     scope           ENUMERATED {
35 *         baseObject              (0),
36 *         singleLevel             (1),
37 *         wholeSubtree            (2),
38 *         ...  },
39 *     derefAliases    ENUMERATED {
40 *         neverDerefAliases       (0),
41 *         derefInSearching        (1),
42 *         derefFindingBaseObj     (2),
43 *         derefAlways             (3) },
44 *     sizeLimit       INTEGER (0 ..  maxInt),
45 *     timeLimit       INTEGER (0 ..  maxInt),
46 *     typesOnly       BOOLEAN,
47 *     filter          Filter,
48 *     attributes      AttributeSelection }
49 *
50 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
51 */
52class SearchRequest implements RequestInterface
53{
54    /**
55     * Searches a scope of a single object (IE. a specific DN)
56     */
57    public const SCOPE_BASE_OBJECT = 0;
58
59    /**
60     * Searches one level under a specific DN (ie. like a non-recursive directory listing).
61     */
62    public const SCOPE_SINGLE_LEVEL = 1;
63
64    /**
65     * Searches a complete subtree under a DN (ie. like a recursive directory listing).
66     */
67    public const SCOPE_WHOLE_SUBTREE = 2;
68
69    /**
70     * Never dereference aliases.
71     */
72    public const DEREF_NEVER = 0;
73
74    public const DEREF_IN_SEARCHING = 1;
75
76    /**
77     * Dereference aliases when finding the base object only.
78     */
79    public const DEREF_FINDING_BASE_OBJECT = 2;
80
81    /**
82     * Always dereference aliases.
83     */
84    public const DEREF_ALWAYS = 3;
85
86    protected const APP_TAG = 3;
87
88    /**
89     * @var Dn|null
90     */
91    protected $baseDn;
92
93    /**
94     * @var int
95     */
96    protected $scope = self::SCOPE_WHOLE_SUBTREE;
97
98    /**
99     * @var int
100     */
101    protected $derefAliases = self::DEREF_NEVER;
102
103    /**
104     * @var int
105     */
106    protected $sizeLimit = 0;
107
108    /**
109     * @var int
110     */
111    protected $timeLimit = 0;
112
113    /**
114     * @var bool
115     */
116    protected $attributesOnly = false;
117
118    /**
119     * @var FilterInterface
120     */
121    protected $filter;
122
123    /**
124     * @var Attribute[]
125     */
126    protected $attributes = [];
127
128    /**
129     * @param FilterInterface $filter
130     * @param string|Attribute ...$attributes
131     */
132    public function __construct(FilterInterface $filter, ...$attributes)
133    {
134        $this->filter = $filter;
135        $this->setAttributes(...$attributes);
136    }
137
138    /**
139     * Alias to setAttributes. Convenience for a more fluent method call.
140     *
141     * @param string|Attribute ...$attributes
142     * @return SearchRequest
143     */
144    public function select(...$attributes)
145    {
146        return $this->setAttributes(...$attributes);
147    }
148
149    /**
150     * Alias to setBaseDn. Convenience for a more fluent method call.
151     *
152     * @param string|Dn|null $dn
153     * @return SearchRequest
154     */
155    public function base($dn)
156    {
157        return $this->setBaseDn($dn);
158    }
159
160    /**
161     * Set the search scope for all children underneath the base DN.
162     *
163     * @return $this
164     */
165    public function useSubtreeScope()
166    {
167        $this->scope = self::SCOPE_WHOLE_SUBTREE;
168
169        return $this;
170    }
171
172    /**
173     * Set the search scope to the base DN object only.
174     *
175     * @return $this
176     */
177    public function useBaseScope()
178    {
179        $this->scope = self::SCOPE_BASE_OBJECT;
180
181        return $this;
182    }
183
184    /**
185     * Set the search scope to a single level listing from the base DN.
186     *
187     * @return $this
188     */
189    public function useSingleLevelScope()
190    {
191        $this->scope = self::SCOPE_SINGLE_LEVEL;
192
193        return $this;
194    }
195
196    /**
197     * Alias to setSizeLimit. Convenience for a more fluent method call.
198     *
199     * @param int $size
200     * @return SearchRequest
201     */
202    public function sizeLimit(int $size)
203    {
204        return $this->setSizeLimit($size);
205    }
206
207    /**
208     * Alias to setTimeLimit. Convenience for a more fluent method call.
209     *
210     * @param int $time
211     * @return SearchRequest
212     */
213    public function timeLimit($time)
214    {
215        return $this->setTimeLimit($time);
216    }
217
218    /**
219     * @return Attribute[]
220     */
221    public function getAttributes(): array
222    {
223        return $this->attributes;
224    }
225
226    /**
227     * @param string|Attribute ...$attributes
228     * @return $this
229     */
230    public function setAttributes(...$attributes)
231    {
232        $attr = [];
233        foreach ($attributes as $attribute) {
234            $attr[] = $attribute instanceof Attribute ? $attribute : new Attribute($attribute);
235        }
236        $this->attributes = $attr;
237
238        return $this;
239    }
240
241    /**
242     * @return Dn|null
243     */
244    public function getBaseDn(): ?Dn
245    {
246        return $this->baseDn;
247    }
248
249    /**
250     * @param string|Dn|null $baseDn
251     * @return $this
252     */
253    public function setBaseDn($baseDn)
254    {
255        if ($baseDn !== null) {
256            $baseDn = $baseDn instanceof Dn ? $baseDn : new Dn($baseDn);
257        }
258        $this->baseDn = $baseDn;
259
260        return $this;
261    }
262
263    /**
264     * @return int
265     */
266    public function getScope(): int
267    {
268        return $this->scope;
269    }
270
271    /**
272     * @param int $scope
273     * @return $this
274     */
275    public function setScope(int $scope)
276    {
277        $this->scope = $scope;
278
279        return $this;
280    }
281
282    /**
283     * @return int
284     */
285    public function getDereferenceAliases(): int
286    {
287        return $this->derefAliases;
288    }
289
290    /**
291     * @param int $derefAliases
292     * @return $this
293     */
294    public function setDereferenceAliases(int $derefAliases)
295    {
296        $this->derefAliases = $derefAliases;
297
298        return $this;
299    }
300
301    /**
302     * @return int
303     */
304    public function getSizeLimit(): int
305    {
306        return $this->sizeLimit;
307    }
308
309    /**
310     * @param int $sizeLimit
311     * @return $this
312     */
313    public function setSizeLimit(int $sizeLimit)
314    {
315        $this->sizeLimit = $sizeLimit;
316
317        return $this;
318    }
319
320    /**
321     * @return int
322     */
323    public function getTimeLimit(): int
324    {
325        return $this->timeLimit;
326    }
327
328    /**
329     * @param int $timeLimit
330     * @return $this
331     */
332    public function setTimeLimit(int $timeLimit)
333    {
334        $this->timeLimit = $timeLimit;
335
336        return $this;
337    }
338
339    /**
340     * @return bool
341     */
342    public function getAttributesOnly(): bool
343    {
344        return $this->attributesOnly;
345    }
346
347    /**
348     * @param bool $attributesOnly
349     * @return $this
350     */
351    public function setAttributesOnly(bool $attributesOnly)
352    {
353        $this->attributesOnly = $attributesOnly;
354
355        return $this;
356    }
357
358    /**
359     * @return FilterInterface
360     */
361    public function getFilter(): FilterInterface
362    {
363        return $this->filter;
364    }
365
366    /**
367     * @param FilterInterface $filter
368     * @return $this
369     */
370    public function setFilter(FilterInterface $filter)
371    {
372        $this->filter = $filter;
373
374        return $this;
375    }
376
377    /**
378     * {@inheritDoc}
379     * @return self
380     * @throws RuntimeException
381     */
382    public static function fromAsn1(AbstractType $type)
383    {
384        if (!($type instanceof SequenceType && count($type) === 8)) {
385            throw new ProtocolException('The search request is malformed');
386        }
387        $baseDn = $type->getChild(0);
388        $scope = $type->getChild(1);
389        $deref = $type->getChild(2);
390        $sizeLimit = $type->getChild(3);
391        $timeLimit = $type->getChild(4);
392        $typesOnly = $type->getChild(5);
393        $attributes = $type->getChild(7);
394
395        $filter = $type->getChild(6);
396        if ($filter === null) {
397            throw new ProtocolException('The search request is malformed.');
398        }
399        $filter = FilterFactory::get($filter);
400
401        if (
402            !($baseDn instanceof OctetStringType
403            && $scope instanceof EnumeratedType
404            && $deref instanceof EnumeratedType
405            && $sizeLimit instanceof IntegerType
406            && $timeLimit instanceof IntegerType
407            && $typesOnly instanceof BooleanType
408            && $attributes instanceof SequenceType)
409        ) {
410            throw new ProtocolException('The search request is malformed');
411        }
412
413        $attrList = [];
414        foreach ($attributes->getChildren() as $attribute) {
415            if (!$attribute instanceof OctetStringType) {
416                throw new ProtocolException('The search request is malformed.');
417            }
418            $attrList[] = new Attribute($attribute->getValue());
419        }
420
421        $search = new self($filter, ...$attrList);
422        $search->setScope($scope->getValue());
423        $search->setBaseDn($baseDn->getValue());
424        $search->setDereferenceAliases($deref->getValue());
425        $search->setSizeLimit($sizeLimit->getValue());
426        $search->setTimeLimit($timeLimit->getValue());
427        $search->setAttributesOnly($typesOnly->getValue());
428
429        return $search;
430    }
431
432    /**
433     * @throws RuntimeException
434     */
435    public function toAsn1(): AbstractType
436    {
437        if ($this->baseDn === null) {
438            throw new RuntimeException('The search baseDn cannot be empty.');
439        }
440
441        return Asn1::application(self::APP_TAG, Asn1::sequence(
442            Asn1::octetString($this->baseDn),
443            Asn1::enumerated($this->scope),
444            Asn1::enumerated($this->derefAliases),
445            Asn1::integer($this->sizeLimit),
446            Asn1::integer($this->timeLimit),
447            Asn1::boolean($this->attributesOnly),
448            $this->filter->toAsn1(),
449            Asn1::sequenceOf(...array_map(function ($attr) {
450                /** @var Attribute|string $attr */
451                return Asn1::octetString($attr instanceof Attribute ? $attr->getDescription() : $attr);
452            }, $this->attributes))
453        ));
454    }
455}
456