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