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\Entry;
13
14use FreeDSx\Ldap\Exception\InvalidArgumentException;
15use function array_keys;
16use function array_values;
17use function count;
18use function explode;
19use function preg_split;
20use function str_replace;
21use function substr;
22use function substr_replace;
23
24/**
25 * Represents a Relative Distinguished Name.
26 *
27 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
28 */
29class Rdn
30{
31    use EscapeTrait;
32
33    public const ESCAPE_MAP = [
34        '\\' => '\\5c',
35        '"' => '\\22',
36        '+' => '\\2b',
37        ',' => '\\2c',
38        ';' => '\\3b',
39        '<' => '\\3c',
40        '>' => '\\3e',
41    ];
42
43    /**
44     * @var string
45     */
46    protected $name;
47
48    /**
49     * @var string
50     */
51    protected $value;
52
53    /**
54     * @var Rdn[]
55     */
56    protected $additional = [];
57
58    /**
59     * @param string $name
60     * @param string $value
61     */
62    public function __construct(string $name, string $value)
63    {
64        $this->name = $name;
65        $this->value = $value;
66    }
67
68    /**
69     * @return string
70     */
71    public function getName(): string
72    {
73        return $this->name;
74    }
75
76    /**
77     * @return string
78     */
79    public function getValue(): string
80    {
81        return $this->value;
82    }
83
84    /**
85     * @return bool
86     */
87    public function isMultivalued(): bool
88    {
89        return count($this->additional) !== 0;
90    }
91
92    /**
93     * @return string
94     */
95    public function toString(): string
96    {
97        $rdn = $this->name . '=' . $this->value;
98
99        foreach ($this->additional as $additional) {
100            $rdn .= '+' . $additional->getName() . '=' . $additional->getValue();
101        }
102
103        return $rdn;
104    }
105
106    /**
107     * @return string
108     */
109    public function __toString()
110    {
111        return $this->toString();
112    }
113
114    /**
115     * @param string $rdn
116     * @return Rdn
117     * @throws InvalidArgumentException
118     */
119    public static function create(string $rdn): Rdn
120    {
121        $pieces = preg_split('/(?<!\\\\)\+/', $rdn);
122        if ($pieces === false) {
123            throw new InvalidArgumentException(sprintf('The RDN "%s" is invalid.', $rdn));
124        }
125
126        // @todo Simplify this logic somehow?
127        $obj = null;
128        foreach ($pieces as $piece) {
129            $parts = explode('=', $piece, 2);
130            if (count($parts) !== 2) {
131                throw new InvalidArgumentException(sprintf('The RDN "%s" is invalid.', $piece));
132            }
133            if ($obj === null) {
134                $obj = new self($parts[0], $parts[1]);
135            } else {
136                /** @var Rdn $obj */
137                $obj->additional[] = new self($parts[0], $parts[1]);
138            }
139        }
140
141        if ($obj === null) {
142            throw new InvalidArgumentException(sprintf("The RDN '%s' is not valid.", $rdn));
143        }
144
145        return $obj;
146    }
147
148    /**
149     * Escape an RDN value.
150     *
151     * @param string $value
152     * @return string
153     */
154    public static function escape(string $value): string
155    {
156        if (self::shouldNotEscape($value)) {
157            return $value;
158        }
159        $value = str_replace(array_keys(self::ESCAPE_MAP), array_values(self::ESCAPE_MAP), $value);
160
161        if ($value[0] === '#' || $value[0] === ' ') {
162            $value = ($value[0] === '#' ? '\23' : '\20') . substr($value, 1);
163        }
164        if ($value[-1] === ' ') {
165            $value = substr_replace($value, '\20', -1, 1);
166        }
167
168        return self::escapeNonPrintable($value);
169    }
170}
171