Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[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

Merged
merged 1 commit into from
Mar 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ before_install:
set -e
stty cols 120
mkdir /tmp/slapd
if [ ! -e /tmp/slapd-modules ]; then
[ -d /usr/lib/openldap ] && ln -s /usr/lib/openldap /tmp/slapd-modules || ln -s /usr/lib/ldap /tmp/slapd-modules
fi
slapd -f src/Symfony/Component/Ldap/Tests/Fixtures/conf/slapd.conf -h ldap://localhost:3389 &
[ -d ~/.composer ] || mkdir ~/.composer
cp .composer/* ~/.composer/
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Ldap/Adapter/AbstractQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function __construct(ConnectionInterface $connection, string $dn, string
'deref' => static::DEREF_NEVER,
'attrsOnly' => 0,
'scope' => static::SCOPE_SUB,
'pageSize' => 0,
]);
$resolver->setAllowedValues('deref', [static::DEREF_ALWAYS, static::DEREF_NEVER, static::DEREF_FINDING, static::DEREF_SEARCHING]);
$resolver->setAllowedValues('scope', [static::SCOPE_BASE, static::SCOPE_ONE, static::SCOPE_SUB]);
Expand Down
33 changes: 21 additions & 12 deletions src/Symfony/Component/Ldap/Adapter/ExtLdap/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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) {
$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);
}
}
}

Expand Down
154 changes: 124 additions & 30 deletions src/Symfony/Component/Ldap/Adapter/ExtLdap/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@
*/
class Query extends AbstractQuery
{
// As of PHP 7.2, we can use LDAP_CONTROL_PAGEDRESULTS instead of this
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 = [])
{
Expand All @@ -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']) {
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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);
}
}
}
5 changes: 5 additions & 0 deletions src/Symfony/Component/Ldap/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

4.3.0
-----

* Added pagination support to the ExtLdap adapter with the pageSize query option.

4.2.0
-----

Expand Down
Loading