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