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\PagingControl; 16use FreeDSx\Ldap\Controls; 17use FreeDSx\Ldap\Entry\Entries; 18use FreeDSx\Ldap\Exception\OperationException; 19use FreeDSx\Ldap\Exception\ProtocolException; 20use FreeDSx\Ldap\LdapClient; 21use FreeDSx\Ldap\Operation\Request\SearchRequest; 22use FreeDSx\Ldap\Operation\Response\SearchResponse; 23 24/** 25 * Provides a simple wrapper around paging a search operation. 26 * 27 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 28 */ 29class Paging 30{ 31 /** 32 * @var PagingControl|null 33 */ 34 protected $control; 35 36 /** 37 * @var LdapClient 38 */ 39 protected $client; 40 41 /** 42 * @var int 43 */ 44 protected $size; 45 46 /** 47 * @var SearchRequest 48 */ 49 protected $search; 50 51 /** 52 * @var bool 53 */ 54 protected $ended = false; 55 56 /** 57 * @var bool 58 */ 59 protected $isCritical = false; 60 61 /** 62 * @param LdapClient $client 63 * @param SearchRequest $search 64 * @param int $size 65 */ 66 public function __construct(LdapClient $client, SearchRequest $search, int $size = 1000) 67 { 68 $this->search = $search; 69 $this->client = $client; 70 $this->size = $size; 71 } 72 73 /** 74 * Set the criticality of the control. Setting this will cause the LDAP server to return an error if paging is not 75 * possible. 76 * 77 * @param bool $isCritical 78 * @return $this 79 */ 80 public function isCritical(bool $isCritical = true): self 81 { 82 $this->isCritical = $isCritical; 83 84 return $this; 85 } 86 87 /** 88 * Start a new paging operation with a search request. This must be called first if you reuse the paging object. 89 * 90 * @param SearchRequest $search 91 * @param int|null $size 92 */ 93 public function start(SearchRequest $search, ?int $size = null): void 94 { 95 $this->size = $size ?? $this->size; 96 $this->search = $search; 97 $this->control = null; 98 $this->ended = false; 99 } 100 101 /** 102 * End the paging operation. This can be triggered at any time. 103 * 104 * @return $this 105 * @throws OperationException 106 */ 107 public function end() 108 { 109 $this->send(0); 110 $this->ended = true; 111 112 return $this; 113 } 114 115 /** 116 * Get the next set of entries of results. 117 * 118 * @param int|null $size 119 * @return Entries 120 * @throws OperationException 121 */ 122 public function getEntries(?int $size = null): Entries 123 { 124 return $this->send($size); 125 } 126 127 /** 128 * @return bool 129 */ 130 public function hasEntries() 131 { 132 if ($this->ended) { 133 return false; 134 } 135 136 return $this->control === null || !($this->control->getCookie() === ''); 137 } 138 139 /** 140 * The size may be set to the server's estimate of the total number of entries in the entire result set. Servers 141 * that cannot provide such an estimate may set this size to zero. 142 * 143 * @return int|null 144 */ 145 public function sizeEstimate(): ?int 146 { 147 return ($this->control !== null) ? $this->control->getSize() : null; 148 } 149 150 /** 151 * @param int|null $size 152 * @return Entries 153 * @throws OperationException 154 */ 155 protected function send(?int $size = null) 156 { 157 $cookie = ($this->control !== null) 158 ? $this->control->getCookie() 159 : ''; 160 $message = $this->client->sendAndReceive( 161 $this->search, 162 Controls::paging($size ?? $this->size, $cookie) 163 ->setCriticality($this->isCritical) 164 ); 165 $control = $message->controls() 166 ->get(Control::OID_PAGING); 167 168 if ($control !== null && !$control instanceof PagingControl) { 169 throw new ProtocolException(sprintf( 170 'Expected a paging control, but received: %s.', 171 get_class($control) 172 )); 173 } 174 # OpenLDAP returns no paging control in response to an abandon request. However, other LDAP implementations do; 175 # such as Active Directory. It's not clear from the paging RFC which is correct. 176 if ($control === null && $size !== 0 && $this->isCritical) { 177 throw new ProtocolException('Expected a paging control, but received none.'); 178 } 179 # The server does not support paging, but the control was not marked as critical. In this case the server will 180 # return results but might ignore the control altogether. 181 if ($control === null && $size !== 0 && !$this->isCritical) { 182 $this->ended = true; 183 } 184 $this->control = $control; 185 /** @var SearchResponse $response */ 186 $response = $message->getResponse(); 187 188 return $response->getEntries(); 189 } 190} 191