* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace FreeDSx\Ldap\Entry; use ArrayIterator; use Countable; use IteratorAggregate; use Traversable; use function array_keys; use function array_search; use function array_shift; use function array_values; use function count; use function explode; use function implode; use function str_replace; use function strpos; use function strtolower; /** * Represents an entry attribute and any values. * * @author Chad Sikorra */ class Attribute implements IteratorAggregate, Countable { use EscapeTrait; protected const ESCAPE_MAP = [ '\\' => '\5c', '*' => '\2a', '(' => '\28', ')' => '\29', "\x00" => '\00', ]; /** * @var string */ protected $attribute; /** * @var null|string */ protected $lcAttribute; /** * @var mixed[]|string[] */ protected $values = []; /** * @var null|Options */ protected $options; /** * @param string $attribute * @param mixed|string ...$values */ public function __construct(string $attribute, ...$values) { $this->attribute = $attribute; $this->values = $values; } /** * Add a value, or values, to the attribute. * * @param mixed|string ...$values * @return $this */ public function add(...$values): self { foreach ($values as $value) { $this->values[] = $value; } return $this; } /** * Check if the attribute has a specific value. * * @param mixed|string $value * @return bool */ public function has($value): bool { return array_search($value, $this->values, true) !== false; } /** * Remove a specific value, or values, from an attribute. * * @param mixed|string ...$values * @return $this */ public function remove(...$values): self { foreach ($values as $value) { if (($i = array_search($value, $this->values, true)) !== false) { unset($this->values[$i]); } } return $this; } /** * Resets the values to any empty array. * * @return $this */ public function reset(): self { $this->values = []; return $this; } /** * Set the values for the attribute. * * @param mixed|string ...$values * @return $this */ public function set(...$values): self { $this->values = $values; return $this; } /** * Gets the name (AttributeType) portion of the AttributeDescription, which excludes the options. * * @return string */ public function getName(): string { $this->options(); return $this->attribute; } /** * Gets the full AttributeDescription (RFC 4512, 2.5), which contains the attribute type (name) and options. * * @return string */ public function getDescription(): string { return $this->getName() . ($this->options()->count() > 0 ? ';' . $this->options()->toString() : ''); } /** * Gets any values associated with the attribute. * * @return array */ public function getValues(): array { return $this->values; } /** * Retrieve the first value of the attribute. * * @return string|mixed|null */ public function firstValue() { return $this->values[0] ?? null; } /** * Retrieve the last value of the attribute. * * @return string|mixed|null */ public function lastValue() { $last = end($this->values); reset($this->values); return $last === false ? null : $last; } /** * Gets the options within the AttributeDescription (semi-colon separated list of options). * * @return Options */ public function getOptions(): Options { return $this->options(); } /** * @return bool */ public function hasOptions(): bool { return ($this->options()->count() > 0); } /** * {@inheritDoc} */ public function getIterator(): Traversable { return new ArrayIterator($this->values); } /** * {@inheritDoc} */ public function count(): int { return count($this->values); } /** * @param Attribute $attribute * @param bool $strict If set to true, then options must also match. * @return bool */ public function equals(Attribute $attribute, bool $strict = false): bool { $this->options(); $attribute->options(); if ($this->lcAttribute === null) { $this->lcAttribute = strtolower($this->attribute); } if ($attribute->lcAttribute === null) { $attribute->lcAttribute = strtolower($attribute->attribute); } $nameMatches = ($this->lcAttribute === $attribute->lcAttribute); # Only the name of the attribute is checked for by default. # If strict is selected, or the attribute to be checked has explicit options, then the opposing attribute must too if ($strict || $attribute->hasOptions()) { return $nameMatches && ($this->getOptions()->toString(true) === $attribute->getOptions()->toString(true)); } return $nameMatches; } /** * @return string */ public function __toString(): string { return implode(', ', $this->values); } /** * Escape an attribute value for a filter. * * @param string $value * @return string */ public static function escape(string $value): string { if (self::shouldNotEscape($value)) { return $value; } $value = str_replace(array_keys(self::ESCAPE_MAP), array_values(self::ESCAPE_MAP), $value); return self::escapeNonPrintable($value); } /** * A one time check and load of any attribute options. */ protected function options(): Options { if ($this->options !== null) { return $this->options; } if (strpos($this->attribute, ';') === false) { $this->options = new Options(); return $this->options; } $options = explode(';', $this->attribute); $this->attribute = (string) array_shift($options); $this->options = new Options(...$options); return $this->options; } }