<?php /** * This file is part of the FreeDSx LDAP package. * * (c) Chad Sikorra <Chad.Sikorra@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace FreeDSx\Ldap\Server\Paging; use FreeDSx\Ldap\Control\Control; use FreeDSx\Ldap\Control\ControlBag; use FreeDSx\Ldap\Entry\Attribute; use FreeDSx\Ldap\Protocol\LdapEncoder; /** * This determines "equality" of one paging request with another. * * Per RFC 2696: * * When the client wants to retrieve more entries for the result set, it MUST * send to the server a searchRequest with all values identical to the * initial request with the exception of the messageID, the cookie, and * optionally a modified pageSize. The cookie MUST be the octet string * on the last searchResultDone response returned by the server. * * @author Chad Sikorra <Chad.Sikorra@gmail.com> */ class PagingRequestComparator { /** * @var LdapEncoder */ private $encoder; public function __construct(LdapEncoder $encoder = null) { $this->encoder = $encoder ?? new LdapEncoder(); } /** * Compares the old paging request with the new request to determine if it is valid. * * @param PagingRequest $oldPagingRequest The previous paging request. * @param PagingRequest $newPagingRequest The paging request that was received. * @return bool */ public function compare( PagingRequest $oldPagingRequest, PagingRequest $newPagingRequest ): bool { if ($oldPagingRequest->getNextCookie() !== $newPagingRequest->getCookie()) { return false; } $oldSearch = $oldPagingRequest->getSearchRequest(); $newSearch = $newPagingRequest->getSearchRequest(); return $newPagingRequest->isCritical() === $oldPagingRequest->isCritical() && $oldSearch->getAttributesOnly() === $newSearch->getAttributesOnly() && $oldSearch->getDereferenceAliases() === $newSearch->getDereferenceAliases() && $oldSearch->getScope() === $newSearch->getScope() && $oldSearch->getTimeLimit() === $newSearch->getTimeLimit() && $oldSearch->getSizeLimit() === $newSearch->getSizeLimit() && (string)$oldSearch->getBaseDn() === (string)$newSearch->getBaseDn() && $oldSearch->getFilter()->toString() === $newSearch->getFilter()->toString() && $this->attributesMatch($oldSearch->getAttributes(), $newSearch->getAttributes()) && $this->controlsMatch($newPagingRequest->controls(), $oldPagingRequest->controls()); } /** * @param Attribute[] $oldAttrs * @param Attribute[] $newAttrs * @return bool */ private function attributesMatch( array $oldAttrs, array $newAttrs ): bool { if (count($oldAttrs) !== count($newAttrs)) { return false; } // This works by removing each attribute from their respective arrays as a match is found. // By the end, each array should be empty. We check that below. foreach ($oldAttrs as $iN => $oldAttr) { foreach ($newAttrs as $iO => $newAttr) { if ($newAttr->equals($oldAttr)) { unset($newAttrs[$iO]); unset($oldAttrs[$iN]); continue 2; } } } return empty($newAttrs) && empty($oldAttrs); } /** * This is a somewhat crude way to determine that two sets of controls are "equal". It does the following: * * 1. Sorts the controls based on their type. * 2. Encodes the controls to their string value. * 3. Compares the collections encoded value to see if they match. * * If the encoded values of the two collections are the same, then they are considered "equal". * * @param ControlBag $newControls * @param ControlBag $oldControls * @return bool */ private function controlsMatch( ControlBag $newControls, ControlBag $oldControls ): bool { if ($oldControls->count() !== $newControls->count()) { return false; } // Short circuit this...nothing to check. Only a paging control. if (empty($oldControls) && empty($newControls)) { return true; } $oldControls = $oldControls->toArray(); $newControls = $newControls->toArray(); // Sort both arrays, so they are ordered the same, then encode to a string and compare. usort($oldControls, function (Control $a, Control $b) { return $a->getTypeOid() <=> $b->getTypeOid(); }); usort($newControls, function (Control $a, Control $b) { return $a->getTypeOid() <=> $b->getTypeOid(); }); $oldEncoded = array_map(function (Control $control) { return $this->encoder->encode($control->toAsn1()); }, $oldControls); $newEncoded = array_map(function (Control $control) { return $this->encoder->encode($control->toAsn1()); }, $newControls); return implode('', $oldEncoded) === implode('', $newEncoded); } }