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\Search\Filter;
13
14use FreeDSx\Asn1\Asn1;
15use FreeDSx\Asn1\Exception\EncoderException;
16use FreeDSx\Asn1\Type\AbstractType;
17use FreeDSx\Asn1\Type\BooleanType;
18use FreeDSx\Asn1\Type\IncompleteType;
19use FreeDSx\Asn1\Type\OctetStringType;
20use FreeDSx\Asn1\Type\SequenceType;
21use FreeDSx\Ldap\Entry\Attribute;
22use FreeDSx\Ldap\Exception\ProtocolException;
23use FreeDSx\Ldap\Protocol\LdapEncoder;
24
25/**
26 * Represents an extensible matching rule filter. RFC 4511, 4.5.1.7.7
27 *
28 * MatchingRuleAssertion ::= SEQUENCE {
29 *     matchingRule    [1] MatchingRuleId OPTIONAL,
30 *     type            [2] AttributeDescription OPTIONAL,
31 *     matchValue      [3] AssertionValue,
32 *     dnAttributes    [4] BOOLEAN DEFAULT FALSE }
33 *
34 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
35 */
36class MatchingRuleFilter implements FilterInterface
37{
38    protected const CHOICE_TAG = 9;
39
40    /**
41     * @var null|string
42     */
43    protected $matchingRule;
44
45    /**
46     * @var null|string
47     */
48    protected $attribute;
49
50    /**
51     * @var string
52     */
53    protected $value;
54
55    /**
56     * @var bool
57     */
58    protected $useDnAttributes;
59
60    /**
61     * @param null|string $matchingRule
62     * @param null|string $attribute
63     * @param string $value
64     * @param bool $useDnAttributes
65     */
66    public function __construct(?string $matchingRule, ?string $attribute, string $value, bool $useDnAttributes = false)
67    {
68        $this->matchingRule = $matchingRule;
69        $this->attribute = $attribute;
70        $this->value = $value;
71        $this->useDnAttributes = $useDnAttributes;
72    }
73
74    /**
75     * @return null|string
76     */
77    public function getAttribute(): ?string
78    {
79        return $this->attribute;
80    }
81
82    /**
83     * @param null|string $attribute
84     * @return $this
85     */
86    public function setAttribute(?string $attribute)
87    {
88        $this->attribute = $attribute;
89
90        return $this;
91    }
92
93    /**
94     * @return null|string
95     */
96    public function getMatchingRule(): ?string
97    {
98        return $this->matchingRule;
99    }
100
101    /**
102     * @param null|string $matchingRule
103     * @return $this
104     */
105    public function setMatchingRule(?string $matchingRule)
106    {
107        $this->matchingRule = $matchingRule;
108
109        return $this;
110    }
111
112    /**
113     * @return string
114     */
115    public function getValue(): string
116    {
117        return $this->value;
118    }
119
120    /**
121     * @param string $value
122     * @return $this
123     */
124    public function setValue(string $value)
125    {
126        $this->value = $value;
127
128        return $this;
129    }
130
131    /**
132     * @return bool
133     */
134    public function getUseDnAttributes(): bool
135    {
136        return $this->useDnAttributes;
137    }
138
139    /**
140     * @param bool $useDnAttributes
141     * @return $this
142     */
143    public function setUseDnAttributes(bool $useDnAttributes)
144    {
145        $this->useDnAttributes = $useDnAttributes;
146
147        return $this;
148    }
149
150    /**
151     * {@inheritdoc}
152     */
153    public function toAsn1(): AbstractType
154    {
155        /** @var \FreeDSx\Asn1\Type\SequenceType $matchingRule */
156        $matchingRule = Asn1::context(self::CHOICE_TAG, Asn1::sequence());
157
158        if ($this->matchingRule !== null) {
159            $matchingRule->addChild(Asn1::context(1, Asn1::octetString($this->matchingRule)));
160        }
161        if ($this->attribute !== null) {
162            $matchingRule->addChild(Asn1::context(2, Asn1::octetString($this->attribute)));
163        }
164        $matchingRule->addChild(Asn1::context(3, Asn1::octetString($this->value)));
165        $matchingRule->addChild(Asn1::context(4, Asn1::boolean($this->useDnAttributes)));
166
167        return $matchingRule;
168    }
169
170    /**
171     * {@inheritdoc}
172     */
173    public function toString(): string
174    {
175        $filter = '';
176        if ($this->attribute !== null) {
177            $filter = $this->attribute;
178        }
179        if ($this->matchingRule !== null) {
180            $filter .= ':' . $this->matchingRule;
181        }
182        if ($this->useDnAttributes) {
183            $filter .= ':dn';
184        }
185
186        return self::PAREN_LEFT . $filter . self::FILTER_EXT . Attribute::escape($this->value) . self::PAREN_RIGHT;
187    }
188
189    /**
190     * @return string
191     */
192    public function __toString()
193    {
194        return $this->toString();
195    }
196
197    /**
198     * {@inheritDoc}
199     * @return MatchingRuleFilter
200     * @throws EncoderException
201     */
202    public static function fromAsn1(AbstractType $type)
203    {
204        $type = $type instanceof IncompleteType ? (new LdapEncoder())->complete($type, AbstractType::TAG_TYPE_SEQUENCE) : $type;
205        if (!($type instanceof SequenceType && (count($type) >= 1 && count($type) <= 4))) {
206            throw new ProtocolException('The matching rule filter is malformed');
207        }
208        $matchingRule = null;
209        $matchingType = null;
210        $matchValue = null;
211        $useDnAttr = null;
212
213        foreach ($type->getChildren() as $child) {
214            if ($child->getTagClass() !== AbstractType::TAG_CLASS_CONTEXT_SPECIFIC) {
215                continue;
216            }
217            if ($child->getTagNumber() === 1) {
218                $matchingRule = $child;
219            } elseif ($child->getTagNumber() === 2) {
220                $matchingType = $child;
221            } elseif ($child->getTagNumber() === 3) {
222                $matchValue = $child;
223            } elseif ($child->getTagNumber() === 4) {
224                $useDnAttr = $child;
225            }
226        }
227        if (!$matchValue instanceof OctetStringType) {
228            throw new ProtocolException('The matching rule filter is malformed.');
229        }
230        if ($matchingRule !== null && !$matchingRule instanceof OctetStringType) {
231            throw new ProtocolException('The matching rule filter is malformed.');
232        }
233        if ($matchingType !== null && !$matchingType instanceof OctetStringType) {
234            throw new ProtocolException('The matching rule filter is malformed.');
235        }
236        if ($useDnAttr !== null && !$useDnAttr instanceof BooleanType) {
237            throw new ProtocolException('The matching rule filter is malformed.');
238        }
239        $matchingRule = ($matchingRule !== null) ? $matchingRule->getValue() : null;
240        $matchingType = ($matchingType !== null) ? $matchingType->getValue() : null;
241        $useDnAttr = ($useDnAttr !== null) ? $useDnAttr->getValue() : false;
242
243        return new self(
244            $matchingRule,
245            $matchingType,
246            $matchValue->getValue(),
247            $useDnAttr
248        );
249    }
250}
251