blob: c6977a8e8d057cf728df7c57d833ff22557859ed [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace LdapRecord\Models\Attributes;
4
5use LdapRecord\EscapesValues;
6use LdapRecord\Support\Arr;
7
8class DistinguishedName
9{
10 use EscapesValues;
11
12 /**
13 * The underlying raw value.
14 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010015 * @var string
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020016 */
17 protected $value;
18
19 /**
20 * Constructor.
21 *
22 * @param string|null $value
23 */
24 public function __construct($value = null)
25 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010026 $this->value = trim((string) $value);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020027 }
28
29 /**
30 * Get the distinguished name value.
31 *
32 * @return string
33 */
34 public function __toString()
35 {
36 return (string) $this->value;
37 }
38
39 /**
40 * Alias of the "build" method.
41 *
42 * @param string|null $value
43 *
44 * @return DistinguishedNameBuilder
45 */
46 public static function of($value = null)
47 {
48 return static::build($value);
49 }
50
51 /**
52 * Get a new DN builder object from the given DN.
53 *
54 * @param string|null $value
55 *
56 * @return DistinguishedNameBuilder
57 */
58 public static function build($value = null)
59 {
60 return new DistinguishedNameBuilder($value);
61 }
62
63 /**
64 * Make a new distinguished name instance.
65 *
66 * @param string|null $value
67 *
68 * @return static
69 */
70 public static function make($value = null)
71 {
72 return new static($value);
73 }
74
75 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010076 * Determine if the given value is a valid distinguished name.
77 *
78 * @param string $value
79 *
80 * @return bool
81 */
82 public static function isValid($value)
83 {
84 return ! static::make($value)->isEmpty();
85 }
86
87 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020088 * Explode a distinguished name into relative distinguished names.
89 *
90 * @param string $dn
91 *
92 * @return array
93 */
94 public static function explode($dn)
95 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010096 $components = ldap_explode_dn($dn, (int) $withoutAttributes = false);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020097
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010098 if (! is_array($components)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020099 return [];
100 }
101
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100102 if (! array_key_exists('count', $components)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200103 return [];
104 }
105
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100106 unset($components['count']);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200107
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100108 return $components;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200109 }
110
111 /**
112 * Un-escapes a hexadecimal string into its original string representation.
113 *
114 * @param string $value
115 *
116 * @return string
117 */
118 public static function unescape($value)
119 {
120 return preg_replace_callback('/\\\([0-9A-Fa-f]{2})/', function ($matches) {
121 return chr(hexdec($matches[1]));
122 }, $value);
123 }
124
125 /**
126 * Explode the RDN into an attribute and value.
127 *
128 * @param string $rdn
129 *
130 * @return array
131 */
132 public static function explodeRdn($rdn)
133 {
134 return explode('=', $rdn, $limit = 2);
135 }
136
137 /**
138 * Implode the component attribute and value into an RDN.
139 *
140 * @param string $rdn
141 *
142 * @return string
143 */
144 public static function makeRdn(array $component)
145 {
146 return implode('=', $component);
147 }
148
149 /**
150 * Get the underlying value.
151 *
152 * @return string|null
153 */
154 public function get()
155 {
156 return $this->value;
157 }
158
159 /**
160 * Set the underlying value.
161 *
162 * @param string|null $value
163 *
164 * @return $this
165 */
166 public function set($value)
167 {
168 $this->value = $value;
169
170 return $this;
171 }
172
173 /**
174 * Get the distinguished name values without attributes.
175 *
176 * @return array
177 */
178 public function values()
179 {
180 $values = [];
181
182 foreach ($this->multi() as [, $value]) {
183 $values[] = static::unescape($value);
184 }
185
186 return $values;
187 }
188
189 /**
190 * Get the distinguished name attributes without values.
191 *
192 * @return array
193 */
194 public function attributes()
195 {
196 $attributes = [];
197
198 foreach ($this->multi() as [$attribute]) {
199 $attributes[] = $attribute;
200 }
201
202 return $attributes;
203 }
204
205 /**
206 * Get the distinguished name components with attributes.
207 *
208 * @return array
209 */
210 public function components()
211 {
212 $components = [];
213
214 foreach ($this->multi() as [$attribute, $value]) {
215 // When a distinguished name is exploded, the values are automatically
216 // escaped. This cannot be opted out of. Here we will unescape
217 // the attribute value, then re-escape it to its original
218 // representation from the server using the "dn" flag.
219 $value = $this->escape(static::unescape($value))->dn();
220
221 $components[] = static::makeRdn([$attribute, $value]);
222 }
223
224 return $components;
225 }
226
227 /**
228 * Convert the distinguished name into an associative array.
229 *
230 * @return array
231 */
232 public function assoc()
233 {
234 $map = [];
235
236 foreach ($this->multi() as [$attribute, $value]) {
237 $attribute = $this->normalize($attribute);
238
239 array_key_exists($attribute, $map)
240 ? $map[$attribute][] = $value
241 : $map[$attribute] = [$value];
242 }
243
244 return $map;
245 }
246
247 /**
248 * Split the RDNs into a multi-dimensional array.
249 *
250 * @return array
251 */
252 public function multi()
253 {
254 return array_map(function ($rdn) {
255 return static::explodeRdn($rdn);
256 }, $this->rdns());
257 }
258
259 /**
260 * Split the distinguished name into an array of unescaped RDN's.
261 *
262 * @return array
263 */
264 public function rdns()
265 {
266 return static::explode($this->value);
267 }
268
269 /**
270 * Get the first RDNs value.
271 *
272 * @return string|null
273 */
274 public function name()
275 {
276 return Arr::first($this->values());
277 }
278
279 /**
280 * Get the first RDNs attribute.
281 *
282 * @return string|null
283 */
284 public function head()
285 {
286 return Arr::first($this->attributes());
287 }
288
289 /**
290 * Get the relative distinguished name.
291 *
292 * @return string|null
293 */
294 public function relative()
295 {
296 return Arr::first($this->components());
297 }
298
299 /**
300 * Alias of relative().
301 *
302 * Get the first RDN from the distinguished name.
303 *
304 * @return string|null
305 */
306 public function first()
307 {
308 return $this->relative();
309 }
310
311 /**
312 * Get the parent distinguished name.
313 *
314 * @return string|null
315 */
316 public function parent()
317 {
318 $components = $this->components();
319
320 array_shift($components);
321
322 return implode(',', $components) ?: null;
323 }
324
325 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100326 * Determine if the distinguished name is empty.
327 *
328 * @return bool
329 */
330 public function isEmpty()
331 {
332 return empty(
333 array_filter($this->values())
334 );
335 }
336
337 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200338 * Determine if the current distinguished name is a parent of the given child.
339 *
340 * @param DistinguishedName $child
341 *
342 * @return bool
343 */
344 public function isParentOf(self $child)
345 {
346 return $child->isChildOf($this);
347 }
348
349 /**
350 * Determine if the current distinguished name is a child of the given parent.
351 *
352 * @param DistinguishedName $parent
353 *
354 * @return bool
355 */
356 public function isChildOf(self $parent)
357 {
358 if (
359 empty($components = $this->components()) ||
360 empty($parentComponents = $parent->components())
361 ) {
362 return false;
363 }
364
365 array_shift($components);
366
367 return $this->compare($components, $parentComponents);
368 }
369
370 /**
371 * Determine if the current distinguished name is an ancestor of the descendant.
372 *
373 * @param DistinguishedName $descendant
374 *
375 * @return bool
376 */
377 public function isAncestorOf(self $descendant)
378 {
379 return $descendant->isDescendantOf($this);
380 }
381
382 /**
383 * Determine if the current distinguished name is a descendant of the ancestor.
384 *
385 * @param DistinguishedName $ancestor
386 *
387 * @return bool
388 */
389 public function isDescendantOf(self $ancestor)
390 {
391 if (
392 empty($components = $this->components()) ||
393 empty($ancestorComponents = $ancestor->components())
394 ) {
395 return false;
396 }
397
398 if (! $length = count($components) - count($ancestorComponents)) {
399 return false;
400 }
401
402 array_splice($components, $offset = 0, $length);
403
404 return $this->compare($components, $ancestorComponents);
405 }
406
407 /**
408 * Compare whether the two distinguished name values are equal.
409 *
410 * @param array $values
411 * @param array $other
412 *
413 * @return bool
414 */
415 protected function compare(array $values, array $other)
416 {
417 return $this->recase($values) == $this->recase($other);
418 }
419
420 /**
421 * Recase the array values.
422 *
423 * @param array $values
424 *
425 * @return array
426 */
427 protected function recase(array $values)
428 {
429 return array_map([$this, 'normalize'], $values);
430 }
431
432 /**
433 * Normalize the string value.
434 *
435 * @param string $value
436 *
437 * @return string
438 */
439 protected function normalize($value)
440 {
441 return strtolower($value);
442 }
443}