235 lines
7.7 KiB
PHP
235 lines
7.7 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Symfony package.
|
|
*
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
|
|
|
|
use LDAP\Connection as LDAPConnection;
|
|
use LDAP\Result;
|
|
use Symfony\Component\Ldap\Adapter\AbstractQuery;
|
|
use Symfony\Component\Ldap\Adapter\CollectionInterface;
|
|
use Symfony\Component\Ldap\Exception\LdapException;
|
|
use Symfony\Component\Ldap\Exception\NotBoundException;
|
|
|
|
/**
|
|
* @author Charles Sarrazin <charles@sarraz.in>
|
|
* @author Bob van de Vijver <bobvandevijver@hotmail.com>
|
|
*/
|
|
class Query extends AbstractQuery
|
|
{
|
|
public const PAGINATION_OID = \LDAP_CONTROL_PAGEDRESULTS;
|
|
|
|
/** @var resource[]|Result[] */
|
|
private array $results;
|
|
|
|
private array $serverctrls = [];
|
|
|
|
public function __sleep(): array
|
|
{
|
|
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
|
}
|
|
|
|
public function __wakeup()
|
|
{
|
|
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
|
}
|
|
|
|
public function __destruct()
|
|
{
|
|
$con = $this->connection->getResource();
|
|
$this->connection = null;
|
|
|
|
if (!isset($this->results)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($this->results as $result) {
|
|
if (false === $result || null === $result) {
|
|
continue;
|
|
}
|
|
if (!ldap_free_result($result)) {
|
|
throw new LdapException('Could not free results: '.ldap_error($con));
|
|
}
|
|
}
|
|
unset($this->results);
|
|
}
|
|
|
|
public function execute(): CollectionInterface
|
|
{
|
|
if (!isset($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();
|
|
|
|
$func = match ($this->options['scope']) {
|
|
static::SCOPE_BASE => 'ldap_read',
|
|
static::SCOPE_ONE => 'ldap_list',
|
|
static::SCOPE_SUB => 'ldap_search',
|
|
default => throw new LdapException(sprintf('Could not search in scope "%s".', $this->options['scope'])),
|
|
};
|
|
|
|
$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) {
|
|
$this->controlPagedResult($pageSize, true, $cookie);
|
|
}
|
|
$sizeLimit = $itemsLeft;
|
|
if ($pageSize > 0 && $sizeLimit >= $pageSize) {
|
|
$sizeLimit = 0;
|
|
}
|
|
$search = $this->callSearchFunction($con, $func, $sizeLimit);
|
|
|
|
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) {
|
|
$cookie = $this->controlPagedResultResponse($con, $search);
|
|
}
|
|
} while (null !== $cookie && '' !== $cookie);
|
|
|
|
if ($pageControl) {
|
|
$this->resetPagination();
|
|
}
|
|
}
|
|
|
|
return new Collection($this->connection, $this);
|
|
}
|
|
|
|
/**
|
|
* Returns an LDAP search resource. If this query resulted in multiple searches, only the first
|
|
* page will be returned.
|
|
*
|
|
* @return resource|Result|null
|
|
*
|
|
* @internal
|
|
*/
|
|
public function getResource(int $idx = 0)
|
|
{
|
|
return $this->results[$idx] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Returns all LDAP search resources.
|
|
*
|
|
* @return resource[]|Result[]
|
|
*
|
|
* @internal
|
|
*/
|
|
public function getResources(): array
|
|
{
|
|
return $this->results;
|
|
}
|
|
|
|
/**
|
|
* Resets pagination on the current connection.
|
|
*/
|
|
private function resetPagination(): void
|
|
{
|
|
$con = $this->connection->getResource();
|
|
$this->controlPagedResult(0, false, '');
|
|
$this->serverctrls = [];
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets LDAP pagination controls.
|
|
*/
|
|
private function controlPagedResult(int $pageSize, bool $critical, string $cookie): bool
|
|
{
|
|
$this->serverctrls = [
|
|
[
|
|
'oid' => \LDAP_CONTROL_PAGEDRESULTS,
|
|
'isCritical' => $critical,
|
|
'value' => [
|
|
'size' => $pageSize,
|
|
'cookie' => $cookie,
|
|
],
|
|
],
|
|
];
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Retrieve LDAP pagination cookie.
|
|
*
|
|
* @param resource|LDAPConnection $con
|
|
* @param resource|Result $result
|
|
*/
|
|
private function controlPagedResultResponse($con, $result): string
|
|
{
|
|
ldap_parse_result($con, $result, $errcode, $matcheddn, $errmsg, $referrals, $controls);
|
|
|
|
return $controls[\LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] ?? '';
|
|
}
|
|
|
|
/**
|
|
* Calls actual LDAP search function with the prepared options and parameters.
|
|
*
|
|
* @param resource|LDAPConnection $con
|
|
*
|
|
* @return resource|Result|false
|
|
*/
|
|
private function callSearchFunction($con, callable $func, int $sizeLimit)
|
|
{
|
|
return @$func($con, $this->dn, $this->query, $this->options['filter'], $this->options['attrsOnly'], $sizeLimit, $this->options['timeout'], $this->options['deref'], $this->serverctrls);
|
|
}
|
|
}
|