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 Closure; 15use FreeDSx\Ldap\Control\Ad\DirSyncRequestControl; 16use FreeDSx\Ldap\Control\Ad\DirSyncResponseControl; 17use FreeDSx\Ldap\Control\Control; 18use FreeDSx\Ldap\Controls; 19use FreeDSx\Ldap\Entry\Entries; 20use FreeDSx\Ldap\Exception\OperationException; 21use FreeDSx\Ldap\Exception\RuntimeException; 22use FreeDSx\Ldap\LdapClient; 23use FreeDSx\Ldap\Operation\Request\SearchRequest; 24use FreeDSx\Ldap\Operation\Response\SearchResponse; 25use FreeDSx\Ldap\Protocol\LdapMessageResponse; 26use FreeDSx\Ldap\Search\Filter\FilterInterface; 27 28/** 29 * Provides a simple wrapper around DirSync for Active Directory. 30 * 31 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 32 */ 33class DirSync 34{ 35 /** 36 * @var DirSyncResponseControl|null 37 */ 38 protected $lastResponse; 39 40 /** 41 * @var SearchRequest 42 */ 43 protected $search; 44 45 /** 46 * @var string|null 47 */ 48 protected $namingContext; 49 50 /** 51 * @var bool 52 */ 53 protected $incrementalValues = true; 54 55 /** 56 * @var bool 57 */ 58 protected $objectSecurity = false; 59 60 /** 61 * @var bool 62 */ 63 protected $ancestorFirstOrder = false; 64 65 /** 66 * @var null|string 67 */ 68 protected $defaultRootNc; 69 70 /** 71 * @var LdapClient 72 */ 73 protected $client; 74 75 /** 76 * @var DirSyncRequestControl 77 */ 78 protected $dirSyncRequest; 79 80 /** 81 * @param LdapClient $client 82 * @param string|null $namingContext 83 * @param FilterInterface|null $filter 84 * @param mixed ...$attributes 85 */ 86 public function __construct(LdapClient $client, ?string $namingContext = null, ?FilterInterface $filter = null, ...$attributes) 87 { 88 $this->client = $client; 89 $this->namingContext = $namingContext; 90 $this->dirSyncRequest = Controls::dirSync(); 91 $this->search = (new SearchRequest($filter ?? Filters::present('objectClass'), ...$attributes)); 92 } 93 94 /** 95 * A convenience method to easily watch for changes with an anonymous function. The anonymous function will be passed 96 * two arguments: 97 * 98 * - The Entries object containing the changes. 99 * - A boolean value indicating whether or not the entries are part of the initial sync (the initial sync returns 100 * all entries matching the filter). 101 * 102 * An optional second argument then determines how many seconds to wait between checking for changes. 103 * 104 * @param \Closure $handler An anonymous function to pass results to. 105 * @param int $checkInterval How often to check for changes (in seconds). 106 * @throws OperationException 107 */ 108 public function watch(Closure $handler, int $checkInterval = 10): void 109 { 110 $handler($this->getChanges(), true); 111 while ($this->hasChanges()) { 112 $handler($this->getChanges(), true); 113 } 114 115 while (true) { 116 sleep($checkInterval); 117 $entries = $this->getChanges(); 118 if ($entries->count() === 0) { 119 continue; 120 } 121 $handler($entries, false); 122 while ($this->hasChanges()) { 123 $handler($this->getChanges(), false); 124 } 125 } 126 } 127 128 /** 129 * Check whether or not there are more changes to receive. 130 * 131 * @return bool 132 */ 133 public function hasChanges(): bool 134 { 135 if ($this->lastResponse === null) { 136 return false; 137 } 138 139 return $this->lastResponse->hasMoreResults(); 140 } 141 142 /** 143 * Get the changes as entries. This may be empty if there are no changes since the last query. This should be 144 * followed with a hasChanges() call to determine if more changes are still available. 145 * 146 * @return Entries 147 * @throws OperationException 148 */ 149 public function getChanges(): Entries 150 { 151 /** @var LdapMessageResponse $response */ 152 $response = $this->client->send($this->getSearchRequest(), $this->getDirSyncControl()); 153 $lastResponse = $response->controls()->get(Control::OID_DIR_SYNC); 154 if ($lastResponse === null || !$lastResponse instanceof DirSyncResponseControl) { 155 throw new RuntimeException('Expected a DirSync control in the response, but none was received.'); 156 } 157 $this->lastResponse = $lastResponse; 158 $this->dirSyncRequest->setCookie($this->lastResponse->getCookie()); 159 /** @var SearchResponse $searchResponse */ 160 $searchResponse = $response->getResponse(); 161 162 return $searchResponse->getEntries(); 163 } 164 165 /** 166 * The attributes to return from the DirSync search. 167 * 168 * @param mixed ...$attributes 169 * @return DirSync 170 */ 171 public function selectAttributes(...$attributes) 172 { 173 $this->search->select(...$attributes); 174 175 return $this; 176 } 177 178 /** 179 * A specific DirSync cookie to use. For example, this could be a cookie from a previous DirSync request, assuming 180 * the server still thinks it's valid. 181 * 182 * @param string $cookie 183 * @return $this 184 */ 185 public function useCookie(string $cookie) 186 { 187 $this->dirSyncRequest->setCookie($cookie); 188 189 return $this; 190 } 191 192 /** 193 * The naming context to run the DirSync against. This MUST be a root naming context. 194 * 195 * @param string|null $namingContext 196 * @return $this 197 */ 198 public function useNamingContext(?string $namingContext) 199 { 200 $this->namingContext = $namingContext; 201 202 return $this; 203 } 204 205 /** 206 * The LDAP filter to limit the results to. 207 * 208 * @param FilterInterface $filter 209 * @return $this 210 */ 211 public function useFilter(FilterInterface $filter) 212 { 213 $this->search->setFilter($filter); 214 215 return $this; 216 } 217 218 /** 219 * Whether or not to return only incremental changes on a multivalued attribute that has changed. 220 * 221 * @param bool $incrementalValues 222 * @return $this 223 */ 224 public function useIncrementalValues(bool $incrementalValues = true) 225 { 226 $this->incrementalValues = $incrementalValues; 227 228 return $this; 229 } 230 231 /** 232 * Whether or not to only retrieve objects and attributes that are accessible to the client. 233 * 234 * @param bool $objectSecurity 235 * @return $this 236 */ 237 public function useObjectSecurity(bool $objectSecurity = true) 238 { 239 $this->objectSecurity = $objectSecurity; 240 241 return $this; 242 } 243 244 /** 245 * Whether or not the server should return parent objects before child objects. 246 * 247 * @param bool $ancestorFirstOrder 248 * @return $this 249 */ 250 public function useAncestorFirstOrder(bool $ancestorFirstOrder = true) 251 { 252 $this->ancestorFirstOrder = $ancestorFirstOrder; 253 254 return $this; 255 } 256 257 /** 258 * Get the cookie currently in use. 259 * 260 * @return string 261 */ 262 public function getCookie(): string 263 { 264 return $this->dirSyncRequest->getCookie(); 265 } 266 267 /** 268 * @return SearchRequest 269 * @throws OperationException 270 */ 271 protected function getSearchRequest(): SearchRequest 272 { 273 $this->search->base($this->namingContext ?? $this->getDefaultRootNc()); 274 275 return $this->search; 276 } 277 278 /** 279 * @return DirSyncRequestControl 280 */ 281 protected function getDirSyncControl(): DirSyncRequestControl 282 { 283 $flags = 0; 284 if ($this->incrementalValues) { 285 $flags |= DirSyncRequestControl::FLAG_INCREMENTAL_VALUES; 286 } 287 if ($this->ancestorFirstOrder) { 288 $flags |= DirSyncRequestControl::FLAG_ANCESTORS_FIRST_ORDER; 289 } 290 if ($this->objectSecurity) { 291 $flags |= DirSyncRequestControl::FLAG_OBJECT_SECURITY; 292 } 293 $this->dirSyncRequest->setFlags($flags); 294 295 return $this->dirSyncRequest; 296 } 297 298 /** 299 * @return string 300 * @throws OperationException 301 */ 302 protected function getDefaultRootNc(): string 303 { 304 if ($this->defaultRootNc === null) { 305 $this->defaultRootNc = (string) $this->client->readOrFail('', ['defaultNamingContext'])->get('defaultNamingContext'); 306 } 307 if ($this->defaultRootNc === '') { 308 throw new RuntimeException('Unable to determine the root naming context automatically.'); 309 } 310 311 return $this->defaultRootNc; 312 } 313} 314