10b3fd2d3SAndreas Gohr<?php 2*dad993c5SAndreas Gohr 30b3fd2d3SAndreas Gohr/** 40b3fd2d3SAndreas Gohr * This file is part of the FreeDSx LDAP package. 50b3fd2d3SAndreas Gohr * 60b3fd2d3SAndreas Gohr * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 70b3fd2d3SAndreas Gohr * 80b3fd2d3SAndreas Gohr * For the full copyright and license information, please view the LICENSE 90b3fd2d3SAndreas Gohr * file that was distributed with this source code. 100b3fd2d3SAndreas Gohr */ 110b3fd2d3SAndreas Gohr 120b3fd2d3SAndreas Gohrnamespace FreeDSx\Ldap\Search; 130b3fd2d3SAndreas Gohr 140b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Entry\Attribute; 150b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Entry\Dn; 160b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Entry\Entry; 170b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Entry\Option; 18*dad993c5SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException; 190b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Exception\RuntimeException; 200b3fd2d3SAndreas Gohruse FreeDSx\Ldap\LdapClient; 210b3fd2d3SAndreas Gohr 220b3fd2d3SAndreas Gohr/** 230b3fd2d3SAndreas Gohr * Provides simple helper APIs for retrieving ranged results for an entry attribute. 240b3fd2d3SAndreas Gohr * 250b3fd2d3SAndreas Gohr * @see https://docs.microsoft.com/en-us/windows/desktop/adsi/attribute-range-retrieval 260b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com> 270b3fd2d3SAndreas Gohr */ 280b3fd2d3SAndreas Gohrclass RangeRetrieval 290b3fd2d3SAndreas Gohr{ 300b3fd2d3SAndreas Gohr /** 310b3fd2d3SAndreas Gohr * @var LdapClient 320b3fd2d3SAndreas Gohr */ 330b3fd2d3SAndreas Gohr protected $client; 340b3fd2d3SAndreas Gohr 350b3fd2d3SAndreas Gohr /** 360b3fd2d3SAndreas Gohr * @param LdapClient $client 370b3fd2d3SAndreas Gohr */ 380b3fd2d3SAndreas Gohr public function __construct(LdapClient $client) 390b3fd2d3SAndreas Gohr { 400b3fd2d3SAndreas Gohr $this->client = $client; 410b3fd2d3SAndreas Gohr } 420b3fd2d3SAndreas Gohr 430b3fd2d3SAndreas Gohr /** 440b3fd2d3SAndreas Gohr * Get a specific ranged attribute by name from an entry. If it does not exist it will return null. 450b3fd2d3SAndreas Gohr * 460b3fd2d3SAndreas Gohr * @param string|Attribute $attribute 470b3fd2d3SAndreas Gohr */ 480b3fd2d3SAndreas Gohr public function getRanged(Entry $entry, $attribute): ?Attribute 490b3fd2d3SAndreas Gohr { 500b3fd2d3SAndreas Gohr $attribute = $attribute instanceof Attribute ? new Attribute($attribute->getName()) : new Attribute($attribute); 510b3fd2d3SAndreas Gohr 520b3fd2d3SAndreas Gohr foreach ($this->getAllRanged($entry) as $rangedAttribute) { 530b3fd2d3SAndreas Gohr if ($rangedAttribute->equals($attribute)) { 540b3fd2d3SAndreas Gohr return $rangedAttribute; 550b3fd2d3SAndreas Gohr } 560b3fd2d3SAndreas Gohr } 570b3fd2d3SAndreas Gohr 580b3fd2d3SAndreas Gohr return null; 590b3fd2d3SAndreas Gohr } 600b3fd2d3SAndreas Gohr 610b3fd2d3SAndreas Gohr /** 620b3fd2d3SAndreas Gohr * Get all ranged attributes as an array from a entry. 630b3fd2d3SAndreas Gohr * 640b3fd2d3SAndreas Gohr * @param Entry $entry 650b3fd2d3SAndreas Gohr * @return Attribute[] 66*dad993c5SAndreas Gohr * @psalm-return list<Attribute> 670b3fd2d3SAndreas Gohr */ 680b3fd2d3SAndreas Gohr public function getAllRanged(Entry $entry): array 690b3fd2d3SAndreas Gohr { 700b3fd2d3SAndreas Gohr $ranged = []; 710b3fd2d3SAndreas Gohr 720b3fd2d3SAndreas Gohr foreach ($entry->getAttributes() as $attribute) { 730b3fd2d3SAndreas Gohr if (!$attribute->hasOptions()) { 740b3fd2d3SAndreas Gohr continue; 750b3fd2d3SAndreas Gohr } 760b3fd2d3SAndreas Gohr /** @var Option $option */ 770b3fd2d3SAndreas Gohr foreach ($attribute->getOptions() as $option) { 780b3fd2d3SAndreas Gohr if ($option->isRange()) { 790b3fd2d3SAndreas Gohr $ranged[] = $attribute; 800b3fd2d3SAndreas Gohr break; 810b3fd2d3SAndreas Gohr } 820b3fd2d3SAndreas Gohr } 830b3fd2d3SAndreas Gohr } 840b3fd2d3SAndreas Gohr 850b3fd2d3SAndreas Gohr return $ranged; 860b3fd2d3SAndreas Gohr } 870b3fd2d3SAndreas Gohr 880b3fd2d3SAndreas Gohr /** 890b3fd2d3SAndreas Gohr * A simple check to determine if an entry contains any ranged attributes. Optionally pass an attribute 900b3fd2d3SAndreas Gohr * 910b3fd2d3SAndreas Gohr * @param Entry $entry 920b3fd2d3SAndreas Gohr * @param Attribute|string|null $attribute 930b3fd2d3SAndreas Gohr * @return bool 940b3fd2d3SAndreas Gohr */ 950b3fd2d3SAndreas Gohr public function hasRanged(Entry $entry, $attribute = null): bool 960b3fd2d3SAndreas Gohr { 970b3fd2d3SAndreas Gohr return (bool) ($attribute !== null ? $this->getRanged($entry, $attribute) : $this->getAllRanged($entry)); 980b3fd2d3SAndreas Gohr } 990b3fd2d3SAndreas Gohr 1000b3fd2d3SAndreas Gohr /** 1010b3fd2d3SAndreas Gohr * Check if an attribute has more range values that can be queried. 1020b3fd2d3SAndreas Gohr * 1030b3fd2d3SAndreas Gohr * @param Attribute $attribute 1040b3fd2d3SAndreas Gohr * @return bool 1050b3fd2d3SAndreas Gohr */ 1060b3fd2d3SAndreas Gohr public function hasMoreValues(Attribute $attribute): bool 1070b3fd2d3SAndreas Gohr { 1080b3fd2d3SAndreas Gohr if (($range = $this->getRangeOption($attribute)) === null) { 1090b3fd2d3SAndreas Gohr return false; 1100b3fd2d3SAndreas Gohr } 1110b3fd2d3SAndreas Gohr 1120b3fd2d3SAndreas Gohr return $range->getHighRange() !== '*'; 1130b3fd2d3SAndreas Gohr } 1140b3fd2d3SAndreas Gohr 1150b3fd2d3SAndreas Gohr /** 1160b3fd2d3SAndreas Gohr * Given a specific Entry/DN and an attribute, get the next set of ranged values available. Optionally pass a third 1170b3fd2d3SAndreas Gohr * parameter to control how many values to grab next. 1180b3fd2d3SAndreas Gohr * 1190b3fd2d3SAndreas Gohr * @param Entry|Dn|string $entry 1200b3fd2d3SAndreas Gohr * @param Attribute $attribute 1210b3fd2d3SAndreas Gohr * @param string|int $amount 1220b3fd2d3SAndreas Gohr * @return Attribute 123*dad993c5SAndreas Gohr * @throws OperationException 1240b3fd2d3SAndreas Gohr */ 1250b3fd2d3SAndreas Gohr public function getMoreValues($entry, Attribute $attribute, $amount = '*'): Attribute 1260b3fd2d3SAndreas Gohr { 1270b3fd2d3SAndreas Gohr if (($range = $this->getRangeOption($attribute)) === null || !$this->hasMoreValues($attribute)) { 1280b3fd2d3SAndreas Gohr return new Attribute($attribute->getName()); 1290b3fd2d3SAndreas Gohr } 1300b3fd2d3SAndreas Gohr if ($amount !== '*') { 1310b3fd2d3SAndreas Gohr $amount = (int) $amount + (int) $range->getHighRange(); 1320b3fd2d3SAndreas Gohr } 1330b3fd2d3SAndreas Gohr $attrReq = new Attribute($attribute->getName()); 1340b3fd2d3SAndreas Gohr $startAt = (int) $range->getHighRange() + 1; 1350b3fd2d3SAndreas Gohr $attrReq->getOptions()->set(Option::fromRange((string) $startAt, (string) $amount)); 1360b3fd2d3SAndreas Gohr $result = $this->client->readOrFail($entry, [$attrReq]); 1370b3fd2d3SAndreas Gohr 1380b3fd2d3SAndreas Gohr $attrResult = $result->get($attribute->getName()); 1390b3fd2d3SAndreas Gohr if ($attrResult === null) { 1400b3fd2d3SAndreas Gohr throw new RuntimeException(sprintf( 1410b3fd2d3SAndreas Gohr 'The attribute %s was not returned from LDAP', 1420b3fd2d3SAndreas Gohr $attribute->getName() 1430b3fd2d3SAndreas Gohr )); 1440b3fd2d3SAndreas Gohr } 1450b3fd2d3SAndreas Gohr if (($range = $this->getRangeOption($attrResult)) === null) { 1460b3fd2d3SAndreas Gohr throw new RuntimeException(sprintf( 1470b3fd2d3SAndreas Gohr 'No ranged option received for attribute "%s" on "%s".', 1480b3fd2d3SAndreas Gohr $attribute->getName(), 1490b3fd2d3SAndreas Gohr $result->getDn()->toString() 1500b3fd2d3SAndreas Gohr )); 1510b3fd2d3SAndreas Gohr } 1520b3fd2d3SAndreas Gohr 1530b3fd2d3SAndreas Gohr return $attrResult; 1540b3fd2d3SAndreas Gohr } 1550b3fd2d3SAndreas Gohr 1560b3fd2d3SAndreas Gohr /** 1570b3fd2d3SAndreas Gohr * Given a specific entry and attribute, range retrieve all values of the attribute. 1580b3fd2d3SAndreas Gohr * 1590b3fd2d3SAndreas Gohr * @param Entry|Dn|string $entry 1600b3fd2d3SAndreas Gohr * @param string|Attribute $attribute 1610b3fd2d3SAndreas Gohr * @return Attribute 162*dad993c5SAndreas Gohr * @throws OperationException 1630b3fd2d3SAndreas Gohr */ 1640b3fd2d3SAndreas Gohr public function getAllValues($entry, $attribute): Attribute 1650b3fd2d3SAndreas Gohr { 1660b3fd2d3SAndreas Gohr $attrResult = $attribute instanceof Attribute ? new Attribute($attribute->getName()) : new Attribute($attribute); 1670b3fd2d3SAndreas Gohr $attrResult->getOptions()->set(Option::fromRange('0')); 1680b3fd2d3SAndreas Gohr 1690b3fd2d3SAndreas Gohr $entry = $this->client->readOrFail($entry, [$attrResult]); 1700b3fd2d3SAndreas Gohr $attribute = $this->getRanged($entry, $attrResult); 1710b3fd2d3SAndreas Gohr if ($attribute === null) { 1720b3fd2d3SAndreas Gohr throw new RuntimeException(sprintf( 1730b3fd2d3SAndreas Gohr 'No ranged result received for "%s" on entry "%s".', 1740b3fd2d3SAndreas Gohr $attrResult->getName(), 1750b3fd2d3SAndreas Gohr $entry->getDn()->toString() 1760b3fd2d3SAndreas Gohr )); 1770b3fd2d3SAndreas Gohr } 1780b3fd2d3SAndreas Gohr 1790b3fd2d3SAndreas Gohr $attrResult->add(...$attribute->getValues()); 1800b3fd2d3SAndreas Gohr while ($this->hasMoreValues($attribute)) { 1810b3fd2d3SAndreas Gohr $attribute = $this->getMoreValues($entry, $attribute); 1820b3fd2d3SAndreas Gohr $attrResult->add(...$attribute->getValues()); 1830b3fd2d3SAndreas Gohr } 1840b3fd2d3SAndreas Gohr 1850b3fd2d3SAndreas Gohr return $attrResult; 1860b3fd2d3SAndreas Gohr } 1870b3fd2d3SAndreas Gohr 1880b3fd2d3SAndreas Gohr /** 1890b3fd2d3SAndreas Gohr * @param Attribute $attribute 1900b3fd2d3SAndreas Gohr * @return Option|null 1910b3fd2d3SAndreas Gohr */ 1920b3fd2d3SAndreas Gohr protected function getRangeOption(Attribute $attribute): ?Option 1930b3fd2d3SAndreas Gohr { 1940b3fd2d3SAndreas Gohr /** @var Option $option */ 1950b3fd2d3SAndreas Gohr foreach ($attribute->getOptions() as $option) { 1960b3fd2d3SAndreas Gohr if ($option->isRange()) { 1970b3fd2d3SAndreas Gohr return $option; 1980b3fd2d3SAndreas Gohr } 1990b3fd2d3SAndreas Gohr } 2000b3fd2d3SAndreas Gohr 2010b3fd2d3SAndreas Gohr return null; 2020b3fd2d3SAndreas Gohr } 2030b3fd2d3SAndreas Gohr} 204