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