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\EnumeratedType;
17use FreeDSx\Asn1\Type\OctetStringType;
18use FreeDSx\Asn1\Type\SequenceType;
19use FreeDSx\Asn1\Type\SetType;
20use FreeDSx\Ldap\Entry\Attribute;
21use FreeDSx\Ldap\Entry\Change;
22use FreeDSx\Ldap\Entry\Dn;
23use FreeDSx\Ldap\Exception\ProtocolException;
24use function array_map;
25use function count;
26
27/**
28 * A Modify Request. RFC 4511, 4.6
29 *
30 * ModifyRequest ::= [APPLICATION 6] SEQUENCE {
31 *     object          LDAPDN,
32 *     changes         SEQUENCE OF change SEQUENCE {
33 *         operation       ENUMERATED {
34 *             add     (0),
35 *             delete  (1),
36 *             replace (2),
37 *             ...  },
38 *         modification    PartialAttribute } }
39 *
40 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
41 */
42class ModifyRequest implements RequestInterface, DnRequestInterface
43{
44    protected const APP_TAG = 6;
45
46    /**
47     * @var Change[]
48     */
49    protected $changes;
50
51    /**
52     * @var Dn
53     */
54    protected $dn;
55
56    /**
57     * @param string|Dn $dn
58     * @param Change ...$changes
59     */
60    public function __construct($dn, Change ...$changes)
61    {
62        $this->setDn($dn);
63        $this->changes = $changes;
64    }
65
66    /**
67     * @return Change[]
68     */
69    public function getChanges(): array
70    {
71        return $this->changes;
72    }
73
74    /**
75     * @param Change ...$changes
76     * @return $this
77     */
78    public function setChanges(Change ...$changes)
79    {
80        $this->changes = $changes;
81
82        return $this;
83    }
84
85    /**
86     * @param string|Dn $dn
87     * @return $this
88     */
89    public function setDn($dn)
90    {
91        $this->dn = $dn instanceof $dn ? $dn : new Dn($dn);
92
93        return $this;
94    }
95
96    /**
97     * @return Dn
98     */
99    public function getDn(): Dn
100    {
101        return $this->dn;
102    }
103
104    /**
105     * {@inheritDoc}
106     * @return self
107     */
108    public static function fromAsn1(AbstractType $type)
109    {
110        if (!($type instanceof SequenceType && count($type) === 2)) {
111            throw new ProtocolException('The modify request is malformed');
112        }
113
114        $dn = $type->getChild(0);
115        $changes = $type->getChild(1);
116        if (!($dn instanceof OctetStringType && $changes instanceof SequenceType)) {
117            throw new ProtocolException('The modify request is malformed');
118        }
119
120        $changeList = [];
121        foreach ($changes->getChildren() as $change) {
122            $changeList[] = self::parseChange($change);
123        }
124
125        return new self($dn->getValue(), ...$changeList);
126    }
127
128    /**
129     * {@inheritdoc}
130     */
131    public function toAsn1(): AbstractType
132    {
133        $changes = Asn1::sequenceOf();
134
135        foreach ($this->changes as $change) {
136            $changeSeq = Asn1::sequence(Asn1::enumerated($change->getType()));
137
138            $changeSeq->addChild(Asn1::sequence(
139                Asn1::octetString($change->getAttribute()->getDescription()),
140                Asn1::setOf(...array_map(function ($value) {
141                    return Asn1::octetString($value);
142                }, $change->getAttribute()->getValues()))
143            ));
144
145            $changes->addChild($changeSeq);
146        }
147
148        return Asn1::application(self::APP_TAG, Asn1::sequence(
149            Asn1::octetString($this->dn->toString()),
150            $changes
151        ));
152    }
153
154    /**
155     * @param AbstractType $type
156     * @return Change
157     * @throws ProtocolException
158     */
159    protected static function parseChange(AbstractType $type): Change
160    {
161        if (!($type instanceof SequenceType && count($type->getChildren()) === 2)) {
162            throw new ProtocolException('The change for the modify request is malformed.');
163        }
164
165        $operation = $type->getChild(0);
166        $modification = $type->getChild(1);
167        if (!($operation instanceof EnumeratedType && $modification instanceof SequenceType)) {
168            throw new ProtocolException('The change for the modify request is malformed.');
169        }
170
171        return new Change($operation->getValue(), self::parsePartialAttribute($modification));
172    }
173
174    /**
175     * @param SequenceType $type
176     * @return Attribute
177     * @throws ProtocolException
178     */
179    protected static function parsePartialAttribute(SequenceType $type): Attribute
180    {
181        if (count($type->getChildren()) !== 2) {
182            throw new ProtocolException('The partial attribute for the modify request is malformed.');
183        }
184
185        $attrType = $type->getChild(0);
186        $attrVals = $type->getChild(1);
187        if (!($attrType instanceof OctetStringType && $attrVals instanceof SetType)) {
188            throw new ProtocolException('The partial attribute for the modify request is malformed.');
189        }
190
191        $values = [];
192        foreach ($attrVals->getChildren() as $attrVal) {
193            if (!$attrVal instanceof OctetStringType) {
194                throw new ProtocolException('The partial attribute for the modify request is malformed.');
195            }
196            $values[] = $attrVal->getValue();
197        }
198
199        return new Attribute($attrType->getValue(), ...$values);
200    }
201}
202