1<?php 2 3/** 4 * This file is part of the FreeDSx LDAP package. 5 * 6 * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace FreeDSx\Ldap\Server\Paging; 13 14use FreeDSx\Ldap\Control\Control; 15use FreeDSx\Ldap\Control\ControlBag; 16use FreeDSx\Ldap\Entry\Attribute; 17use FreeDSx\Ldap\Protocol\LdapEncoder; 18 19/** 20 * This determines "equality" of one paging request with another. 21 * 22 * Per RFC 2696: 23 * 24 * When the client wants to retrieve more entries for the result set, it MUST 25 * send to the server a searchRequest with all values identical to the 26 * initial request with the exception of the messageID, the cookie, and 27 * optionally a modified pageSize. The cookie MUST be the octet string 28 * on the last searchResultDone response returned by the server. 29 * 30 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 31 */ 32class PagingRequestComparator 33{ 34 /** 35 * @var LdapEncoder 36 */ 37 private $encoder; 38 39 public function __construct(LdapEncoder $encoder = null) 40 { 41 $this->encoder = $encoder ?? new LdapEncoder(); 42 } 43 44 /** 45 * Compares the old paging request with the new request to determine if it is valid. 46 * 47 * @param PagingRequest $oldPagingRequest The previous paging request. 48 * @param PagingRequest $newPagingRequest The paging request that was received. 49 * @return bool 50 */ 51 public function compare( 52 PagingRequest $oldPagingRequest, 53 PagingRequest $newPagingRequest 54 ): bool { 55 if ($oldPagingRequest->getNextCookie() !== $newPagingRequest->getCookie()) { 56 return false; 57 } 58 59 $oldSearch = $oldPagingRequest->getSearchRequest(); 60 $newSearch = $newPagingRequest->getSearchRequest(); 61 62 return $newPagingRequest->isCritical() === $oldPagingRequest->isCritical() 63 && $oldSearch->getAttributesOnly() === $newSearch->getAttributesOnly() 64 && $oldSearch->getDereferenceAliases() === $newSearch->getDereferenceAliases() 65 && $oldSearch->getScope() === $newSearch->getScope() 66 && $oldSearch->getTimeLimit() === $newSearch->getTimeLimit() 67 && $oldSearch->getSizeLimit() === $newSearch->getSizeLimit() 68 && (string)$oldSearch->getBaseDn() === (string)$newSearch->getBaseDn() 69 && $oldSearch->getFilter()->toString() === $newSearch->getFilter()->toString() 70 && $this->attributesMatch($oldSearch->getAttributes(), $newSearch->getAttributes()) 71 && $this->controlsMatch($newPagingRequest->controls(), $oldPagingRequest->controls()); 72 } 73 74 /** 75 * @param Attribute[] $oldAttrs 76 * @param Attribute[] $newAttrs 77 * @return bool 78 */ 79 private function attributesMatch( 80 array $oldAttrs, 81 array $newAttrs 82 ): bool { 83 if (count($oldAttrs) !== count($newAttrs)) { 84 return false; 85 } 86 87 // This works by removing each attribute from their respective arrays as a match is found. 88 // By the end, each array should be empty. We check that below. 89 foreach ($oldAttrs as $iN => $oldAttr) { 90 foreach ($newAttrs as $iO => $newAttr) { 91 if ($newAttr->equals($oldAttr)) { 92 unset($newAttrs[$iO]); 93 unset($oldAttrs[$iN]); 94 continue 2; 95 } 96 } 97 } 98 99 return empty($newAttrs) 100 && empty($oldAttrs); 101 } 102 103 /** 104 * This is a somewhat crude way to determine that two sets of controls are "equal". It does the following: 105 * 106 * 1. Sorts the controls based on their type. 107 * 2. Encodes the controls to their string value. 108 * 3. Compares the collections encoded value to see if they match. 109 * 110 * If the encoded values of the two collections are the same, then they are considered "equal". 111 * 112 * @param ControlBag $newControls 113 * @param ControlBag $oldControls 114 * @return bool 115 */ 116 private function controlsMatch( 117 ControlBag $newControls, 118 ControlBag $oldControls 119 ): bool { 120 if ($oldControls->count() !== $newControls->count()) { 121 return false; 122 } 123 124 // Short circuit this...nothing to check. Only a paging control. 125 if (empty($oldControls) && empty($newControls)) { 126 return true; 127 } 128 129 $oldControls = $oldControls->toArray(); 130 $newControls = $newControls->toArray(); 131 132 // Sort both arrays, so they are ordered the same, then encode to a string and compare. 133 usort($oldControls, function (Control $a, Control $b) { 134 return $a->getTypeOid() <=> $b->getTypeOid(); 135 }); 136 usort($newControls, function (Control $a, Control $b) { 137 return $a->getTypeOid() <=> $b->getTypeOid(); 138 }); 139 140 $oldEncoded = array_map(function (Control $control) { 141 return $this->encoder->encode($control->toAsn1()); 142 }, $oldControls); 143 $newEncoded = array_map(function (Control $control) { 144 return $this->encoder->encode($control->toAsn1()); 145 }, $newControls); 146 147 return implode('', $oldEncoded) === implode('', $newEncoded); 148 } 149} 150