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\Search; 13 14use FreeDSx\Ldap\Entry\Attribute; 15use FreeDSx\Ldap\Entry\Dn; 16use FreeDSx\Ldap\Entry\Entry; 17use FreeDSx\Ldap\Entry\Option; 18use FreeDSx\Ldap\Exception\OperationException; 19use FreeDSx\Ldap\Exception\RuntimeException; 20use FreeDSx\Ldap\LdapClient; 21 22/** 23 * Provides simple helper APIs for retrieving ranged results for an entry attribute. 24 * 25 * @see https://docs.microsoft.com/en-us/windows/desktop/adsi/attribute-range-retrieval 26 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 27 */ 28class RangeRetrieval 29{ 30 /** 31 * @var LdapClient 32 */ 33 protected $client; 34 35 /** 36 * @param LdapClient $client 37 */ 38 public function __construct(LdapClient $client) 39 { 40 $this->client = $client; 41 } 42 43 /** 44 * Get a specific ranged attribute by name from an entry. If it does not exist it will return null. 45 * 46 * @param string|Attribute $attribute 47 */ 48 public function getRanged(Entry $entry, $attribute): ?Attribute 49 { 50 $attribute = $attribute instanceof Attribute ? new Attribute($attribute->getName()) : new Attribute($attribute); 51 52 foreach ($this->getAllRanged($entry) as $rangedAttribute) { 53 if ($rangedAttribute->equals($attribute)) { 54 return $rangedAttribute; 55 } 56 } 57 58 return null; 59 } 60 61 /** 62 * Get all ranged attributes as an array from a entry. 63 * 64 * @param Entry $entry 65 * @return Attribute[] 66 * @psalm-return list<Attribute> 67 */ 68 public function getAllRanged(Entry $entry): array 69 { 70 $ranged = []; 71 72 foreach ($entry->getAttributes() as $attribute) { 73 if (!$attribute->hasOptions()) { 74 continue; 75 } 76 /** @var Option $option */ 77 foreach ($attribute->getOptions() as $option) { 78 if ($option->isRange()) { 79 $ranged[] = $attribute; 80 break; 81 } 82 } 83 } 84 85 return $ranged; 86 } 87 88 /** 89 * A simple check to determine if an entry contains any ranged attributes. Optionally pass an attribute 90 * 91 * @param Entry $entry 92 * @param Attribute|string|null $attribute 93 * @return bool 94 */ 95 public function hasRanged(Entry $entry, $attribute = null): bool 96 { 97 return (bool) ($attribute !== null ? $this->getRanged($entry, $attribute) : $this->getAllRanged($entry)); 98 } 99 100 /** 101 * Check if an attribute has more range values that can be queried. 102 * 103 * @param Attribute $attribute 104 * @return bool 105 */ 106 public function hasMoreValues(Attribute $attribute): bool 107 { 108 if (($range = $this->getRangeOption($attribute)) === null) { 109 return false; 110 } 111 112 return $range->getHighRange() !== '*'; 113 } 114 115 /** 116 * Given a specific Entry/DN and an attribute, get the next set of ranged values available. Optionally pass a third 117 * parameter to control how many values to grab next. 118 * 119 * @param Entry|Dn|string $entry 120 * @param Attribute $attribute 121 * @param string|int $amount 122 * @return Attribute 123 * @throws OperationException 124 */ 125 public function getMoreValues($entry, Attribute $attribute, $amount = '*'): Attribute 126 { 127 if (($range = $this->getRangeOption($attribute)) === null || !$this->hasMoreValues($attribute)) { 128 return new Attribute($attribute->getName()); 129 } 130 if ($amount !== '*') { 131 $amount = (int) $amount + (int) $range->getHighRange(); 132 } 133 $attrReq = new Attribute($attribute->getName()); 134 $startAt = (int) $range->getHighRange() + 1; 135 $attrReq->getOptions()->set(Option::fromRange((string) $startAt, (string) $amount)); 136 $result = $this->client->readOrFail($entry, [$attrReq]); 137 138 $attrResult = $result->get($attribute->getName()); 139 if ($attrResult === null) { 140 throw new RuntimeException(sprintf( 141 'The attribute %s was not returned from LDAP', 142 $attribute->getName() 143 )); 144 } 145 if (($range = $this->getRangeOption($attrResult)) === null) { 146 throw new RuntimeException(sprintf( 147 'No ranged option received for attribute "%s" on "%s".', 148 $attribute->getName(), 149 $result->getDn()->toString() 150 )); 151 } 152 153 return $attrResult; 154 } 155 156 /** 157 * Given a specific entry and attribute, range retrieve all values of the attribute. 158 * 159 * @param Entry|Dn|string $entry 160 * @param string|Attribute $attribute 161 * @return Attribute 162 * @throws OperationException 163 */ 164 public function getAllValues($entry, $attribute): Attribute 165 { 166 $attrResult = $attribute instanceof Attribute ? new Attribute($attribute->getName()) : new Attribute($attribute); 167 $attrResult->getOptions()->set(Option::fromRange('0')); 168 169 $entry = $this->client->readOrFail($entry, [$attrResult]); 170 $attribute = $this->getRanged($entry, $attrResult); 171 if ($attribute === null) { 172 throw new RuntimeException(sprintf( 173 'No ranged result received for "%s" on entry "%s".', 174 $attrResult->getName(), 175 $entry->getDn()->toString() 176 )); 177 } 178 179 $attrResult->add(...$attribute->getValues()); 180 while ($this->hasMoreValues($attribute)) { 181 $attribute = $this->getMoreValues($entry, $attribute); 182 $attrResult->add(...$attribute->getValues()); 183 } 184 185 return $attrResult; 186 } 187 188 /** 189 * @param Attribute $attribute 190 * @return Option|null 191 */ 192 protected function getRangeOption(Attribute $attribute): ?Option 193 { 194 /** @var Option $option */ 195 foreach ($attribute->getOptions() as $option) { 196 if ($option->isRange()) { 197 return $option; 198 } 199 } 200 201 return null; 202 } 203} 204