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\Control;
13
14use FreeDSx\Asn1\Asn1;
15use FreeDSx\Asn1\Exception\EncoderException;
16use FreeDSx\Asn1\Exception\PartialPduException;
17use FreeDSx\Asn1\Type\AbstractType;
18use FreeDSx\Asn1\Type\BooleanType;
19use FreeDSx\Asn1\Type\OctetStringType;
20use FreeDSx\Asn1\Type\SequenceType;
21use FreeDSx\Ldap\Exception\ProtocolException;
22use FreeDSx\Ldap\Protocol\LdapEncoder;
23use FreeDSx\Ldap\Protocol\ProtocolElementInterface;
24use function count;
25
26/**
27 * Represents a control. RFC 4511, 4.1.11
28 *
29 * Control ::= SEQUENCE {
30 *     controlType             LDAPOID,
31 *     criticality             BOOLEAN DEFAULT FALSE,
32 *     controlValue            OCTET STRING OPTIONAL }
33 *
34 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
35 */
36class Control implements ProtocolElementInterface
37{
38    public const OID_DIR_SYNC = '1.2.840.113556.1.4.841';
39
40    public const OID_EXPECTED_ENTRY_COUNT = '1.2.840.113556.1.4.2211';
41
42    public const OID_EXTENDED_DN = '1.2.840.113556.1.4.529';
43
44    public const OID_PAGING = '1.2.840.113556.1.4.319';
45
46    public const OID_POLICY_HINTS = '1.2.840.113556.1.4.2239';
47
48    public const OID_PWD_POLICY = '1.3.6.1.4.1.42.2.27.8.5.1';
49
50    public const OID_SET_OWNER = '1.2.840.113556.1.4.2255';
51
52    public const OID_SD_FLAGS = '1.2.840.113556.1.4.801';
53
54    public const OID_SHOW_DELETED = '1.2.840.113556.1.4.417';
55
56    public const OID_SHOW_RECYCLED = '1.2.840.113556.1.4.2064';
57
58    public const OID_SUBTREE_DELETE = '1.2.840.113556.1.4.805';
59
60    public const OID_SORTING = '1.2.840.113556.1.4.473';
61
62    public const OID_SORTING_RESPONSE = '1.2.840.113556.1.4.474';
63
64    public const OID_VLV = '2.16.840.1.113730.3.4.9';
65
66    public const OID_VLV_RESPONSE = '2.16.840.1.113730.3.4.10';
67
68    /**
69     * @var string
70     */
71    protected $controlType;
72
73    /**
74     * @var bool
75     */
76    protected $criticality;
77
78    /**
79     * @var AbstractType|ProtocolElementInterface|string|null
80     */
81    protected $controlValue;
82
83    /**
84     * @param string $controlType
85     * @param bool $criticality
86     * @param null|mixed $controlValue
87     */
88    public function __construct(string $controlType, bool $criticality = false, $controlValue = null)
89    {
90        $this->controlType = $controlType;
91        $this->criticality = $criticality;
92        $this->controlValue = $controlValue;
93    }
94
95    /**
96     * @param string $oid
97     * @return $this
98     */
99    public function setTypeOid(string $oid)
100    {
101        $this->controlType = $oid;
102
103        return $this;
104    }
105
106    /**
107     * @return string
108     */
109    public function getTypeOid(): string
110    {
111        return $this->controlType;
112    }
113
114    /**
115     * @param bool $criticality
116     * @return $this
117     */
118    public function setCriticality(bool $criticality)
119    {
120        $this->criticality = $criticality;
121
122        return $this;
123    }
124
125    /**
126     * @return bool
127     */
128    public function getCriticality(): bool
129    {
130        return $this->criticality;
131    }
132
133    /**
134     * @param AbstractType|ProtocolElementInterface|string|null $controlValue
135     * @return $this
136     */
137    public function setValue($controlValue)
138    {
139        $this->controlValue = $controlValue;
140
141        return $this;
142    }
143
144    /**
145     * @return AbstractType|ProtocolElementInterface|string|null
146     */
147    public function getValue()
148    {
149        return $this->controlValue;
150    }
151
152    /**
153     * @return AbstractType
154     * @throws EncoderException
155     */
156    public function toAsn1(): AbstractType
157    {
158        $asn1 = Asn1::sequence(
159            Asn1::octetString($this->controlType),
160            Asn1::boolean($this->criticality)
161        );
162
163        if ($this->controlValue !== null) {
164            $encoder = new LdapEncoder();
165            if ($this->controlValue instanceof AbstractType) {
166                $value = $encoder->encode($this->controlValue);
167            } elseif ($this->controlValue instanceof ProtocolElementInterface) {
168                $value = $encoder->encode($this->controlValue->toAsn1());
169            } else {
170                $value = $this->controlValue;
171            }
172            $asn1->addChild(Asn1::octetString($value));
173        }
174
175        return $asn1;
176    }
177
178    /**
179     * @return string
180     */
181    public function __toString()
182    {
183        return $this->controlType;
184    }
185
186    /**
187     * {@inheritDoc}
188     * @return static|ProtocolElementInterface
189     */
190    public static function fromAsn1(AbstractType $type)
191    {
192        if (!$type instanceof SequenceType) {
193            throw new ProtocolException(sprintf(
194                'Protocol encoding issue. Expected a sequence type but received: %s',
195                get_class($type)
196            ));
197        }
198
199        return new static(...self::parseAsn1ControlValues($type));
200    }
201
202    /**
203     * @param Control $control
204     * @param AbstractType $type
205     * @return self
206     * @throws ProtocolException
207     */
208    protected static function mergeControlData(Control $control, AbstractType $type)
209    {
210        if (!($type instanceof SequenceType && count($type->getChildren()) <= 3)) {
211            throw new ProtocolException(sprintf(
212                'The received control is malformed. Expected at least 3 sequence values. Received %s.',
213                count($type->getChildren())
214            ));
215        }
216        [0 => $control->controlType, 1 => $control->criticality, 2 => $control->controlValue] = self::parseAsn1ControlValues($type);
217
218        return $control;
219    }
220
221    /**
222     * @param AbstractType $type
223     * @return AbstractType
224     * @throws ProtocolException
225     * @throws EncoderException
226     * @throws PartialPduException
227     */
228    protected static function decodeEncodedValue(AbstractType $type)
229    {
230        if (!$type instanceof SequenceType) {
231            throw new ProtocolException('The received control is malformed. Unable to get the encoded value.');
232        }
233
234        [2 => $value] = self::parseAsn1ControlValues($type);
235        if ($value === null) {
236            throw new ProtocolException('The received control is malformed. Unable to get the encoded value.');
237        }
238
239        return (new LdapEncoder())->decode($value);
240    }
241
242    /**
243     * @param SequenceType $type
244     * @return array
245     */
246    protected static function parseAsn1ControlValues(SequenceType $type)
247    {
248        $oid = null;
249        $criticality = false;
250        $value = null;
251
252        /*
253         * RFC 4511, 4.1.1. States responses should not have criticality set, but not that it must not be set. So do not
254         * assume the position of the octet string value. Accounts for the additional logic of the checks here.
255         */
256        foreach ($type->getChildren() as $i => $child) {
257            if ($child->getTagClass() !== AbstractType::TAG_CLASS_UNIVERSAL) {
258                continue;
259            }
260
261            if ($i === 0) {
262                $oid = $child->getValue();
263            } elseif ($child instanceof BooleanType) {
264                $criticality = $child->getValue();
265            } elseif ($child instanceof OctetStringType) {
266                $value = $child->getValue();
267            }
268        }
269
270        return [$oid, $criticality, $value];
271    }
272}
273