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