-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Ldap] Implement pagination #29495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Ldap] Implement pagination #29495
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,31 +44,40 @@ public function toArray() | |
|
||
public function count() | ||
{ | ||
if (false !== $count = ldap_count_entries($this->connection->getResource(), $this->search->getResource())) { | ||
return $count; | ||
$con = $this->connection->getResource(); | ||
$searches = $this->search->getResources(); | ||
$count = 0; | ||
foreach ($searches as $search) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we keep the old way of counting without pagination to avoid n requests? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm... sure, but currently without pagination you should only have the single set of results and thus one trip to through the loop and one request. I hadn't done anything special previously because I figured the PHP overhead was fairly minimal. |
||
$searchCount = ldap_count_entries($con, $search); | ||
if (false === $searchCount) { | ||
throw new LdapException(sprintf('Error while retrieving entry count: %s.', ldap_error($con))); | ||
} | ||
$count += $searchCount; | ||
} | ||
|
||
throw new LdapException(sprintf('Error while retrieving entry count: %s.', ldap_error($this->connection->getResource()))); | ||
return $count; | ||
} | ||
|
||
public function getIterator() | ||
{ | ||
$con = $this->connection->getResource(); | ||
$search = $this->search->getResource(); | ||
$current = ldap_first_entry($con, $search); | ||
|
||
if (0 === $this->count()) { | ||
return; | ||
} | ||
|
||
if (false === $current) { | ||
throw new LdapException(sprintf('Could not rewind entries array: %s.', ldap_error($con))); | ||
} | ||
$con = $this->connection->getResource(); | ||
$searches = $this->search->getResources(); | ||
foreach ($searches as $search) { | ||
kevans91 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$current = ldap_first_entry($con, $search); | ||
|
||
yield $this->getSingleEntry($con, $current); | ||
if (false === $current) { | ||
throw new LdapException(sprintf('Could not rewind entries array: %s.', ldap_error($con))); | ||
} | ||
|
||
while (false !== $current = ldap_next_entry($con, $current)) { | ||
yield $this->getSingleEntry($con, $current); | ||
|
||
while (false !== $current = ldap_next_entry($con, $current)) { | ||
yield $this->getSingleEntry($con, $current); | ||
} | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,11 +21,14 @@ | |
*/ | ||
class Query extends AbstractQuery | ||
{ | ||
// As of PHP 7.2, we can use LDAP_CONTROL_PAGEDRESULTS instead of this | ||
fabpot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const PAGINATION_OID = '1.2.840.113556.1.4.319'; | ||
|
||
/** @var Connection */ | ||
protected $connection; | ||
|
||
/** @var resource */ | ||
private $search; | ||
/** @var resource[] */ | ||
private $results; | ||
|
||
public function __construct(Connection $connection, string $dn, string $query, array $options = []) | ||
{ | ||
|
@@ -37,29 +40,33 @@ public function __destruct() | |
$con = $this->connection->getResource(); | ||
$this->connection = null; | ||
|
||
if (null === $this->search || false === $this->search) { | ||
if (null === $this->results) { | ||
return; | ||
} | ||
|
||
$success = ldap_free_result($this->search); | ||
$this->search = null; | ||
|
||
if (!$success) { | ||
throw new LdapException(sprintf('Could not free results: %s.', ldap_error($con))); | ||
foreach ($this->results as $result) { | ||
if (false === $result || null === $result) { | ||
continue; | ||
} | ||
if (!ldap_free_result($result)) { | ||
throw new LdapException(sprintf('Could not free results: %s.', ldap_error($con))); | ||
} | ||
} | ||
$this->results = null; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function execute() | ||
{ | ||
if (null === $this->search) { | ||
if (null === $this->results) { | ||
// If the connection is not bound, throw an exception. Users should use an explicit bind call first. | ||
if (!$this->connection->isBound()) { | ||
throw new NotBoundException('Query execution is not possible without binding the connection first.'); | ||
} | ||
|
||
$this->results = []; | ||
$con = $this->connection->getResource(); | ||
|
||
switch ($this->options['scope']) { | ||
|
@@ -76,39 +83,126 @@ public function execute() | |
throw new LdapException(sprintf('Could not search in scope "%s".', $this->options['scope'])); | ||
} | ||
|
||
$this->search = @$func( | ||
$con, | ||
$this->dn, | ||
$this->query, | ||
$this->options['filter'], | ||
$this->options['attrsOnly'], | ||
$this->options['maxItems'], | ||
$this->options['timeout'], | ||
$this->options['deref'] | ||
); | ||
} | ||
|
||
if (false === $this->search) { | ||
$ldapError = ''; | ||
if ($errno = ldap_errno($con)) { | ||
$ldapError = sprintf(' LDAP error was [%d] %s', $errno, ldap_error($con)); | ||
$itemsLeft = $maxItems = $this->options['maxItems']; | ||
$pageSize = $this->options['pageSize']; | ||
// Deal with the logic to handle maxItems properly. If we can satisfy it in | ||
// one request based on pageSize, we don't need to bother sending page control | ||
// to the server so that it can determine what we already know. | ||
if (0 !== $maxItems && $pageSize > $maxItems) { | ||
$pageSize = 0; | ||
} elseif (0 !== $maxItems) { | ||
$pageSize = min($maxItems, $pageSize); | ||
} | ||
$pageControl = $this->options['scope'] != static::SCOPE_BASE && $pageSize > 0; | ||
$cookie = ''; | ||
do { | ||
if ($pageControl) { | ||
ldap_control_paged_result($con, $pageSize, true, $cookie); | ||
} | ||
$sizeLimit = $itemsLeft; | ||
if ($pageSize > 0 && $sizeLimit >= $pageSize) { | ||
$sizeLimit = 0; | ||
} | ||
$search = @$func( | ||
$con, | ||
$this->dn, | ||
$this->query, | ||
$this->options['filter'], | ||
$this->options['attrsOnly'], | ||
$sizeLimit, | ||
$this->options['timeout'], | ||
$this->options['deref'] | ||
); | ||
|
||
if (false === $search) { | ||
$ldapError = ''; | ||
if ($errno = ldap_errno($con)) { | ||
$ldapError = sprintf(' LDAP error was [%d] %s', $errno, ldap_error($con)); | ||
} | ||
if ($pageControl) { | ||
$this->resetPagination(); | ||
} | ||
|
||
throw new LdapException(sprintf('Could not complete search with dn "%s", query "%s" and filters "%s".%s', $this->dn, $this->query, implode(',', $this->options['filter']), $ldapError)); | ||
} | ||
|
||
$this->results[] = $search; | ||
$itemsLeft -= min($itemsLeft, $pageSize); | ||
|
||
if (0 !== $maxItems && 0 === $itemsLeft) { | ||
break; | ||
} | ||
if ($pageControl) { | ||
ldap_control_paged_result_response($con, $search, $cookie); | ||
} | ||
} while (null !== $cookie && '' !== $cookie); | ||
|
||
if ($pageControl) { | ||
$this->resetPagination(); | ||
} | ||
|
||
throw new LdapException(sprintf('Could not complete search with dn "%s", query "%s" and filters "%s".%s', $this->dn, $this->query, implode(',', $this->options['filter']), $ldapError)); | ||
} | ||
|
||
return new Collection($this->connection, $this); | ||
} | ||
|
||
/** | ||
* Returns a LDAP search resource. | ||
* Returns a LDAP search resource. If this query resulted in multiple searches, only the first | ||
* page will be returned. | ||
* | ||
* @return resource | ||
* | ||
* @internal | ||
*/ | ||
public function getResource() | ||
public function getResource($idx = 0) | ||
{ | ||
return $this->search; | ||
if (null === $this->results || $idx >= \count($this->results)) { | ||
return null; | ||
} | ||
|
||
return $this->results[$idx]; | ||
} | ||
|
||
/** | ||
* Returns all LDAP search resources. | ||
* | ||
* @return resource[] | ||
* | ||
* @internal | ||
*/ | ||
public function getResources() | ||
{ | ||
return $this->results; | ||
} | ||
|
||
/** | ||
* Resets pagination on the current connection. | ||
* | ||
* @internal | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not needed as the method is private anyway. |
||
*/ | ||
private function resetPagination() | ||
{ | ||
$con = $this->connection->getResource(); | ||
ldap_control_paged_result($con, 0); | ||
|
||
// This is a workaround for a bit of a bug in the above invocation | ||
// of ldap_control_paged_result. Instead of indicating to extldap that | ||
// we no longer wish to page queries on this link, this invocation sets | ||
// the LDAP_CONTROL_PAGEDRESULTS OID with a page size of 0. This isn't | ||
// well defined by RFC 2696 if there is no cookie present, so some servers | ||
// will interpret it differently and do the wrong thing. Forcefully remove | ||
// the OID for now until a fix can make its way through the versions of PHP | ||
// the we support. | ||
// | ||
// This is not supported in PHP < 7.2, so these versions will remain broken. | ||
$ctl = []; | ||
ldap_get_option($con, LDAP_OPT_SERVER_CONTROLS, $ctl); | ||
if (!empty($ctl)) { | ||
foreach ($ctl as $idx => $info) { | ||
if (static::PAGINATION_OID == $info['oid']) { | ||
unset($ctl[$idx]); | ||
} | ||
} | ||
ldap_set_option($con, LDAP_OPT_SERVER_CONTROLS, $ctl); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.