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