* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace FreeDSx\Ldap\Search; use Closure; use FreeDSx\Ldap\Control\Ad\DirSyncRequestControl; use FreeDSx\Ldap\Control\Ad\DirSyncResponseControl; use FreeDSx\Ldap\Control\Control; use FreeDSx\Ldap\Controls; use FreeDSx\Ldap\Entry\Entries; use FreeDSx\Ldap\Exception\OperationException; use FreeDSx\Ldap\Exception\RuntimeException; use FreeDSx\Ldap\LdapClient; use FreeDSx\Ldap\Operation\Request\SearchRequest; use FreeDSx\Ldap\Operation\Response\SearchResponse; use FreeDSx\Ldap\Protocol\LdapMessageResponse; use FreeDSx\Ldap\Search\Filter\FilterInterface; /** * Provides a simple wrapper around DirSync for Active Directory. * * @author Chad Sikorra */ class DirSync { /** * @var DirSyncResponseControl|null */ protected $lastResponse; /** * @var SearchRequest */ protected $search; /** * @var string|null */ protected $namingContext; /** * @var bool */ protected $incrementalValues = true; /** * @var bool */ protected $objectSecurity = false; /** * @var bool */ protected $ancestorFirstOrder = false; /** * @var null|string */ protected $defaultRootNc; /** * @var LdapClient */ protected $client; /** * @var DirSyncRequestControl */ protected $dirSyncRequest; /** * @param LdapClient $client * @param string|null $namingContext * @param FilterInterface|null $filter * @param mixed ...$attributes */ public function __construct(LdapClient $client, ?string $namingContext = null, ?FilterInterface $filter = null, ...$attributes) { $this->client = $client; $this->namingContext = $namingContext; $this->dirSyncRequest = Controls::dirSync(); $this->search = (new SearchRequest($filter ?? Filters::present('objectClass'), ...$attributes)); } /** * A convenience method to easily watch for changes with an anonymous function. The anonymous function will be passed * two arguments: * * - The Entries object containing the changes. * - A boolean value indicating whether or not the entries are part of the initial sync (the initial sync returns * all entries matching the filter). * * An optional second argument then determines how many seconds to wait between checking for changes. * * @param \Closure $handler An anonymous function to pass results to. * @param int $checkInterval How often to check for changes (in seconds). * @throws OperationException */ public function watch(Closure $handler, int $checkInterval = 10): void { $handler($this->getChanges(), true); while ($this->hasChanges()) { $handler($this->getChanges(), true); } while (true) { sleep($checkInterval); $entries = $this->getChanges(); if ($entries->count() === 0) { continue; } $handler($entries, false); while ($this->hasChanges()) { $handler($this->getChanges(), false); } } } /** * Check whether or not there are more changes to receive. * * @return bool */ public function hasChanges(): bool { if ($this->lastResponse === null) { return false; } return $this->lastResponse->hasMoreResults(); } /** * Get the changes as entries. This may be empty if there are no changes since the last query. This should be * followed with a hasChanges() call to determine if more changes are still available. * * @return Entries * @throws OperationException */ public function getChanges(): Entries { /** @var LdapMessageResponse $response */ $response = $this->client->send($this->getSearchRequest(), $this->getDirSyncControl()); $lastResponse = $response->controls()->get(Control::OID_DIR_SYNC); if ($lastResponse === null || !$lastResponse instanceof DirSyncResponseControl) { throw new RuntimeException('Expected a DirSync control in the response, but none was received.'); } $this->lastResponse = $lastResponse; $this->dirSyncRequest->setCookie($this->lastResponse->getCookie()); /** @var SearchResponse $searchResponse */ $searchResponse = $response->getResponse(); return $searchResponse->getEntries(); } /** * The attributes to return from the DirSync search. * * @param mixed ...$attributes * @return DirSync */ public function selectAttributes(...$attributes) { $this->search->select(...$attributes); return $this; } /** * A specific DirSync cookie to use. For example, this could be a cookie from a previous DirSync request, assuming * the server still thinks it's valid. * * @param string $cookie * @return $this */ public function useCookie(string $cookie) { $this->dirSyncRequest->setCookie($cookie); return $this; } /** * The naming context to run the DirSync against. This MUST be a root naming context. * * @param string|null $namingContext * @return $this */ public function useNamingContext(?string $namingContext) { $this->namingContext = $namingContext; return $this; } /** * The LDAP filter to limit the results to. * * @param FilterInterface $filter * @return $this */ public function useFilter(FilterInterface $filter) { $this->search->setFilter($filter); return $this; } /** * Whether or not to return only incremental changes on a multivalued attribute that has changed. * * @param bool $incrementalValues * @return $this */ public function useIncrementalValues(bool $incrementalValues = true) { $this->incrementalValues = $incrementalValues; return $this; } /** * Whether or not to only retrieve objects and attributes that are accessible to the client. * * @param bool $objectSecurity * @return $this */ public function useObjectSecurity(bool $objectSecurity = true) { $this->objectSecurity = $objectSecurity; return $this; } /** * Whether or not the server should return parent objects before child objects. * * @param bool $ancestorFirstOrder * @return $this */ public function useAncestorFirstOrder(bool $ancestorFirstOrder = true) { $this->ancestorFirstOrder = $ancestorFirstOrder; return $this; } /** * Get the cookie currently in use. * * @return string */ public function getCookie(): string { return $this->dirSyncRequest->getCookie(); } /** * @return SearchRequest * @throws OperationException */ protected function getSearchRequest(): SearchRequest { $this->search->base($this->namingContext ?? $this->getDefaultRootNc()); return $this->search; } /** * @return DirSyncRequestControl */ protected function getDirSyncControl(): DirSyncRequestControl { $flags = 0; if ($this->incrementalValues) { $flags |= DirSyncRequestControl::FLAG_INCREMENTAL_VALUES; } if ($this->ancestorFirstOrder) { $flags |= DirSyncRequestControl::FLAG_ANCESTORS_FIRST_ORDER; } if ($this->objectSecurity) { $flags |= DirSyncRequestControl::FLAG_OBJECT_SECURITY; } $this->dirSyncRequest->setFlags($flags); return $this->dirSyncRequest; } /** * @return string * @throws OperationException */ protected function getDefaultRootNc(): string { if ($this->defaultRootNc === null) { $this->defaultRootNc = (string) $this->client->readOrFail('', ['defaultNamingContext'])->get('defaultNamingContext'); } if ($this->defaultRootNc === '') { throw new RuntimeException('Unable to determine the root naming context automatically.'); } return $this->defaultRootNc; } }