xref: /plugin/pureldap/vendor/freedsx/ldap/src/FreeDSx/Ldap/Search/DirSync.php (revision dad993c57a70866aa1db59c43f043769c2eb7ed0)
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