xref: /plugin/pureldap/vendor/freedsx/ldap/src/FreeDSx/Ldap/Entry/Attribute.php (revision 0b3fd2d31e4d1997548a8fbc53fa771027c4a47f)
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\Entry;
12
13/**
14 * Represents an entry attribute and any values.
15 *
16 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
17 */
18class Attribute implements \IteratorAggregate, \Countable
19{
20    use EscapeTrait;
21
22    protected const ESCAPE_MAP = [
23        '\\' => '\5c',
24        '*' => '\2a',
25        '(' => '\28',
26        ')' => '\29',
27        "\x00" => '\00',
28    ];
29
30    /**
31     * @var string
32     */
33    protected $attribute;
34
35    /**
36     * @var null|string
37     */
38    protected $lcAttribute;
39
40    /**
41     * @var mixed[]|string[]
42     */
43    protected $values = [];
44
45    /**
46     * @var null|Options
47     */
48    protected $options;
49
50    /**
51     * @param string $attribute
52     * @param mixed[]|string[] ...$values
53     */
54    public function __construct(string $attribute, ...$values)
55    {
56        $this->attribute = $attribute;
57        $this->values = $values;
58    }
59
60    /**
61     * Add a value, or values, to the attribute.
62     *
63     * @param mixed[]|string[] ...$values
64     * @return $this
65     */
66    public function add(...$values): self
67    {
68        foreach ($values as $value) {
69            $this->values[] = $value;
70        }
71
72        return $this;
73    }
74
75    /**
76     * Check if the attribute has a specific value.
77     *
78     * @param mixed|string $value
79     * @return bool
80     */
81    public function has($value): bool
82    {
83        return \array_search($value, $this->values, true) !== false;
84    }
85
86    /**
87     * Remove a specific value, or values, from an attribute.
88     *
89     * @param mixed[]|string[] ...$values
90     * @return $this
91     */
92    public function remove(...$values): self
93    {
94        foreach ($values as $value) {
95            if (($i = \array_search($value, $this->values, true)) !== false) {
96                unset($this->values[$i]);
97            }
98        }
99
100        return $this;
101    }
102
103    /**
104     * Resets the values to any empty array.
105     *
106     * @return $this
107     */
108    public function reset(): self
109    {
110        $this->values = [];
111
112        return $this;
113    }
114
115    /**
116     * Set the values for the attribute.
117     *
118     * @param mixed[]|string[] ...$values
119     * @return $this
120     */
121    public function set(...$values): self
122    {
123        $this->values = $values;
124
125        return $this;
126    }
127
128    /**
129     * Gets the name (AttributeType) portion of the AttributeDescription, which excludes the options.
130     *
131     * @return string
132     */
133    public function getName(): string
134    {
135        $this->options();
136
137        return $this->attribute;
138    }
139
140    /**
141     * Gets the full AttributeDescription (RFC 4512, 2.5), which contains the attribute type (name) and options.
142     *
143     * @return string
144     */
145    public function getDescription(): string
146    {
147        return $this->getName() . ($this->options()->count() > 0 ? ';' . $this->options()->toString() : '');
148    }
149
150    /**
151     * Gets any values associated with the attribute.
152     *
153     * @return array
154     */
155    public function getValues(): array
156    {
157        return $this->values;
158    }
159
160    /**
161     * Retrieve the first value of the attribute.
162     *
163     * @return string|mixed|null
164     */
165    public function firstValue()
166    {
167        return $this->values[0] ?? null;
168    }
169
170    /**
171     * Retrieve the last value of the attribute.
172     *
173     * @return string|mixed|null
174     */
175    public function lastValue()
176    {
177        $last = end($this->values);
178        reset($this->values);
179
180        return $last === false ? null : $last;
181    }
182
183    /**
184     * Gets the options within the AttributeDescription (semi-colon separated list of options).
185     *
186     * @return Options
187     */
188    public function getOptions(): Options
189    {
190        return $this->options();
191    }
192
193    /**
194     * @return bool
195     */
196    public function hasOptions(): bool
197    {
198        return ($this->options()->count() > 0);
199    }
200
201    /**
202     * {@inheritDoc}
203     */
204    public function getIterator(): \ArrayIterator
205    {
206        return new \ArrayIterator($this->values);
207    }
208
209    /**
210     * {@inheritDoc}
211     */
212    public function count(): int
213    {
214        return \count($this->values);
215    }
216
217    /**
218     * @param Attribute $attribute
219     * @param bool $strict If set to true, then options must also match.
220     * @return bool
221     */
222    public function equals(Attribute $attribute, bool $strict = false): bool
223    {
224        $this->options();
225        $attribute->options();
226        if ($this->lcAttribute === null) {
227            $this->lcAttribute = \strtolower($this->attribute);
228        }
229        if ($attribute->lcAttribute === null) {
230            $attribute->lcAttribute = \strtolower($attribute->attribute);
231        }
232        $nameMatches = ($this->lcAttribute === $attribute->lcAttribute);
233
234        # Only the name of the attribute is checked for by default.
235        # If strict is selected, or the attribute to be checked has explicit options, then the opposing attribute must too
236        if ($strict || $attribute->hasOptions()) {
237            return $nameMatches && ($this->getOptions()->toString(true) === $attribute->getOptions()->toString(true));
238        }
239
240        return $nameMatches;
241    }
242
243    /**
244     * @return string
245     */
246    public function __toString(): string
247    {
248        return \implode(', ', $this->values);
249    }
250
251    /**
252     * Escape an attribute value for a filter.
253     *
254     * @param string $value
255     * @return string
256     */
257    public static function escape(string $value): string
258    {
259        if (self::shouldNotEscape($value)) {
260            return $value;
261        }
262        $value = \str_replace(\array_keys(self::ESCAPE_MAP), \array_values(self::ESCAPE_MAP), $value);
263
264        return self::escapeNonPrintable($value);
265    }
266
267    /**
268     * A one time check and load of any attribute options.
269     */
270    protected function options(): Options
271    {
272        if ($this->options !== null) {
273            return $this->options;
274        }
275        if (\strpos($this->attribute, ';') === false) {
276            $this->options = new Options();
277
278            return $this->options;
279        }
280        $options = \explode(';', $this->attribute);
281        $this->attribute = (string) \array_shift($options);
282        $this->options = new Options(...$options);
283
284        return $this->options;
285    }
286}
287