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;
13
14use FreeDSx\Asn1\Asn1;
15use FreeDSx\Asn1\Exception\EncoderException;
16use FreeDSx\Asn1\Type\AbstractType;
17use FreeDSx\Asn1\Type\IncompleteType;
18use FreeDSx\Asn1\Type\SequenceType;
19use FreeDSx\Ldap\Entry\Dn;
20use FreeDSx\Ldap\Exception\ProtocolException;
21use FreeDSx\Ldap\Exception\UrlParseException;
22use FreeDSx\Ldap\LdapUrl;
23use FreeDSx\Ldap\Operation\Response\ResponseInterface;
24use FreeDSx\Ldap\Protocol\LdapEncoder;
25use function array_map;
26use function count;
27
28/**
29 * Represents the result of an operation request. RFC 4511, 4.1.9
30 *
31 * LDAPResult ::= SEQUENCE {
32 *     resultCode         ENUMERATED {
33 *         success                      (0),
34 *         operationsError              (1),
35 *         protocolError                (2),
36 *         timeLimitExceeded            (3),
37 *         sizeLimitExceeded            (4),
38 *         compareFalse                 (5),
39 *         compareTrue                  (6),
40 *         authMethodNotSupported       (7),
41 *         strongerAuthRequired         (8),
42 *         -- 9 reserved --
43 *         referral                     (10),
44 *         adminLimitExceeded           (11),
45 *         unavailableCriticalExtension (12),
46 *         confidentialityRequired      (13),
47 *         saslBindInProgress           (14),
48 *         noSuchAttribute              (16),
49 *         undefinedAttributeType       (17),
50 *         inappropriateMatching        (18),
51 *         constraintViolation          (19),
52 *         attributeOrValueExists       (20),
53 *         invalidAttributeSyntax       (21),
54 *         -- 22-31 unused --
55 *         noSuchObject                 (32),
56 *         aliasProblem                 (33),
57 *         invalidDNSyntax              (34),
58 *         -- 35 reserved for undefined isLeaf --
59 *         aliasDereferencingProblem    (36),
60 *         -- 37-47 unused --
61 *         inappropriateAuthentication  (48),
62 *         invalidCredentials           (49),
63 *         insufficientAccessRights     (50),
64 *         busy                         (51),
65 *         unavailable                  (52),
66 *         unwillingToPerform           (53),
67 *         loopDetect                   (54),
68 *         -- 55-63 unused --
69 *         namingViolation              (64),
70 *         objectClassViolation         (65),
71 *         notAllowedOnNonLeaf          (66),
72 *         notAllowedOnRDN              (67),
73 *         entryAlreadyExists           (68),
74 *         objectClassModsProhibited    (69),
75 *         -- 70 reserved for CLDAP --
76 *         affectsMultipleDSAs          (71),
77 *         -- 72-79 unused --
78 *         other                        (80),
79 *         ...  },
80 *     matchedDN          LDAPDN,
81 *     diagnosticMessage  LDAPString,
82 *     referral           [3] Referral OPTIONAL }
83 *
84 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
85 */
86class LdapResult implements ResponseInterface
87{
88    /**
89     * @var int
90     */
91    protected $tagNumber;
92
93    /**
94     * @var int
95     */
96    protected $resultCode;
97
98    /**
99     * @var Dn
100     */
101    protected $dn;
102
103    /**
104     * @var string
105     */
106    protected $diagnosticMessage;
107
108    /**
109     * @var LdapUrl[]
110     */
111    protected $referrals = [];
112
113    public function __construct(int $resultCode, string $dn = '', string $diagnosticMessage = '', LdapUrl ...$referrals)
114    {
115        $this->resultCode = $resultCode;
116        $this->dn = new Dn($dn);
117        $this->diagnosticMessage = $diagnosticMessage;
118        $this->referrals = $referrals;
119    }
120
121    /**
122     * @return string
123     */
124    public function getDiagnosticMessage(): string
125    {
126        return $this->diagnosticMessage;
127    }
128
129    /**
130     * @return Dn
131     */
132    public function getDn(): Dn
133    {
134        return $this->dn;
135    }
136
137    /**
138     * @return LdapUrl[]
139     */
140    public function getReferrals(): array
141    {
142        return $this->referrals;
143    }
144
145    /**
146     * {@inheritdoc}
147     */
148    public function getResultCode(): int
149    {
150        return $this->resultCode;
151    }
152
153    /**
154     * @return AbstractType
155     * @throws ProtocolException
156     */
157    public function toAsn1(): AbstractType
158    {
159        $result = Asn1::sequence(
160            Asn1::enumerated($this->resultCode),
161            Asn1::octetString($this->dn),
162            Asn1::octetString($this->diagnosticMessage)
163        );
164        if (count($this->referrals) !== 0) {
165            $result->addChild(Asn1::context(3, Asn1::sequence(...array_map(function ($v) {
166                return Asn1::octetString($v);
167            }, $this->referrals))));
168        }
169        if ($this->tagNumber === null) {
170            throw new ProtocolException(sprintf('You must define the tag number property on %s', get_parent_class()));
171        }
172
173        return Asn1::application($this->tagNumber, $result);
174    }
175
176    /**
177     * {@inheritDoc}
178     * @return self
179     * @throws EncoderException
180     */
181    public static function fromAsn1(AbstractType $type)
182    {
183        [$resultCode, $dn, $diagnosticMessage, $referrals] = self::parseResultData($type);
184
185        return new static($resultCode, $dn, $diagnosticMessage, ...$referrals);
186    }
187
188    /**
189     * @param AbstractType $type
190     * @return array
191     * @psalm-return array{0: mixed, 1: mixed, 2: mixed, 3: list<LdapUrl>}
192     * @throws ProtocolException
193     * @throws EncoderException
194     */
195    protected static function parseResultData(AbstractType $type)
196    {
197        if (!$type instanceof SequenceType) {
198            throw new ProtocolException('The LDAP result is malformed.');
199        }
200        $referrals = [];
201
202        # Somewhat ugly minor optimization. Though it's probably less likely for most setups to get referrals.
203        # So only try to iterate them if we possibly have them.
204        $count = count($type->getChildren());
205        if ($count > 3) {
206            for ($i = 3; $i < $count; $i++) {
207                $child = $type->getChild($i);
208                if ($child !== null && $child->getTagClass() === AbstractType::TAG_CLASS_CONTEXT_SPECIFIC && $child->getTagNumber() === 3) {
209                    if (!$child instanceof IncompleteType) {
210                        throw new ProtocolException('The ASN1 structure for the referrals is malformed.');
211                    }
212                    $child = (new LdapEncoder())->complete($child, AbstractType::TAG_TYPE_SEQUENCE);
213                    foreach ($child->getChildren() as $ldapUrl) {
214                        try {
215                            $referrals[] = LdapUrl::parse($ldapUrl->getValue());
216                        } catch (UrlParseException $e) {
217                            throw new ProtocolException($e->getMessage());
218                        }
219                    }
220                }
221            }
222        }
223
224        $result = $type->getChild(0);
225        $dn = $type->getChild(1);
226        $diagnostic = $type->getChild(2);
227        if ($result === null || $dn === null || $diagnostic === null) {
228            throw new ProtocolException('The LDAP result is malformed.');
229        }
230
231        return [
232            $result->getValue(),
233            $dn->getValue(),
234            $diagnostic->getValue(),
235            $referrals
236        ];
237    }
238}
239