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