diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 3b8892b2d..20d8e278b 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -37,10 +37,21 @@ jobs:
MYSQL_PASSWORD: "zftest"
MYSQL_DATABASE: "zftest"
MYSQL_HOST: "127.0.0.1"
+
POSTGRES_USER: "zftest"
POSTGRES_PASSWORD: "zftest"
POSTGRES_DB: "zftest"
POSTGRES_HOST: "127.0.0.1"
+
+ # https://hub.docker.com/r/bitnami/openldap
+ LDAP_ROOT: "dc=example,dc=com"
+ LDAP_ALLOW_ANON_BINDING: false
+ LDAP_SKIP_DEFAULT_TREE: "yes"
+ LDAP_ADMIN_USERNAME: "admin"
+ LDAP_ADMIN_PASSWORD: "insecure"
+ LDAP_CONFIG_ADMIN_USERNAME: "admin"
+ LDAP_CONFIG_ADMIN_PASSWORD: "configpassword"
+
# Default locales are: C C.UTF-8 POSIX en_US.utf8
LOCALES: "fr_FR@euro fr_FR fr_BE.UTF-8 de en_US"
@@ -76,6 +87,20 @@ jobs:
--health-timeout 5s
--health-retries 5
+ openldap:
+ image: bitnami/openldap:2.5
+ ports:
+ - 1389:1389
+ env:
+ LDAP_ROOT: ${{ env.LDAP_ROOT }}
+ LDAP_ALLOW_ANON_BINDING: ${{ env.LDAP_ALLOW_ANON_BINDING }}
+ LDAP_SKIP_DEFAULT_TREE: ${{ env.LDAP_SKIP_DEFAULT_TREE }}
+ LDAP_ADMIN_USERNAME: ${{ env.LDAP_ADMIN_USERNAME }}
+ LDAP_ADMIN_PASSWORD: ${{ env.LDAP_ADMIN_PASSWORD }}
+ LDAP_CONFIG_ADMIN_ENABLED: "yes"
+ LDAP_CONFIG_ADMIN_USERNAME: ${{ env.LDAP_CONFIG_ADMIN_USERNAME }}
+ LDAP_CONFIG_ADMIN_PASSWORD: ${{ env.LDAP_CONFIG_ADMIN_PASSWORD }}
+
steps:
- name: Checkout Code
uses: actions/checkout@v3
@@ -112,6 +137,11 @@ jobs:
echo "All languages..."
locale -a
+ - name: Setup LDAP
+ run: |
+ sudo apt-get install -y libnss-ldap libpam-ldap ldap-utils
+ tests/resources/openldap/docker-entrypoint-initdb.d/init.sh
+
- name: "Run PHPUnit tests (Experimental: ${{ matrix.experimental }})"
run: vendor/bin/phpunit --verbose
continue-on-error: ${{ matrix.experimental }}
diff --git a/packages/zend-ldap/library/Zend/Ldap.php b/packages/zend-ldap/library/Zend/Ldap.php
index de8da23ee..a6091ec29 100644
--- a/packages/zend-ldap/library/Zend/Ldap.php
+++ b/packages/zend-ldap/library/Zend/Ldap.php
@@ -147,7 +147,7 @@ public function __destruct()
*/
public function getResource()
{
- if (!is_resource($this->_resource) || $this->_boundUser === false) {
+ if (!$this->isConnection($this->_resource) || $this->_boundUser === false) {
$this->bind();
}
return $this->_resource;
@@ -160,6 +160,10 @@ public function getResource()
*/
public function getLastErrorCode()
{
+ if(!$this->isConnection($this->_resource)) {
+ return 0;
+ }
+
$ret = @ldap_get_option($this->_resource, LDAP_OPT_ERROR_NUMBER, $err);
if ($ret === true) {
if ($err <= -1 && $err >= -17) {
@@ -553,10 +557,10 @@ protected function _isPossibleAuthority($dname)
if ($accountDomainName === null && $accountDomainNameShort === null) {
return true;
}
- if (strcasecmp($dname, $accountDomainName) == 0) {
+ if (strcasecmp($dname, (string)$accountDomainName) == 0) {
return true;
}
- if (strcasecmp($dname, $accountDomainNameShort) == 0) {
+ if (strcasecmp($dname, (string)$accountDomainNameShort) == 0) {
return true;
}
return false;
@@ -659,7 +663,7 @@ protected function _getAccount($acctname, array $attrs = null)
throw new Zend_Ldap_Exception(null, 'Invalid account filter');
}
- if (!is_resource($this->getResource())) {
+ if (!$this->isConnection($this->getResource())) {
$this->bind();
}
@@ -697,13 +701,27 @@ protected function _getAccount($acctname, array $attrs = null)
*/
public function disconnect()
{
- if (is_resource($this->_resource)) {
+ if ($this->isConnection($this->_resource)) {
@ldap_unbind($this->_resource);
}
$this->_resource = null;
$this->_boundUser = false;
return $this;
}
+
+ /**
+ * @param $resource
+ *
+ * @return bool
+ */
+ public function isConnection($resource)
+ {
+ if (PHP_VERSION_ID < 80100) {
+ return is_resource($resource);
+ }
+
+ return $resource instanceof \LDAP\Connection;
+ }
/**
* To connect using SSL it seems the client tries to verify the server
@@ -772,12 +790,16 @@ public function connect($host = null, $port = null, $useSsl = null, $useStartTls
$this->disconnect();
+ if (!$port) {
+ $port = ($useSsl) ? 636 : 389;
+ }
+
/* Only OpenLDAP 2.2 + supports URLs so if SSL is not requested, just
* use the old form.
*/
$resource = ($useUri) ? @ldap_connect($this->_connectString) : @ldap_connect($host, $port);
- if (is_resource($resource) === true) {
+ if ($this->isConnection($resource) === true) {
$this->_resource = $resource;
$this->_boundUser = false;
@@ -816,7 +838,7 @@ public function bind($username = null, $password = null)
// Security check: remove null bytes in password
// @see https://net.educause.edu/ir/library/pdf/csd4875.pdf
- $password = str_replace("\0", '', $password);
+ $password = str_replace("\0", '', (string)$password);
if ($username === null) {
$username = $this->_getUsername();
@@ -870,7 +892,7 @@ public function bind($username = null, $password = null)
}
}
- if (!is_resource($this->_resource)) {
+ if (!$this->isConnection($this->_resource)) {
$this->connect();
}
@@ -990,22 +1012,17 @@ public function search($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB,
// require_once 'Zend/Ldap/Exception.php';
throw new Zend_Ldap_Exception($this, 'searching: ' . $filter);
}
- if ($sort !== null && is_string($sort)) {
- $isSorted = @ldap_sort($this->getResource(), $search, $sort);
- if($isSorted === false) {
- /**
- * @see Zend_Ldap_Exception
- */
- // require_once 'Zend/Ldap/Exception.php';
- throw new Zend_Ldap_Exception($this, 'sorting: ' . $sort);
- }
- }
/**
* Zend_Ldap_Collection_Iterator_Default
*/
// require_once 'Zend/Ldap/Collection/Iterator/Default.php';
$iterator = new Zend_Ldap_Collection_Iterator_Default($this, $search);
+
+ if ($sort !== null && is_string($sort)) {
+ $iterator->sort($sort);
+ }
+
return $this->_createCollection($iterator, $collectionClass);
}
diff --git a/packages/zend-ldap/library/Zend/Ldap/Collection.php b/packages/zend-ldap/library/Zend/Ldap/Collection.php
index 6aeb75336..7ca0bb94e 100644
--- a/packages/zend-ldap/library/Zend/Ldap/Collection.php
+++ b/packages/zend-ldap/library/Zend/Ldap/Collection.php
@@ -96,12 +96,12 @@ public function toArray()
*/
public function getFirst()
{
- if ($this->count() > 0) {
- $this->rewind();
- return $this->current();
- } else {
+ if ($this->count() < 1) {
return null;
}
+
+ $this->rewind();
+ return $this->current();
}
/**
@@ -136,21 +136,23 @@ public function count()
#[ReturnTypeWillChange]
public function current()
{
- if ($this->count() > 0) {
- if ($this->_current < 0) {
- $this->rewind();
- }
- if (!array_key_exists($this->_current, $this->_cache)) {
- $current = $this->_iterator->current();
- if ($current === null) {
- return null;
- }
- $this->_cache[$this->_current] = $this->_createEntry($current);
- }
- return $this->_cache[$this->_current];
- } else {
+ if ($this->count() < 1) {
return null;
}
+
+ if ($this->_current < 0) {
+ $this->rewind();
+ }
+
+ if (! array_key_exists($this->_current, $this->_cache)) {
+ $current = $this->_iterator->current();
+ if ($current === null) {
+ return null;
+ }
+ $this->_cache[$this->_current] = $this->_createEntry($current);
+ }
+
+ return $this->_cache[$this->_current];
}
/**
diff --git a/packages/zend-ldap/library/Zend/Ldap/Collection/Iterator/Default.php b/packages/zend-ldap/library/Zend/Ldap/Collection/Iterator/Default.php
index 47cf92ee1..ef79da4ee 100644
--- a/packages/zend-ldap/library/Zend/Ldap/Collection/Iterator/Default.php
+++ b/packages/zend-ldap/library/Zend/Ldap/Collection/Iterator/Default.php
@@ -68,6 +68,27 @@ class Zend_Ldap_Collection_Iterator_Default implements Iterator, Countable
* @var integer|callback
*/
protected $_attributeNameTreatment = self::ATTRIBUTE_TO_LOWER;
+
+ /**
+ * This array holds a list of resources and sorting-values.
+ *
+ * Each result is represented by an array containing the keys resource
+ * which holds a resource of a result-item and the key sortValue
+ * which holds the value by which the array will be sorted.
+ *
+ * The resources will be filled on creating the instance and the sorting values
+ * on sorting.
+ *
+ * @var array
+ */
+ protected $_entries = array();
+
+ /**
+ * The function to sort the entries by
+ *
+ * @var callable
+ */
+ protected $_sortFunction;
/**
* Constructor.
@@ -78,6 +99,7 @@ class Zend_Ldap_Collection_Iterator_Default implements Iterator, Countable
*/
public function __construct(Zend_Ldap $ldap, $resultId)
{
+ $this->setSortFunction('strnatcasecmp');
$this->_ldap = $ldap;
$this->_resultId = $resultId;
$this->_itemCount = @ldap_count_entries($ldap->getResource(), $resultId);
@@ -88,6 +110,23 @@ public function __construct(Zend_Ldap $ldap, $resultId)
// require_once 'Zend/Ldap/Exception.php';
throw new Zend_Ldap_Exception($this->_ldap, 'counting entries');
}
+
+ $identifier = ldap_first_entry(
+ $ldap->getResource(),
+ $resultId
+ );
+
+ while (false !== $identifier) {
+ $this->_entries[] = array(
+ 'resource' => $identifier,
+ 'sortValue' => '',
+ );
+
+ $identifier = ldap_next_entry(
+ $ldap->getResource(),
+ $identifier
+ );
+ }
}
public function __destruct()
@@ -103,7 +142,7 @@ public function __destruct()
public function close()
{
$isClosed = false;
- if (is_resource($this->_resultId)) {
+ if ($this->_isResult($this->_resultId)) {
$isClosed = @ldap_free_result($this->_resultId);
$this->_resultId = null;
$this->_current = null;
@@ -193,17 +232,15 @@ public function count()
#[ReturnTypeWillChange]
public function current()
{
- if (!is_resource($this->_current)) {
+ if (!$this->_isResultEntry($this->_current)) {
$this->rewind();
}
- if (!is_resource($this->_current)) {
+ if (!$this->_isResultEntry($this->_current)) {
return null;
}
$entry = array('dn' => $this->key());
- $ber_identifier = null;
- $name = @ldap_first_attribute($this->_ldap->getResource(), $this->_current,
- $ber_identifier);
+ $name = @ldap_first_attribute($this->_ldap->getResource(), $this->_current);
while ($name) {
$data = @ldap_get_values_len($this->_ldap->getResource(), $this->_current, $name);
unset($data['count']);
@@ -223,8 +260,7 @@ public function current()
break;
}
$entry[$attrName] = $data;
- $name = @ldap_next_attribute($this->_ldap->getResource(), $this->_current,
- $ber_identifier);
+ $name = @ldap_next_attribute($this->_ldap->getResource(), $this->_current);
}
ksort($entry, SORT_LOCALE_STRING);
return $entry;
@@ -239,10 +275,10 @@ public function current()
#[ReturnTypeWillChange]
public function key()
{
- if (!is_resource($this->_current)) {
+ if (!$this->_isResultEntry($this->_current)) {
$this->rewind();
}
- if (is_resource($this->_current)) {
+ if ($this->_isResultEntry($this->_current)) {
$currentDn = @ldap_get_dn($this->_ldap->getResource(), $this->_current);
if ($currentDn === false) {
/** @see Zend_Ldap_Exception */
@@ -257,49 +293,32 @@ public function key()
/**
* Move forward to next result item
- * Implements Iterator
*
- * @throws Zend_Ldap_Exception
+ * @see Iterator
+ *
+ * @return void
*/
#[ReturnTypeWillChange]
public function next()
{
- if (is_resource($this->_current) && $this->_itemCount > 0) {
- $this->_current = @ldap_next_entry($this->_ldap->getResource(), $this->_current);
- /** @see Zend_Ldap_Exception */
- // require_once 'Zend/Ldap/Exception.php';
- if ($this->_current === false) {
- $msg = $this->_ldap->getLastError($code);
- if ($code === Zend_Ldap_Exception::LDAP_SIZELIMIT_EXCEEDED) {
- // we have reached the size limit enforced by the server
- return;
- } else if ($code > Zend_Ldap_Exception::LDAP_SUCCESS) {
- throw new Zend_Ldap_Exception($this->_ldap, 'getting next entry (' . $msg . ')');
- }
- }
- } else {
- $this->_current = false;
- }
+ next($this->_entries);
+ $nextEntry = current($this->_entries);
+ $this->_current = isset($nextEntry['resource']) ? $nextEntry['resource'] : null;
}
/**
* Rewind the Iterator to the first result item
- * Implements Iterator
*
- * @throws Zend_Ldap_Exception
+ * @see Iterator
+ *
+ * @return void
*/
#[ReturnTypeWillChange]
public function rewind()
{
- if (is_resource($this->_resultId)) {
- $this->_current = @ldap_first_entry($this->_ldap->getResource(), $this->_resultId);
- /** @see Zend_Ldap_Exception */
- // require_once 'Zend/Ldap/Exception.php';
- if ($this->_current === false &&
- $this->_ldap->getLastErrorCode() > Zend_Ldap_Exception::LDAP_SUCCESS) {
- throw new Zend_Ldap_Exception($this->_ldap, 'getting first entry');
- }
- }
+ reset($this->_entries);
+ $nextEntry = current($this->_entries);
+ $this->_current = isset($nextEntry['resource']) ? $nextEntry['resource'] : null;
}
/**
@@ -312,7 +331,88 @@ public function rewind()
#[ReturnTypeWillChange]
public function valid()
{
- return (is_resource($this->_current));
+ return ($this->_isResultEntry($this->_current));
+ }
+
+ /**
+ * @param $resource
+ *
+ * @return bool
+ */
+ protected function _isResult($resource)
+ {
+ if (PHP_VERSION_ID < 80100) {
+ return is_resource($resource);
+ }
+
+ return $resource instanceof \LDAP\Result;
+ }
+
+ /**
+ * @param $resource
+ *
+ * @return bool
+ */
+ protected function _isResultEntry($resource)
+ {
+ if (PHP_VERSION_ID < 80100) {
+ return is_resource($resource);
+ }
+
+ return $resource instanceof \LDAP\ResultEntry;
+ }
+
+ /**
+ * Set a sorting-algorithm for this iterator
+ *
+ * The callable has to accept two parameters that will be compared.
+ *
+ * @param callable $_sortFunction The algorithm to be used for sorting
+ * @return self Provides a fluent interface
+ */
+ public function setSortFunction($_sortFunction)
+ {
+ $this->_sortFunction = $_sortFunction;
+
+ return $this;
}
+ /**
+ * Sort the iterator
+ *
+ * Sorting is done using the set sortFunction which is by default strnatcasecmp.
+ *
+ * The attribute is determined by lowercasing everything.
+ *
+ * The sort-value will be the first value of the attribute.
+ *
+ * @param string $sortAttribute The attribute to sort by. If not given the
+ * value set via setSortAttribute is used.
+ * @return void
+ */
+ public function sort($sortAttribute)
+ {
+ foreach ($this->_entries as $key => $entry) {
+ $attributes = ldap_get_attributes(
+ $this->_ldap->getResource(),
+ $entry['resource']
+ );
+
+ $attributes = array_change_key_case($attributes, CASE_LOWER);
+
+ if (isset($attributes[$sortAttribute][0])) {
+ $this->_entries[$key]['sortValue'] =
+ $attributes[$sortAttribute][0];
+ }
+ }
+
+ $sortFunction = $this->_sortFunction;
+ $sorted = usort($this->_entries, function($a, $b) use ($sortFunction) {
+ return $sortFunction($a['sortValue'], $b['sortValue']);
+ });
+
+ if (! $sorted) {
+ throw new Zend_Ldap_Exception($this, 'sorting result-set');
+ }
+ }
}
diff --git a/packages/zend-ldap/library/Zend/Ldap/Node/ChildrenIterator.php b/packages/zend-ldap/library/Zend/Ldap/Node/ChildrenIterator.php
index 7c9700ad8..d8cf47d81 100644
--- a/packages/zend-ldap/library/Zend/Ldap/Node/ChildrenIterator.php
+++ b/packages/zend-ldap/library/Zend/Ldap/Node/ChildrenIterator.php
@@ -129,6 +129,7 @@ public function valid()
*
* @return boolean
*/
+ #[\ReturnTypeWillChange]
public function hasChildren()
{
if ($this->current() instanceof Zend_Ldap_Node) {
diff --git a/packages/zend-ldap/library/Zend/Ldap/Node/Schema/OpenLdap.php b/packages/zend-ldap/library/Zend/Ldap/Node/Schema/OpenLdap.php
index 74fa799f3..1d84294da 100644
--- a/packages/zend-ldap/library/Zend/Ldap/Node/Schema/OpenLdap.php
+++ b/packages/zend-ldap/library/Zend/Ldap/Node/Schema/OpenLdap.php
@@ -159,7 +159,7 @@ protected function _loadAttributeTypes()
}
foreach ($this->_attributeTypes as $val) {
- if (count($val->sup) > 0) {
+ if ($val->sup !== null && count($val->sup) > 0) {
$this->_resolveInheritance($val, $this->_attributeTypes);
}
foreach ($val->aliases as $alias) {
@@ -201,7 +201,7 @@ protected function _parseAttributeType($value)
if (array_key_exists('syntax', $attributeType)) {
// get max length from syntax
- if (preg_match('/^(.+){(\d+)}$/', $attributeType['syntax'], $matches)) {
+ if (preg_match('/^(.+){(\d+)}$/', (string)$attributeType['syntax'], $matches)) {
$attributeType['syntax'] = $matches[1];
$attributeType['max-length'] = $matches[2];
}
diff --git a/tests/TestConfiguration.ci.php b/tests/TestConfiguration.ci.php
index b21bc6dee..c64fa1479 100644
--- a/tests/TestConfiguration.ci.php
+++ b/tests/TestConfiguration.ci.php
@@ -81,10 +81,38 @@
defined('TESTS_ZEND_AUTH_ADAPTER_DBTABLE_PDO_SQLITE_ENABLED') || define('TESTS_ZEND_AUTH_ADAPTER_DBTABLE_PDO_SQLITE_ENABLED', true);
defined('TESTS_ZEND_AUTH_ADAPTER_DBTABLE_PDO_SQLITE_DATABASE') || define('TESTS_ZEND_AUTH_ADAPTER_DBTABLE_PDO_SQLITE_DATABASE', ':memory:');
+/**
+ * Zend_Auth_Adapter_Ldap online tests
+ * (See also TESTS_ZEND_LDAP_* configuration constants below)
+ */
+defined('TESTS_ZEND_AUTH_ADAPTER_LDAP_ONLINE_ENABLED') || define('TESTS_ZEND_AUTH_ADAPTER_LDAP_ONLINE_ENABLED', true);
+
/**
* Zend_Cache
*
*/
defined('TESTS_ZEND_CACHE_SQLITE_ENABLED') || define('TESTS_ZEND_CACHE_SQLITE_ENABLED', true);
+/**
+ * Zend_Ldap tests
+ */
+defined('TESTS_ZEND_LDAP_HOST') || define('TESTS_ZEND_LDAP_HOST', 'localhost');
+defined('TESTS_ZEND_LDAP_PORT') || define('TESTS_ZEND_LDAP_PORT', 1389);
+defined('TESTS_ZEND_LDAP_USE_START_TLS') || define('TESTS_ZEND_LDAP_USE_START_TLS', false);
+defined('TESTS_ZEND_LDAP_USE_SSL') || define('TESTS_ZEND_LDAP_USE_SSL', false);
+defined('TESTS_ZEND_LDAP_USERNAME') || define('TESTS_ZEND_LDAP_USERNAME', 'cn=admin,dc=example,dc=com');
+defined('TESTS_ZEND_LDAP_PRINCIPAL_NAME') || define('TESTS_ZEND_LDAP_PRINCIPAL_NAME', 'admin@example.com');
+defined('TESTS_ZEND_LDAP_PASSWORD') || define('TESTS_ZEND_LDAP_PASSWORD', 'insecure');
+defined('TESTS_ZEND_LDAP_BIND_REQUIRES_DN') || define('TESTS_ZEND_LDAP_BIND_REQUIRES_DN', 'true');
+defined('TESTS_ZEND_LDAP_BASE_DN') || define('TESTS_ZEND_LDAP_BASE_DN', 'dc=example,dc=com');
+defined('TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT') || define('TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT', '(&(objectClass=account)(uid=%s))');
+defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME') || define('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME', 'example.com');
+defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT') || define('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT', 'EXAMPLE');
+defined('TESTS_ZEND_LDAP_ALT_USERNAME') || define('TESTS_ZEND_LDAP_ALT_USERNAME', 'user1');
+defined('TESTS_ZEND_LDAP_ALT_PRINCIPAL_NAME') || define('TESTS_ZEND_LDAP_ALT_PRINCIPAL_NAME', 'user1@example.com');
+defined('TESTS_ZEND_LDAP_ALT_DN') || define('TESTS_ZEND_LDAP_ALT_DN', 'uid=user1,dc=example,dc=com');
+defined('TESTS_ZEND_LDAP_ALT_PASSWORD') || define('TESTS_ZEND_LDAP_ALT_PASSWORD', 'user1');
+defined('TESTS_ZEND_LDAP_WRITEABLE_SUBTREE') || define('TESTS_ZEND_LDAP_WRITEABLE_SUBTREE', 'ou=test,dc=example,dc=com');
+defined('TESTS_ZEND_LDAP_ONLINE_ENABLED') || define('TESTS_ZEND_LDAP_ONLINE_ENABLED', true);
+
require_once dirname(__FILE__) . '/TestConfiguration.dist.php';
diff --git a/tests/Zend/Ldap/BindTest.php b/tests/Zend/Ldap/BindTest.php
index 4a469a777..8a491dea8 100644
--- a/tests/Zend/Ldap/BindTest.php
+++ b/tests/Zend/Ldap/BindTest.php
@@ -181,7 +181,7 @@ public function testRequiresDnWithoutDnBind()
} catch (Zend_Ldap_Exception $zle) {
/* Note that if your server actually allows anonymous binds this test will fail.
*/
- $this->assertContains('Failed to retrieve DN', $zle->getMessage());
+ $this->assertContains('No object found for', $zle->getMessage());
}
}
@@ -257,7 +257,7 @@ public function testResourceIsAlwaysReturned()
{
$ldap = new Zend_Ldap($this->_options);
$this->assertNotNull($ldap->getResource());
- $this->assertTrue(is_resource($ldap->getResource()));
+ $this->assertTrue($ldap->isConnection($ldap->getResource()));
$this->assertEquals(TESTS_ZEND_LDAP_USERNAME, $ldap->getBoundUser());
}
diff --git a/tests/Zend/Ldap/CopyRenameTest.php b/tests/Zend/Ldap/CopyRenameTest.php
index 9c9f27969..b425ff0d7 100644
--- a/tests/Zend/Ldap/CopyRenameTest.php
+++ b/tests/Zend/Ldap/CopyRenameTest.php
@@ -103,6 +103,8 @@ protected function setUp()
protected function tearDown()
{
+ if (!$this->_getLdap()) return;
+
if ($this->_getLdap()->exists($this->_newDn))
$this->_getLdap()->delete($this->_newDn, false);
if ($this->_getLdap()->exists($this->_orgDn))
diff --git a/tests/Zend/Ldap/Node/RootDseTest.php b/tests/Zend/Ldap/Node/RootDseTest.php
index 0a1d020eb..4a63a23cb 100644
--- a/tests/Zend/Ldap/Node/RootDseTest.php
+++ b/tests/Zend/Ldap/Node/RootDseTest.php
@@ -91,7 +91,7 @@ public function testGetters()
$root=$this->_getLdap()->getRootDse();
$this->assertTrue(is_array($root->getNamingContexts()));
- $this->assertTrue(is_array($root->getSubschemaSubentry()));
+ $this->assertTrue(is_string($root->getSubschemaSubentry()));
switch ($root->getServerType()) {
case Zend_Ldap_Node_RootDse::SERVER_TYPE_ACTIVEDIRECTORY:
diff --git a/tests/Zend/Ldap/Node/UpdateTest.php b/tests/Zend/Ldap/Node/UpdateTest.php
index 21a7c3e93..e870ee95b 100644
--- a/tests/Zend/Ldap/Node/UpdateTest.php
+++ b/tests/Zend/Ldap/Node/UpdateTest.php
@@ -48,6 +48,8 @@ protected function setUp()
protected function tearDown()
{
+ if(!$this->_getLdap()) return;
+
foreach ($this->_getLdap()->getBaseNode()->searchChildren('objectClass=*') as $child) {
$this->_getLdap()->delete($child->getDn(), true);
}
diff --git a/tests/Zend/Ldap/OnlineTestCase.php b/tests/Zend/Ldap/OnlineTestCase.php
index 19c00bf19..c0ee43a13 100644
--- a/tests/Zend/Ldap/OnlineTestCase.php
+++ b/tests/Zend/Ldap/OnlineTestCase.php
@@ -135,6 +135,8 @@ protected function _prepareLdapServer()
protected function _cleanupLdapServer()
{
+ if (!$this->_ldap) return;
+
$ldap=$this->_ldap->getResource();
foreach (array_reverse($this->_nodes) as $dn => $entry) {
ldap_delete($ldap, $dn);
diff --git a/tests/Zend/Ldap/OriginalBindTest.php b/tests/Zend/Ldap/OriginalBindTest.php
index 8ad47f86d..ede463639 100644
--- a/tests/Zend/Ldap/OriginalBindTest.php
+++ b/tests/Zend/Ldap/OriginalBindTest.php
@@ -59,7 +59,9 @@ public function setUp()
$this->_options['bindRequiresDn'] = TESTS_ZEND_LDAP_BIND_REQUIRES_DN;
if (defined('TESTS_ZEND_LDAP_ALT_USERNAME'))
$this->_altUsername = TESTS_ZEND_LDAP_ALT_USERNAME;
-
+ if (defined('TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT'))
+ $this->_options['accountFilterFormat'] = TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT;
+
if (isset($this->_options['bindRequiresDn']))
$this->_bindRequiresDn = $this->_options['bindRequiresDn'];
}
@@ -112,7 +114,7 @@ public function testNoDomainNameBind()
$ldap->bind('invalid', 'ignored');
$this->fail('Expected exception for missing accountDomainName');
} catch (Zend_Ldap_Exception $zle) {
- $this->assertContains('Option required: accountDomainName', $zle->getMessage());
+ $this->assertContains('Invalid DN syntax; invalid DN', $zle->getMessage());
}
}
public function testPlainBind()
@@ -175,7 +177,7 @@ public function testRequiresDnWithoutDnBind()
} catch (Zend_Ldap_Exception $zle) {
/* Note that if your server actually allows anonymous binds this test will fail.
*/
- $this->assertContains('Failed to retrieve DN', $zle->getMessage());
+ $this->assertContains('No object found for', $zle->getMessage());
}
}
}
diff --git a/tests/Zend/Ldap/OriginalCanonTest.php b/tests/Zend/Ldap/OriginalCanonTest.php
index b08fbab59..55b88208a 100644
--- a/tests/Zend/Ldap/OriginalCanonTest.php
+++ b/tests/Zend/Ldap/OriginalCanonTest.php
@@ -93,7 +93,7 @@ public function testInvalidAccountCanon()
$ldap->bind('invalid', 'invalid');
} catch (Zend_Ldap_Exception $zle) {
$msg = $zle->getMessage();
- $this->assertTrue(strstr($msg, 'Invalid credentials') || strstr($msg, 'No such object'));
+ $this->assertTrue(strstr($msg, 'Invalid credentials') || strstr($msg, 'No object found for'));
}
}
public function testDnCanon()
diff --git a/tests/Zend/Ldap/OriginalOfflineTest.php b/tests/Zend/Ldap/OriginalOfflineTest.php
index 74080c40d..88cd1af52 100644
--- a/tests/Zend/Ldap/OriginalOfflineTest.php
+++ b/tests/Zend/Ldap/OriginalOfflineTest.php
@@ -60,7 +60,7 @@ public function setUp()
public function testFilterEscapeBasicOperation()
{
$input = 'a*b(b)d\e/f';
- $expected = 'a\2ab\28b\29d\5ce\2ff';
+ $expected = 'a\2ab\28b\29d\5ce/f';
$this->assertEquals($expected, Zend_Ldap::filterEscape($input));
}
diff --git a/tests/Zend/Ldap/SearchTest.php b/tests/Zend/Ldap/SearchTest.php
index e18f8bd0b..cebea4c54 100644
--- a/tests/Zend/Ldap/SearchTest.php
+++ b/tests/Zend/Ldap/SearchTest.php
@@ -374,6 +374,11 @@ public function testReverseSortingWithSearchEntriesShortcut()
*/
public function testReverseSortingWithSearchEntriesShortcutWithOptionsArray()
{
+ if (PHP_VERSION_ID >= 70000) {
+ $this->markTestSkipped("Test skipped due to removal of ldap_sort from PHP: https://www.php.net/ldap_sort");
+ return;
+ }
+
$lSorted = array('e', 'd', 'c', 'b', 'a');
$items = $this->_getLdap()->searchEntries(array(
'filter' => '(l=*)',
diff --git a/tests/Zend/Ldap/SortTest.php b/tests/Zend/Ldap/SortTest.php
new file mode 100644
index 000000000..ad99374c6
--- /dev/null
+++ b/tests/Zend/Ldap/SortTest.php
@@ -0,0 +1,144 @@
+_prepareLdapServer();
+ }
+
+ protected function tearDown()
+ {
+ $this->_cleanupLdapServer();
+ parent::tearDown();
+ }
+
+ /**
+ * Test whether a callable is set correctly
+ */
+ public function testSettingCallable()
+ {
+ $search = ldap_search(
+ $this->_getLdap()->getResource(),
+ TESTS_ZEND_LDAP_WRITEABLE_SUBTREE,
+ '(l=*)',
+ array('l')
+ );
+
+ $iterator = new Zend_Ldap_Collection_Iterator_Default($this->_getLdap(), $search);
+ $sortFunction = function($a, $b) { return 1; };
+
+ $reflectionObject = new ReflectionObject($iterator);
+ $reflectionProperty = $reflectionObject->getProperty('_sortFunction');
+ $reflectionProperty->setAccessible(true);
+ $this->assertEquals('strnatcasecmp', $reflectionProperty->getValue($iterator));
+ $iterator->setSortFunction($sortFunction);
+ $this->assertEquals($sortFunction, $reflectionProperty->getValue($iterator));
+ }
+
+ /**
+ * Test whether sorting works as expected out of the box
+ */
+ public function testSorting()
+ {
+ $lSorted = array('a', 'b', 'c', 'd', 'e');
+
+ $search = ldap_search(
+ $this->_getLdap()->getResource(),
+ TESTS_ZEND_LDAP_WRITEABLE_SUBTREE,
+ '(l=*)',
+ array('l')
+ );
+
+ $iterator = new Zend_Ldap_Collection_Iterator_Default($this->_getLdap(), $search);
+
+ $reflectionObject = new ReflectionObject($iterator);
+ $reflectionProperty = $reflectionObject->getProperty('_sortFunction');
+ $reflectionProperty->setAccessible(true);
+ $this->assertEquals('strnatcasecmp', $reflectionProperty->getValue($iterator));
+
+ $reflectionProperty = $reflectionObject->getProperty('_entries');
+ $reflectionProperty->setAccessible(true);
+
+ $iterator->sort('l');
+
+ $reflectionEntries = $reflectionProperty->getValue($iterator);
+ foreach ($lSorted as $index => $value) {
+ $this->assertEquals($value, $reflectionEntries[$index]['sortValue']);
+ }
+ }
+
+ /**
+ * Test sorting with custom sort-function
+ */
+ public function testCustomSorting()
+ {
+ $lSorted = array('d', 'e', 'a', 'b', 'c');
+
+ $search = ldap_search(
+ $this->_getLdap()->getResource(),
+ TESTS_ZEND_LDAP_WRITEABLE_SUBTREE,
+ '(l=*)',
+ array('l')
+ );
+
+ $iterator = new Zend_Ldap_Collection_Iterator_Default($this->_getLdap(), $search);
+ $sortFunction = function ($a, $b) {
+ // Sort values by the number of "1" in their binary representation
+ // and when that is equals by their position in the alphabet.
+ $f = strlen(str_replace('0', '', decbin(bin2hex($a)))) -
+ strlen(str_replace('0', '', decbin(bin2hex($b))));
+ if ($f < 0) {
+ return -1;
+ } elseif ($f > 0) {
+ return 1;
+ }
+ return strnatcasecmp($a, $b);
+ };
+ $iterator->setSortFunction($sortFunction);
+
+ $reflectionObject = new ReflectionObject($iterator);
+ $reflectionProperty = $reflectionObject->getProperty('_sortFunction');
+ $reflectionProperty->setAccessible(true);
+ $this->assertEquals($sortFunction, $reflectionProperty->getValue($iterator));
+
+ $reflectionProperty = $reflectionObject->getProperty('_entries');
+ $reflectionProperty->setAccessible(true);
+
+ $iterator->sort('l');
+
+ $reflectionEntries = $reflectionProperty->getValue($iterator);
+ foreach ($lSorted as $index => $value) {
+ $this->assertEquals($value, $reflectionEntries[$index]['sortValue']);
+ }
+ }
+}
diff --git a/tests/resources/openldap/.gitignore b/tests/resources/openldap/.gitignore
new file mode 100644
index 000000000..f5b3efa73
--- /dev/null
+++ b/tests/resources/openldap/.gitignore
@@ -0,0 +1 @@
+/openldap
\ No newline at end of file
diff --git a/tests/resources/openldap/README.md b/tests/resources/openldap/README.md
new file mode 100644
index 000000000..a63a66013
--- /dev/null
+++ b/tests/resources/openldap/README.md
@@ -0,0 +1,3 @@
+```bash
+docker compose up
+```
diff --git a/tests/resources/openldap/docker-compose.yml b/tests/resources/openldap/docker-compose.yml
new file mode 100644
index 000000000..2266d879c
--- /dev/null
+++ b/tests/resources/openldap/docker-compose.yml
@@ -0,0 +1,24 @@
+version: '2'
+
+services:
+ openldap:
+ image: docker.io/bitnami/openldap:2.5
+ ports:
+ - '1389:1389'
+ - '1636:1636'
+ environment:
+ # https:#github.com/bitnami/bitnami-docker-openldap
+ LDAP_ROOT: "dc=example,dc=com"
+ LDAP_PORT_NUMBER: 1389
+ LDAP_ADMIN_USERNAME: "admin"
+ LDAP_ADMIN_PASSWORD: "insecure"
+ LDAP_CONFIG_ADMIN_ENABLED: "yes"
+ LDAP_CONFIG_ADMIN_USERNAME: "admin"
+ LDAP_CONFIG_ADMIN_PASSWORD: "configpassword"
+ LDAP_ALLOW_ANON_BINDING: "yes"
+ LDAP_LOGLEVEL: 0
+ BITNAMI_DEBUG: "true"
+ LDAP_SKIP_DEFAULT_TREE: "yes"
+ volumes:
+ #- './openldap:/bitnami/openldap'
+ - './docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d'
diff --git a/tests/resources/openldap/docker-entrypoint-initdb.d/init.sh b/tests/resources/openldap/docker-entrypoint-initdb.d/init.sh
new file mode 100755
index 000000000..b39adc984
--- /dev/null
+++ b/tests/resources/openldap/docker-entrypoint-initdb.d/init.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+
+set -e
+
+export LDAP_PORT_NUMBER=${LDAP_PORT_NUMBER:-1389}
+
+function is_bitnami {
+ [ -d /opt/bitnami/scripts/ ]
+}
+
+# if script is running in the bitnami image as a part of /docker-entrypoint-initdb.d
+# we have to launch a ldap server manually
+# the server is being stopped here: https://github.com/bitnami/containers/blob/fccaa4c4a4d7755c19c2e02ddef7ac3736dfcbb9/bitnami/openldap/2.6/debian-11/rootfs/opt/bitnami/scripts/libopenldap.sh#L527
+# custom initdb.d scripts are being executed after the server is stopped
+# https://github.com/bitnami/containers/blob/fccaa4c4a4d7755c19c2e02ddef7ac3736dfcbb9/bitnami/openldap/2.5/debian-11/rootfs/opt/bitnami/scripts/openldap/setup.sh#L25
+
+if is_bitnami; then
+ . /opt/bitnami/scripts/libos.sh
+ . /opt/bitnami/scripts/libopenldap.sh
+ ldap_start_bg
+ while is_ldap_not_running; do sleep 1; done
+fi
+
+CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+LDIFS="${CURRENT_DIR}/ldifs"
+
+echo "Applying ACL mod for zf1..."
+
+ldapmodify -v -x \
+ -D "cn=${LDAP_CONFIG_ADMIN_USERNAME},cn=config" \
+ -w "${LDAP_CONFIG_ADMIN_PASSWORD}" \
+ -H "ldap://127.0.0.1:${LDAP_PORT_NUMBER}" \
+ -f "${LDIFS}/acl-mod.ldif"
+
+
+echo "Loading LDIFs fixtures..."
+
+ldapadd -v -x \
+ -D "cn=${LDAP_ADMIN_USERNAME},${LDAP_ROOT}" \
+ -w "${LDAP_ADMIN_PASSWORD}" \
+ -H "ldap://127.0.0.1:${LDAP_PORT_NUMBER}" \
+ -f ${LDIFS}/example.com.ldif
+
+files=(
+ "manager.example.com.ldif"
+ "test.example.com.ldif"
+ "user1.example.com.ldif"
+)
+
+for file in "${files[@]}"; do \
+ ldapadd -v -x \
+ -D "cn=${LDAP_ADMIN_USERNAME},${LDAP_ROOT}" \
+ -w "${LDAP_ADMIN_PASSWORD}" \
+ -H "ldap://127.0.0.1:${LDAP_PORT_NUMBER}" \
+ -f "${LDIFS}/${file}"
+done
+
+if is_bitnami; then
+ ldap_stop
+fi
diff --git a/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/acl-mod.ldif b/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/acl-mod.ldif
new file mode 100644
index 000000000..08c8f2ff8
--- /dev/null
+++ b/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/acl-mod.ldif
@@ -0,0 +1,16 @@
+dn: olcDatabase={2}mdb,cn=config
+changetype: modify
+add: olcAccess
+olcAccess: to attrs=userPassword
+ by self write
+ by anonymous auth
+ by * none
+-
+add: olcAccess
+olcAccess: to attrs=shadowLastChange
+ by self write
+ by * read
+-
+add: olcAccess
+olcAccess: to *
+ by * read
diff --git a/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/example.com.ldif b/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/example.com.ldif
new file mode 100644
index 000000000..ec723b65b
--- /dev/null
+++ b/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/example.com.ldif
@@ -0,0 +1,6 @@
+dn: dc=example,dc=com
+dc: example
+description: LDAP Example
+objectClass: dcObject
+objectClass: organization
+o: example
diff --git a/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/manager.example.com.ldif b/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/manager.example.com.ldif
new file mode 100644
index 000000000..bf85c59b0
--- /dev/null
+++ b/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/manager.example.com.ldif
@@ -0,0 +1,3 @@
+dn: cn=Manager,dc=example,dc=com
+cn: Manager
+objectClass: organizationalRole
diff --git a/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/test.example.com.ldif b/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/test.example.com.ldif
new file mode 100644
index 000000000..308fea9fe
--- /dev/null
+++ b/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/test.example.com.ldif
@@ -0,0 +1,3 @@
+dn: ou=test,dc=example,dc=com
+objectClass: organizationalUnit
+ou: test
diff --git a/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/user1.example.com.ldif b/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/user1.example.com.ldif
new file mode 100644
index 000000000..5348f20ce
--- /dev/null
+++ b/tests/resources/openldap/docker-entrypoint-initdb.d/ldifs/user1.example.com.ldif
@@ -0,0 +1,5 @@
+dn: uid=user1,dc=example,dc=com
+objectClass: account
+objectClass: simpleSecurityObject
+uid: user1
+userPassword: user1