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