Skip to content

Instantly share code, notes, and snippets.

@ThaDafinser
Created February 23, 2017 14:27
Show Gist options
  • Save ThaDafinser/1d081bed8e5e6505e97bedf5863a187c to your computer and use it in GitHub Desktop.
Save ThaDafinser/1d081bed8e5e6505e97bedf5863a187c to your computer and use it in GitHub Desktop.
<?php
namespace EsSyncAd;
use Iterator;
use Zend\Ldap\Ldap;
use Zend\Ldap\Exception;
use Zend\Ldap\ErrorHandler;
use Zend\Ldap\Exception\LdapException;
final class PagingIterator implements Iterator
{
private $ldap;
private $filter;
private $baseDn;
private $returnAttributes;
private $pageSize;
private $resolveRangedAttributes;
private $entries;
private $current;
/**
* Required for paging
*
* @var unknown
*/
private $currentResult;
/**
* Required for paging
*
* @var unknown
*/
private $cookie = true;
public function __construct(Ldap $ldap, string $filter, string $baseDn = null, array $returnAttributes = null, $pageSize = 250, bool $resolveRangedAttributes = false)
{
$this->ldap = $ldap;
$this->filter = $filter;
$this->baseDn = $baseDn;
$this->returnAttributes = $returnAttributes;
$this->pageSize = $pageSize;
$this->resolveRangedAttributes = $resolveRangedAttributes;
}
private function getLdap()
{
return $this->ldap;
}
private function getFilter()
{
return $this->filter;
}
private function getBaseDn()
{
return $this->baseDn;
}
private function getReturnAttributes()
{
return $this->returnAttributes;
}
private function getPageSize()
{
return $this->pageSize;
}
/**
*
* @return bool
*/
private function getResolveRangedAttributes()
{
return $this->resolveRangedAttributes;
}
private function fetchPagedResult()
{
if ($this->cookie === null || $this->cookie === '') {
return false;
}
if ($this->cookie === true) {
// First fetch!
$this->cookie = '';
}
$ldap = $this->getLdap();
$resource = $ldap->getResource();
ldap_control_paged_result($resource, $this->getPageSize(), true, $this->cookie);
if ($this->getReturnAttributes() !== null) {
$resultResource = ldap_search($resource, $ldap->getBaseDn(), $this->getFilter(), $this->getReturnAttributes());
} else {
$resultResource = ldap_search($resource, $ldap->getBaseDn(), $this->getFilter());
}
if (! is_resource($resultResource)) {
/*
* @TODO better exception msg
*/
throw new \Exception('ldap_search returned something wrong...' . ldap_error($resource));
}
$entries = ldap_get_entries($resource, $resultResource);
if ($entries === false) {
throw new LdapException($ldap, 'Entires could not get fetched');
}
$entries = $this->getConvertedEntries($entries);
ErrorHandler::start();
$response = ldap_control_paged_result_response($resource, $resultResource, $this->cookie);
ErrorHandler::stop();
if ($response !== true) {
throw new LdapException($ldap, 'Paged result was empty');
}
if ($this->entries === null) {
$this->entries = [];
}
$this->entries = array_merge($this->entries, $entries);
return true;
}
private function getConvertedEntries(array $entries)
{
$result = [];
foreach ($entries as $key => $entry) {
if ($key === 'count') {
continue;
}
$result[$key] = $this->getConvertedEntry($entry);
}
return $result;
}
private function getConvertedEntry(array $entry)
{
$result = [];
foreach ($entry as $key => $value) {
if (is_int($key)) {
continue;
}
if ($key === 'count') {
continue;
}
if (isset($value['count'])) {
unset($value['count']);
}
$result[$key] = $value;
}
if ($this->getResolveRangedAttributes() === true) {
$result = $this->resolveRangedAttributes($result);
}
return $result;
}
private function resolveRangedAttributes(array $row)
{
$result = [];
foreach ($row as $key => $value) {
$keyExploded = explode(';range=', $key);
if (count($keyExploded) === 2) {
$range = explode('-', $keyExploded[1]);
$offsetAndLimit = (int) $range[1] + 1;
$result[$keyExploded[0]] = array_merge($value, $this->getAttributeRecursive($row['dn'], $keyExploded[0], $offsetAndLimit, $offsetAndLimit));
} else {
$result[$key] = $value;
}
}
return $result;
}
private function getAttributeRecursive(string $dn, string $attrName, int $offset, int $maxPerRequest)
{
$attributeValue = [];
$limit = $offset + $maxPerRequest - 1;
$searchedAttribute = $attrName . ';range=' . $offset . '-' . $limit;
$ldap = $this->getLdap();
$entry = $ldap->getEntry($dn, [
$searchedAttribute
], true);
foreach ($entry as $key => $value) {
// skip DN and other fields (if returned)
if (stripos($key, $attrName) === false) {
continue;
}
$attributeValue = $value;
// range result (pagination)
$keyExploded = explode(';range=', $key);
$range = explode('-', $keyExploded[1]);
$rangeEnd = (int) $range[1];
if ($range[0] == $offset && $range[1] == $limit) {
// more pages, there are more pages to fetch
$attributeValue = array_merge($attributeValue, $this->getAttributeRecursive($dn, $attrName, $rangeEnd + 1, $maxPerRequest));
}
}
return $attributeValue;
}
public function current()
{
if (! is_array($this->current)) {
$this->rewind();
}
if (! is_array($this->current)) {
return;
}
return $this->current;
}
public function key()
{
if (! is_array($this->current)) {
$this->rewind();
}
if (! is_array($this->current)) {
return;
}
return $this->current['dn'];
}
public function next()
{
// initial
if ($this->entries === null) {
$this->fetchPagedResult();
}
next($this->entries);
$this->current = current($this->entries);
}
public function rewind()
{
// initial
if ($this->entries === null) {
$this->fetchPagedResult();
}
reset($this->entries);
$this->current = current($this->entries);
}
public function valid()
{
if (is_array($this->current)) {
return true;
}
return $this->fetchPagedResult();
}
}
@madmatt
Copy link

madmatt commented Sep 1, 2018

One minor revision that I made to this - lines 103 and 105 should be $this->getBaseDn() instead of $ldap->getBaseDn() - otherwise the LDAP connections base DN is used which is generally much wider than you specifically want to iterate over (otherwise why allow specifying a DN at all, leaving it blank and ldap_search() will automatically use the base DN of the connection).

Also, to use this class, you just create the Zend\Ldap\Ldap class as normal, then create a PagingIterator, passing in the LDAP resource and your filters etc. as you normally would, then iterate over the results, for example:

$ldap = new Zend\Ldap\Ldap($options);
$users = new PagingIterator($ldap, '(&(objectClass=user))');

foreach ($users as $user) {
    printf($user['dn'][0]);
}

// Reset the LDAP pagination control back to the original, otherwise all further LDAP read queries fail
ldap_control_paged_result($ldap->getResource(), 1000);

Note: After using this iterator, you need to reset the pagination control. Setting it to 0 should work, but it doesn't - I'm not sure if this is a PHP or Active Directory issue. Setting it to 1,000 (the default value for a number of LDAP servers including Active Directory) does the trick. I couldn't find a neat way to do that within the iterator itself.

I'm looking to try and merge this into the zend-ldap library via a pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment