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