10b3fd2d3SAndreas Gohr<?php 2*dad993c5SAndreas Gohr 30b3fd2d3SAndreas Gohr/** 40b3fd2d3SAndreas Gohr * This file is part of the FreeDSx LDAP package. 50b3fd2d3SAndreas Gohr * 60b3fd2d3SAndreas Gohr * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 70b3fd2d3SAndreas Gohr * 80b3fd2d3SAndreas Gohr * For the full copyright and license information, please view the LICENSE 90b3fd2d3SAndreas Gohr * file that was distributed with this source code. 100b3fd2d3SAndreas Gohr */ 110b3fd2d3SAndreas Gohr 120b3fd2d3SAndreas Gohrnamespace FreeDSx\Ldap\Search; 130b3fd2d3SAndreas Gohr 14*dad993c5SAndreas Gohruse Closure; 150b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Control\Ad\DirSyncRequestControl; 160b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Control\Ad\DirSyncResponseControl; 170b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Control\Control; 180b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Controls; 190b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Entry\Entries; 20*dad993c5SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException; 210b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Exception\RuntimeException; 220b3fd2d3SAndreas Gohruse FreeDSx\Ldap\LdapClient; 230b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Operation\Request\SearchRequest; 240b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Operation\Response\SearchResponse; 250b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Protocol\LdapMessageResponse; 260b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Search\Filter\FilterInterface; 270b3fd2d3SAndreas Gohr 280b3fd2d3SAndreas Gohr/** 290b3fd2d3SAndreas Gohr * Provides a simple wrapper around DirSync for Active Directory. 300b3fd2d3SAndreas Gohr * 310b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com> 320b3fd2d3SAndreas Gohr */ 330b3fd2d3SAndreas Gohrclass DirSync 340b3fd2d3SAndreas Gohr{ 350b3fd2d3SAndreas Gohr /** 360b3fd2d3SAndreas Gohr * @var DirSyncResponseControl|null 370b3fd2d3SAndreas Gohr */ 380b3fd2d3SAndreas Gohr protected $lastResponse; 390b3fd2d3SAndreas Gohr 400b3fd2d3SAndreas Gohr /** 410b3fd2d3SAndreas Gohr * @var SearchRequest 420b3fd2d3SAndreas Gohr */ 430b3fd2d3SAndreas Gohr protected $search; 440b3fd2d3SAndreas Gohr 450b3fd2d3SAndreas Gohr /** 460b3fd2d3SAndreas Gohr * @var string|null 470b3fd2d3SAndreas Gohr */ 480b3fd2d3SAndreas Gohr protected $namingContext; 490b3fd2d3SAndreas Gohr 500b3fd2d3SAndreas Gohr /** 510b3fd2d3SAndreas Gohr * @var bool 520b3fd2d3SAndreas Gohr */ 530b3fd2d3SAndreas Gohr protected $incrementalValues = true; 540b3fd2d3SAndreas Gohr 550b3fd2d3SAndreas Gohr /** 560b3fd2d3SAndreas Gohr * @var bool 570b3fd2d3SAndreas Gohr */ 580b3fd2d3SAndreas Gohr protected $objectSecurity = false; 590b3fd2d3SAndreas Gohr 600b3fd2d3SAndreas Gohr /** 610b3fd2d3SAndreas Gohr * @var bool 620b3fd2d3SAndreas Gohr */ 630b3fd2d3SAndreas Gohr protected $ancestorFirstOrder = false; 640b3fd2d3SAndreas Gohr 650b3fd2d3SAndreas Gohr /** 660b3fd2d3SAndreas Gohr * @var null|string 670b3fd2d3SAndreas Gohr */ 680b3fd2d3SAndreas Gohr protected $defaultRootNc; 690b3fd2d3SAndreas Gohr 700b3fd2d3SAndreas Gohr /** 710b3fd2d3SAndreas Gohr * @var LdapClient 720b3fd2d3SAndreas Gohr */ 730b3fd2d3SAndreas Gohr protected $client; 740b3fd2d3SAndreas Gohr 750b3fd2d3SAndreas Gohr /** 760b3fd2d3SAndreas Gohr * @var DirSyncRequestControl 770b3fd2d3SAndreas Gohr */ 780b3fd2d3SAndreas Gohr protected $dirSyncRequest; 790b3fd2d3SAndreas Gohr 800b3fd2d3SAndreas Gohr /** 810b3fd2d3SAndreas Gohr * @param LdapClient $client 820b3fd2d3SAndreas Gohr * @param string|null $namingContext 830b3fd2d3SAndreas Gohr * @param FilterInterface|null $filter 840b3fd2d3SAndreas Gohr * @param mixed ...$attributes 850b3fd2d3SAndreas Gohr */ 860b3fd2d3SAndreas Gohr public function __construct(LdapClient $client, ?string $namingContext = null, ?FilterInterface $filter = null, ...$attributes) 870b3fd2d3SAndreas Gohr { 880b3fd2d3SAndreas Gohr $this->client = $client; 890b3fd2d3SAndreas Gohr $this->namingContext = $namingContext; 900b3fd2d3SAndreas Gohr $this->dirSyncRequest = Controls::dirSync(); 910b3fd2d3SAndreas Gohr $this->search = (new SearchRequest($filter ?? Filters::present('objectClass'), ...$attributes)); 920b3fd2d3SAndreas Gohr } 930b3fd2d3SAndreas Gohr 940b3fd2d3SAndreas Gohr /** 950b3fd2d3SAndreas Gohr * A convenience method to easily watch for changes with an anonymous function. The anonymous function will be passed 960b3fd2d3SAndreas Gohr * two arguments: 970b3fd2d3SAndreas Gohr * 980b3fd2d3SAndreas Gohr * - The Entries object containing the changes. 990b3fd2d3SAndreas Gohr * - A boolean value indicating whether or not the entries are part of the initial sync (the initial sync returns 1000b3fd2d3SAndreas Gohr * all entries matching the filter). 1010b3fd2d3SAndreas Gohr * 1020b3fd2d3SAndreas Gohr * An optional second argument then determines how many seconds to wait between checking for changes. 1030b3fd2d3SAndreas Gohr * 1040b3fd2d3SAndreas Gohr * @param \Closure $handler An anonymous function to pass results to. 1050b3fd2d3SAndreas Gohr * @param int $checkInterval How often to check for changes (in seconds). 106*dad993c5SAndreas Gohr * @throws OperationException 1070b3fd2d3SAndreas Gohr */ 108*dad993c5SAndreas Gohr public function watch(Closure $handler, int $checkInterval = 10): void 1090b3fd2d3SAndreas Gohr { 1100b3fd2d3SAndreas Gohr $handler($this->getChanges(), true); 1110b3fd2d3SAndreas Gohr while ($this->hasChanges()) { 1120b3fd2d3SAndreas Gohr $handler($this->getChanges(), true); 1130b3fd2d3SAndreas Gohr } 1140b3fd2d3SAndreas Gohr 1150b3fd2d3SAndreas Gohr while (true) { 1160b3fd2d3SAndreas Gohr sleep($checkInterval); 1170b3fd2d3SAndreas Gohr $entries = $this->getChanges(); 1180b3fd2d3SAndreas Gohr if ($entries->count() === 0) { 1190b3fd2d3SAndreas Gohr continue; 1200b3fd2d3SAndreas Gohr } 1210b3fd2d3SAndreas Gohr $handler($entries, false); 1220b3fd2d3SAndreas Gohr while ($this->hasChanges()) { 1230b3fd2d3SAndreas Gohr $handler($this->getChanges(), false); 1240b3fd2d3SAndreas Gohr } 1250b3fd2d3SAndreas Gohr } 1260b3fd2d3SAndreas Gohr } 1270b3fd2d3SAndreas Gohr 1280b3fd2d3SAndreas Gohr /** 1290b3fd2d3SAndreas Gohr * Check whether or not there are more changes to receive. 1300b3fd2d3SAndreas Gohr * 1310b3fd2d3SAndreas Gohr * @return bool 1320b3fd2d3SAndreas Gohr */ 1330b3fd2d3SAndreas Gohr public function hasChanges(): bool 1340b3fd2d3SAndreas Gohr { 1350b3fd2d3SAndreas Gohr if ($this->lastResponse === null) { 1360b3fd2d3SAndreas Gohr return false; 1370b3fd2d3SAndreas Gohr } 1380b3fd2d3SAndreas Gohr 1390b3fd2d3SAndreas Gohr return $this->lastResponse->hasMoreResults(); 1400b3fd2d3SAndreas Gohr } 1410b3fd2d3SAndreas Gohr 1420b3fd2d3SAndreas Gohr /** 1430b3fd2d3SAndreas Gohr * Get the changes as entries. This may be empty if there are no changes since the last query. This should be 1440b3fd2d3SAndreas Gohr * followed with a hasChanges() call to determine if more changes are still available. 1450b3fd2d3SAndreas Gohr * 1460b3fd2d3SAndreas Gohr * @return Entries 147*dad993c5SAndreas Gohr * @throws OperationException 1480b3fd2d3SAndreas Gohr */ 1490b3fd2d3SAndreas Gohr public function getChanges(): Entries 1500b3fd2d3SAndreas Gohr { 1510b3fd2d3SAndreas Gohr /** @var LdapMessageResponse $response */ 1520b3fd2d3SAndreas Gohr $response = $this->client->send($this->getSearchRequest(), $this->getDirSyncControl()); 1530b3fd2d3SAndreas Gohr $lastResponse = $response->controls()->get(Control::OID_DIR_SYNC); 1540b3fd2d3SAndreas Gohr if ($lastResponse === null || !$lastResponse instanceof DirSyncResponseControl) { 1550b3fd2d3SAndreas Gohr throw new RuntimeException('Expected a DirSync control in the response, but none was received.'); 1560b3fd2d3SAndreas Gohr } 1570b3fd2d3SAndreas Gohr $this->lastResponse = $lastResponse; 1580b3fd2d3SAndreas Gohr $this->dirSyncRequest->setCookie($this->lastResponse->getCookie()); 1590b3fd2d3SAndreas Gohr /** @var SearchResponse $searchResponse */ 1600b3fd2d3SAndreas Gohr $searchResponse = $response->getResponse(); 1610b3fd2d3SAndreas Gohr 1620b3fd2d3SAndreas Gohr return $searchResponse->getEntries(); 1630b3fd2d3SAndreas Gohr } 1640b3fd2d3SAndreas Gohr 1650b3fd2d3SAndreas Gohr /** 1660b3fd2d3SAndreas Gohr * The attributes to return from the DirSync search. 1670b3fd2d3SAndreas Gohr * 1680b3fd2d3SAndreas Gohr * @param mixed ...$attributes 1690b3fd2d3SAndreas Gohr * @return DirSync 1700b3fd2d3SAndreas Gohr */ 1710b3fd2d3SAndreas Gohr public function selectAttributes(...$attributes) 1720b3fd2d3SAndreas Gohr { 1730b3fd2d3SAndreas Gohr $this->search->select(...$attributes); 1740b3fd2d3SAndreas Gohr 1750b3fd2d3SAndreas Gohr return $this; 1760b3fd2d3SAndreas Gohr } 1770b3fd2d3SAndreas Gohr 1780b3fd2d3SAndreas Gohr /** 1790b3fd2d3SAndreas Gohr * A specific DirSync cookie to use. For example, this could be a cookie from a previous DirSync request, assuming 1800b3fd2d3SAndreas Gohr * the server still thinks it's valid. 1810b3fd2d3SAndreas Gohr * 1820b3fd2d3SAndreas Gohr * @param string $cookie 1830b3fd2d3SAndreas Gohr * @return $this 1840b3fd2d3SAndreas Gohr */ 1850b3fd2d3SAndreas Gohr public function useCookie(string $cookie) 1860b3fd2d3SAndreas Gohr { 1870b3fd2d3SAndreas Gohr $this->dirSyncRequest->setCookie($cookie); 1880b3fd2d3SAndreas Gohr 1890b3fd2d3SAndreas Gohr return $this; 1900b3fd2d3SAndreas Gohr } 1910b3fd2d3SAndreas Gohr 1920b3fd2d3SAndreas Gohr /** 1930b3fd2d3SAndreas Gohr * The naming context to run the DirSync against. This MUST be a root naming context. 1940b3fd2d3SAndreas Gohr * 1950b3fd2d3SAndreas Gohr * @param string|null $namingContext 1960b3fd2d3SAndreas Gohr * @return $this 1970b3fd2d3SAndreas Gohr */ 1980b3fd2d3SAndreas Gohr public function useNamingContext(?string $namingContext) 1990b3fd2d3SAndreas Gohr { 2000b3fd2d3SAndreas Gohr $this->namingContext = $namingContext; 2010b3fd2d3SAndreas Gohr 2020b3fd2d3SAndreas Gohr return $this; 2030b3fd2d3SAndreas Gohr } 2040b3fd2d3SAndreas Gohr 2050b3fd2d3SAndreas Gohr /** 2060b3fd2d3SAndreas Gohr * The LDAP filter to limit the results to. 2070b3fd2d3SAndreas Gohr * 2080b3fd2d3SAndreas Gohr * @param FilterInterface $filter 2090b3fd2d3SAndreas Gohr * @return $this 2100b3fd2d3SAndreas Gohr */ 2110b3fd2d3SAndreas Gohr public function useFilter(FilterInterface $filter) 2120b3fd2d3SAndreas Gohr { 2130b3fd2d3SAndreas Gohr $this->search->setFilter($filter); 2140b3fd2d3SAndreas Gohr 2150b3fd2d3SAndreas Gohr return $this; 2160b3fd2d3SAndreas Gohr } 2170b3fd2d3SAndreas Gohr 2180b3fd2d3SAndreas Gohr /** 2190b3fd2d3SAndreas Gohr * Whether or not to return only incremental changes on a multivalued attribute that has changed. 2200b3fd2d3SAndreas Gohr * 2210b3fd2d3SAndreas Gohr * @param bool $incrementalValues 2220b3fd2d3SAndreas Gohr * @return $this 2230b3fd2d3SAndreas Gohr */ 2240b3fd2d3SAndreas Gohr public function useIncrementalValues(bool $incrementalValues = true) 2250b3fd2d3SAndreas Gohr { 2260b3fd2d3SAndreas Gohr $this->incrementalValues = $incrementalValues; 2270b3fd2d3SAndreas Gohr 2280b3fd2d3SAndreas Gohr return $this; 2290b3fd2d3SAndreas Gohr } 2300b3fd2d3SAndreas Gohr 2310b3fd2d3SAndreas Gohr /** 2320b3fd2d3SAndreas Gohr * Whether or not to only retrieve objects and attributes that are accessible to the client. 2330b3fd2d3SAndreas Gohr * 2340b3fd2d3SAndreas Gohr * @param bool $objectSecurity 2350b3fd2d3SAndreas Gohr * @return $this 2360b3fd2d3SAndreas Gohr */ 2370b3fd2d3SAndreas Gohr public function useObjectSecurity(bool $objectSecurity = true) 2380b3fd2d3SAndreas Gohr { 2390b3fd2d3SAndreas Gohr $this->objectSecurity = $objectSecurity; 2400b3fd2d3SAndreas Gohr 2410b3fd2d3SAndreas Gohr return $this; 2420b3fd2d3SAndreas Gohr } 2430b3fd2d3SAndreas Gohr 2440b3fd2d3SAndreas Gohr /** 2450b3fd2d3SAndreas Gohr * Whether or not the server should return parent objects before child objects. 2460b3fd2d3SAndreas Gohr * 2470b3fd2d3SAndreas Gohr * @param bool $ancestorFirstOrder 2480b3fd2d3SAndreas Gohr * @return $this 2490b3fd2d3SAndreas Gohr */ 2500b3fd2d3SAndreas Gohr public function useAncestorFirstOrder(bool $ancestorFirstOrder = true) 2510b3fd2d3SAndreas Gohr { 2520b3fd2d3SAndreas Gohr $this->ancestorFirstOrder = $ancestorFirstOrder; 2530b3fd2d3SAndreas Gohr 2540b3fd2d3SAndreas Gohr return $this; 2550b3fd2d3SAndreas Gohr } 2560b3fd2d3SAndreas Gohr 2570b3fd2d3SAndreas Gohr /** 2580b3fd2d3SAndreas Gohr * Get the cookie currently in use. 2590b3fd2d3SAndreas Gohr * 2600b3fd2d3SAndreas Gohr * @return string 2610b3fd2d3SAndreas Gohr */ 2620b3fd2d3SAndreas Gohr public function getCookie(): string 2630b3fd2d3SAndreas Gohr { 2640b3fd2d3SAndreas Gohr return $this->dirSyncRequest->getCookie(); 2650b3fd2d3SAndreas Gohr } 2660b3fd2d3SAndreas Gohr 2670b3fd2d3SAndreas Gohr /** 2680b3fd2d3SAndreas Gohr * @return SearchRequest 269*dad993c5SAndreas Gohr * @throws OperationException 2700b3fd2d3SAndreas Gohr */ 2710b3fd2d3SAndreas Gohr protected function getSearchRequest(): SearchRequest 2720b3fd2d3SAndreas Gohr { 2730b3fd2d3SAndreas Gohr $this->search->base($this->namingContext ?? $this->getDefaultRootNc()); 2740b3fd2d3SAndreas Gohr 2750b3fd2d3SAndreas Gohr return $this->search; 2760b3fd2d3SAndreas Gohr } 2770b3fd2d3SAndreas Gohr 2780b3fd2d3SAndreas Gohr /** 2790b3fd2d3SAndreas Gohr * @return DirSyncRequestControl 2800b3fd2d3SAndreas Gohr */ 2810b3fd2d3SAndreas Gohr protected function getDirSyncControl(): DirSyncRequestControl 2820b3fd2d3SAndreas Gohr { 2830b3fd2d3SAndreas Gohr $flags = 0; 2840b3fd2d3SAndreas Gohr if ($this->incrementalValues) { 2850b3fd2d3SAndreas Gohr $flags |= DirSyncRequestControl::FLAG_INCREMENTAL_VALUES; 2860b3fd2d3SAndreas Gohr } 2870b3fd2d3SAndreas Gohr if ($this->ancestorFirstOrder) { 2880b3fd2d3SAndreas Gohr $flags |= DirSyncRequestControl::FLAG_ANCESTORS_FIRST_ORDER; 2890b3fd2d3SAndreas Gohr } 2900b3fd2d3SAndreas Gohr if ($this->objectSecurity) { 2910b3fd2d3SAndreas Gohr $flags |= DirSyncRequestControl::FLAG_OBJECT_SECURITY; 2920b3fd2d3SAndreas Gohr } 2930b3fd2d3SAndreas Gohr $this->dirSyncRequest->setFlags($flags); 2940b3fd2d3SAndreas Gohr 2950b3fd2d3SAndreas Gohr return $this->dirSyncRequest; 2960b3fd2d3SAndreas Gohr } 2970b3fd2d3SAndreas Gohr 2980b3fd2d3SAndreas Gohr /** 299*dad993c5SAndreas Gohr * @return string 300*dad993c5SAndreas Gohr * @throws OperationException 3010b3fd2d3SAndreas Gohr */ 302*dad993c5SAndreas Gohr protected function getDefaultRootNc(): string 3030b3fd2d3SAndreas Gohr { 3040b3fd2d3SAndreas Gohr if ($this->defaultRootNc === null) { 3050b3fd2d3SAndreas Gohr $this->defaultRootNc = (string) $this->client->readOrFail('', ['defaultNamingContext'])->get('defaultNamingContext'); 3060b3fd2d3SAndreas Gohr } 3070b3fd2d3SAndreas Gohr if ($this->defaultRootNc === '') { 3080b3fd2d3SAndreas Gohr throw new RuntimeException('Unable to determine the root naming context automatically.'); 3090b3fd2d3SAndreas Gohr } 3100b3fd2d3SAndreas Gohr 3110b3fd2d3SAndreas Gohr return $this->defaultRootNc; 3120b3fd2d3SAndreas Gohr } 3130b3fd2d3SAndreas Gohr} 314