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\Control\Control; 15use FreeDSx\Ldap\Control\Sorting\SortingControl; 16use FreeDSx\Ldap\Control\Sorting\SortKey; 17use FreeDSx\Ldap\Control\Vlv\VlvControl; 18use FreeDSx\Ldap\Control\Vlv\VlvResponseControl; 19use FreeDSx\Ldap\Controls; 20use FreeDSx\Ldap\Entry\Entries; 21use FreeDSx\Ldap\Exception\OperationException; 22use FreeDSx\Ldap\Exception\ProtocolException; 23use FreeDSx\Ldap\LdapClient; 24use FreeDSx\Ldap\Operation\Request\SearchRequest; 25use FreeDSx\Ldap\Operation\Response\SearchResponse; 26use FreeDSx\Ldap\Search\Filter\GreaterThanOrEqualFilter; 27 28/** 29 * Provides a simple wrapper around a VLV (Virtual List View) search operation. 30 * 31 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 32 */ 33class Vlv 34{ 35 /** 36 * @var LdapClient 37 */ 38 protected $client; 39 40 /** 41 * @var SearchRequest 42 */ 43 protected $search; 44 45 /** 46 * @var VlvResponseControl|null 47 */ 48 protected $control; 49 50 /** 51 * @var int 52 */ 53 protected $before; 54 55 /** 56 * @var int 57 */ 58 protected $after; 59 60 /** 61 * @var int 62 */ 63 protected $offset = 1; 64 65 /** 66 * @var GreaterThanOrEqualFilter 67 */ 68 protected $filter; 69 70 /** 71 * @var SortingControl 72 */ 73 protected $sort; 74 75 /** 76 * @var bool 77 */ 78 protected $asPercentage = false; 79 80 /** 81 * @param LdapClient $client 82 * @param SearchRequest $search 83 * @param SortingControl|SortKey|string $sort 84 * @param int $before 85 * @param int $after 86 */ 87 public function __construct(LdapClient $client, SearchRequest $search, $sort, int $after = 100, int $before = 0) 88 { 89 $this->client = $client; 90 $this->search = $search; 91 $this->sort = $sort instanceof SortingControl ? $sort : Controls::sort($sort); 92 $this->before = $before; 93 $this->after = $after; 94 } 95 96 /** 97 * As a percentage the moveTo, moveForward, moveBackward, and position methods work with numbers 0 - 100 and should 98 * be interpreted as percentages. 99 * 100 * @param bool $asPercentage 101 * @return $this 102 */ 103 public function asPercentage(bool $asPercentage = true) 104 { 105 $this->asPercentage = $asPercentage; 106 107 return $this; 108 } 109 110 /** 111 * Request to start at a specific offset/percentage of entries. 112 * 113 * @param int $offset 114 * @return $this 115 */ 116 public function startAt(int $offset) 117 { 118 $this->offset = $offset; 119 120 return $this; 121 } 122 123 /** 124 * Move backward the specified number or percentage from the current position. 125 * 126 * @param int $size 127 * @return $this 128 */ 129 public function moveBackward(int $size) 130 { 131 $this->offset = ($this->offset - $size < 0) ? 0 : $this->offset - $size; 132 133 return $this; 134 } 135 136 /** 137 * Move forward the specified number or percentage from the current position. 138 * 139 * @param int $size 140 * @return $this 141 */ 142 public function moveForward(int $size) 143 { 144 $this->offset = ($this->asPercentage && ($this->offset + $size) > 100) ? 100 : $this->offset + $size; 145 146 return $this; 147 } 148 149 /** 150 * Moves the starting entry of the list to a specific position/percentage of the total list. An alias for startAt(). 151 * 152 * @param int $position 153 * @return Vlv 154 */ 155 public function moveTo(int $position) 156 { 157 return $this->startAt($position); 158 } 159 160 /** 161 * Retrieve the following number of entries after the position specified. 162 * 163 * @param int $after 164 * @return $this 165 */ 166 public function afterPosition(int $after) 167 { 168 $this->after = $after; 169 170 return $this; 171 } 172 173 /** 174 * Retrieve the following number of entries before the position specified. 175 * 176 * @param int $before 177 * @return $this 178 */ 179 public function beforePosition(int $before) 180 { 181 $this->before = $before; 182 183 return $this; 184 } 185 186 /** 187 * Get the servers entry offset of the current list. 188 * 189 * @return int|null 190 */ 191 public function listOffset(): ?int 192 { 193 return ($this->control !== null) ? $this->control->getOffset() : null; 194 } 195 196 /** 197 * Get the severs estimate, from the last request, that indicates how many entries are in the list. 198 * 199 * @return int|null 200 */ 201 public function listSize(): ?int 202 { 203 return ($this->control !== null) ? $this->control->getCount() : null; 204 } 205 206 /** 207 * Get the current position in the list. When as percentage was specified this will be expressed as a percentage. 208 * Use listOffset for a specific entry offset position. 209 * 210 * @return int|null 211 */ 212 public function position(): ?int 213 { 214 $control = $this->control; 215 $pos = $control === null ? null : $control->getOffset(); 216 if ($control === null || $pos === null) { 217 return null; 218 } 219 220 if ($this->asPercentage) { 221 return (int) round($pos / ((int) $control->getCount() / 100)); 222 } else { 223 return $control->getOffset(); 224 } 225 } 226 227 /** 228 * Whether or not we are at the end of the list. 229 * 230 * @return bool 231 */ 232 public function isAtEndOfList(): bool 233 { 234 if ($this->control === null) { 235 return false; 236 } 237 238 $control = $this->control; 239 if ((((int) $control->getOffset() + $this->after) >= (int) $control->getCount())) { 240 return true; 241 } 242 243 return $control->getOffset() === $control->getCount(); 244 } 245 246 /** 247 * Whether or not we are currently at the start of the list. 248 * 249 * @return bool 250 */ 251 public function isAtStartOfList(): bool 252 { 253 if ($this->control === null) { 254 return false; 255 } 256 $control = $this->control; 257 if ($this->before !== 0 && ((int) $control->getOffset() - $this->before) <= 1) { 258 return true; 259 } 260 261 return $control->getOffset() === 1; 262 } 263 264 /** 265 * @return Entries 266 * @throws ProtocolException 267 * @throws OperationException 268 */ 269 public function getEntries(): Entries 270 { 271 return $this->send(); 272 } 273 274 /** 275 * @return Entries 276 * @throws OperationException 277 */ 278 protected function send(): Entries 279 { 280 $contextId = ($this->control !== null) ? $this->control->getContextId() : null; 281 $message = $this->client->sendAndReceive($this->search, $this->createVlvControl($contextId), $this->sort); 282 $control = $message->controls()->get(Control::OID_VLV_RESPONSE); 283 if ($control === null || !$control instanceof VlvResponseControl) { 284 throw new ProtocolException('Expected a VLV response control, but received none.'); 285 } 286 $this->control = $control; 287 /** @var SearchResponse $response */ 288 $response = $message->getResponse(); 289 290 return $response->getEntries(); 291 } 292 293 /** 294 * @return VlvControl 295 */ 296 protected function createVlvControl(?string $contextId): VlvControl 297 { 298 if ($this->filter !== null) { 299 return Controls::vlvFilter($this->before, $this->after, $this->filter, $contextId); 300 } 301 # An offset of 1 and a content size of zero starts from the beginning entry (server uses its assumed count). 302 $count = ($this->control !== null) ? (int) $this->control->getCount() : 0; 303 304 # In percentage mode start off with an assumed count of 100, as the formula the server uses should give us the 305 # expected result 306 if ($this->control === null && $this->asPercentage) { 307 $count = 100; 308 } 309 310 $offset = $this->offset; 311 # Final checks to make sure if we are using a percentage that valid values are used. 312 if ($this->asPercentage && $this->offset > 100) { 313 $offset = 100; 314 } elseif ($this->asPercentage && $this->offset < 0) { 315 $offset = 0; 316 } 317 318 if ($this->asPercentage && $this->control !== null) { 319 $offset = (int) round(((int) $this->control->getCount() / 100) * $offset); 320 } 321 322 return Controls::vlv($this->before, $this->after, $offset, $count, $contextId); 323 } 324} 325