From 65df0823941fe89788d7b27c3a720ee760af75f1 Mon Sep 17 00:00:00 2001 From: RWOverdijk Date: Thu, 9 Aug 2012 10:05:13 +0200 Subject: [PATCH 1/5] missed spaces --- src/Converter/Converter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Converter/Converter.php b/src/Converter/Converter.php index 75f4474b5..771a9caf2 100644 --- a/src/Converter/Converter.php +++ b/src/Converter/Converter.php @@ -311,7 +311,7 @@ public static function fromLdapDateTime($date, $asUtc = true) } // Set Offset - $offsetRegEx = '/([Z\-\+])(\d{2}\'?){0,1}(\d{2}\'?){0,1}$/'; + $offsetRegEx = '/([Z\-\+])(\d{2}\'?) {0,1}(\d{2}\'?) {0,1}$/'; $off = array(); if (preg_match($offsetRegEx, $date, $off)) { $offset = $off[1]; From bfc2f54f1ad3cb960d96577792ee9684bd086a49 Mon Sep 17 00:00:00 2001 From: RWOverdijk Date: Thu, 9 Aug 2012 10:33:56 +0200 Subject: [PATCH 2/5] Removed wrongly placed spaces --- src/Converter/Converter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Converter/Converter.php b/src/Converter/Converter.php index 771a9caf2..75f4474b5 100644 --- a/src/Converter/Converter.php +++ b/src/Converter/Converter.php @@ -311,7 +311,7 @@ public static function fromLdapDateTime($date, $asUtc = true) } // Set Offset - $offsetRegEx = '/([Z\-\+])(\d{2}\'?) {0,1}(\d{2}\'?) {0,1}$/'; + $offsetRegEx = '/([Z\-\+])(\d{2}\'?){0,1}(\d{2}\'?){0,1}$/'; $off = array(); if (preg_match($offsetRegEx, $date, $off)) { $offset = $off[1]; From 9037a102dc009cfdc809d1fcd3f84ba748c09335 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 20 Aug 2012 16:29:39 -0500 Subject: [PATCH 3/5] CS fixes - trailing whitespace --- src/Converter/Converter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Converter/Converter.php b/src/Converter/Converter.php index eb3647b04..30441c810 100644 --- a/src/Converter/Converter.php +++ b/src/Converter/Converter.php @@ -386,7 +386,7 @@ public static function fromLdapUnserialize($value) ErrorHandler::start(E_NOTICE); $v = unserialize($value); ErrorHandler::stop(); - + if (false === $v && $value != 'b:0;') { throw new Exception\UnexpectedValueException('The given value could not be unserialized'); } From fdeb02876fe3906d478eb165f7b09f5d5b91fdc2 Mon Sep 17 00:00:00 2001 From: Domisys Date: Thu, 30 Aug 2012 12:44:33 +0300 Subject: [PATCH 4/5] Update library/Zend/Ldap/Collection/DefaultIterator.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don't use $ldap->getResource() in ldap_* functions to avoid « ErrorHandler already started » error. --- src/Collection/DefaultIterator.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Collection/DefaultIterator.php b/src/Collection/DefaultIterator.php index 3d94aa6ad..7e2afbd48 100644 --- a/src/Collection/DefaultIterator.php +++ b/src/Collection/DefaultIterator.php @@ -75,8 +75,9 @@ public function __construct(Ldap\Ldap $ldap, $resultId) $this->ldap = $ldap; $this->resultId = $resultId; + $resource = $ldap->getResource(); ErrorHandler::start(); - $this->itemCount = ldap_count_entries($ldap->getResource(), $resultId); + $this->itemCount = ldap_count_entries($resource, $resultId); ErrorHandler::stop(); if ($this->itemCount === false) { throw new Exception\LdapException($this->ldap, 'counting entries'); @@ -199,16 +200,17 @@ public function current() $entry = array('dn' => $this->key()); $ber_identifier = null; + $resource = $this->ldap->getResource(); ErrorHandler::start(); $name = ldap_first_attribute( - $this->ldap->getResource(), $this->current, + $resource, $this->current, $ber_identifier ); ErrorHandler::stop(); while ($name) { ErrorHandler::start(); - $data = ldap_get_values_len($this->ldap->getResource(), $this->current, $name); + $data = ldap_get_values_len($resource, $this->current, $name); ErrorHandler::stop(); if (!$data) { @@ -237,7 +239,7 @@ public function current() ErrorHandler::start(); $name = ldap_next_attribute( - $this->ldap->getResource(), $this->current, + $resource, $this->current, $ber_identifier ); ErrorHandler::stop(); @@ -259,8 +261,9 @@ public function key() $this->rewind(); } if (is_resource($this->current)) { + $resource = $this->ldap->getResource(); ErrorHandler::start(); - $currentDn = ldap_get_dn($this->ldap->getResource(), $this->current); + $currentDn = ldap_get_dn($resource, $this->current); ErrorHandler::stop(); if ($currentDn === false) { @@ -285,8 +288,9 @@ public function next() $code = 0; if (is_resource($this->current) && $this->itemCount > 0) { + $resource = $this->ldap->getResource(); ErrorHandler::start(); - $this->current = ldap_next_entry($this->ldap->getResource(), $this->current); + $this->current = ldap_next_entry($resource, $this->current); ErrorHandler::stop(); if ($this->current === false) { $msg = $this->ldap->getLastError($code); @@ -312,8 +316,9 @@ public function next() public function rewind() { if (is_resource($this->resultId)) { + $resource = $this->ldap->getResource(); ErrorHandler::start(); - $this->current = ldap_first_entry($this->ldap->getResource(), $this->resultId); + $this->current = ldap_first_entry($resource, $this->resultId); ErrorHandler::stop(); if ($this->current === false && $this->ldap->getLastErrorCode() > Exception\LdapException::LDAP_SUCCESS From a6fe452d2636b3b7a0cb99fcb11dc5b1dad231cb Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 31 Aug 2012 14:44:59 -0500 Subject: [PATCH 5/5] [zendframework/zf2#2284][ZF2-507] Updated README - Notice about Date header --- .coveralls.yml | 3 + .gitattributes | 6 + .gitignore | 14 + .php_cs | 43 + .travis.yml | 35 + CONTRIBUTING.md | 229 +++ LICENSE.txt | 27 + README.md | 8 + composer.json | 40 + phpunit.xml.dist | 83 + phpunit.xml.travis | 83 + src/Attribute.php | 376 ++++ src/Collection.php | 228 +++ src/Collection/DefaultIterator.php | 342 ++++ src/Converter/Converter.php | 395 +++++ .../Exception/ConverterException.php | 19 + .../Exception/ExceptionInterface.php | 19 + .../Exception/InvalidArgumentException.php | 19 + .../Exception/UnexpectedValueException.php | 19 + src/Dn.php | 770 +++++++++ src/Exception/BadMethodCallException.php | 19 + src/Exception/ExceptionInterface.php | 19 + src/Exception/InvalidArgumentException.php | 19 + src/Exception/LdapException.php | 134 ++ src/Filter.php | 240 +++ src/Filter/AbstractFilter.php | 131 ++ src/Filter/AbstractLogicalFilter.php | 88 + src/Filter/AndFilter.php | 31 + src/Filter/Exception/ExceptionInterface.php | 21 + src/Filter/Exception/FilterException.php | 20 + src/Filter/MaskFilter.php | 48 + src/Filter/NotFilter.php | 58 + src/Filter/OrFilter.php | 31 + src/Filter/StringFilter.php | 48 + src/Ldap.php | 1540 +++++++++++++++++ src/Ldif/Encoder.php | 299 ++++ src/Node.php | 1098 ++++++++++++ src/Node/AbstractNode.php | 468 +++++ src/Node/ChildrenIterator.php | 196 +++ src/Node/Collection.php | 47 + src/Node/RootDse.php | 127 ++ src/Node/RootDse/ActiveDirectory.php | 229 +++ src/Node/RootDse/OpenLdap.php | 87 + src/Node/RootDse/eDirectory.php | 145 ++ src/Node/Schema.php | 96 + src/Node/Schema/AbstractItem.php | 151 ++ src/Node/Schema/ActiveDirectory.php | 86 + .../Schema/AttributeType/ActiveDirectory.php | 84 + .../AttributeType/AttributeTypeInterface.php | 63 + src/Node/Schema/AttributeType/OpenLdap.php | 117 ++ .../Schema/ObjectClass/ActiveDirectory.php | 95 + .../ObjectClass/ObjectClassInterface.php | 71 + src/Node/Schema/ObjectClass/OpenLdap.php | 156 ++ src/Node/Schema/OpenLdap.php | 499 ++++++ test/AbstractOnlineTestCase.php | 148 ++ test/AbstractTestCase.php | 46 + test/AttributeTest.php | 507 ++++++ test/BindTest.php | 274 +++ test/CanonTest.php | 456 +++++ test/ChangePasswordTest.php | 213 +++ test/ConnectTest.php | 257 +++ test/Converter/ConverterTest.php | 255 +++ test/CopyRenameTest.php | 364 ++++ test/CrudTest.php | 498 ++++++ test/Dn/CreationTest.php | 201 +++ test/Dn/EscapingTest.php | 45 + test/Dn/ExplodingTest.php | 252 +++ test/Dn/ImplodingTest.php | 133 ++ test/Dn/MiscTest.php | 72 + test/Dn/ModificationTest.php | 316 ++++ test/FilterTest.php | 200 +++ test/Ldif/SimpleDecoderTest.php | 373 ++++ test/Ldif/SimpleEncoderTest.php | 255 +++ test/Node/AttributeIterationTest.php | 46 + test/Node/ChildrenIterationTest.php | 104 ++ test/Node/ChildrenTest.php | 188 ++ test/Node/OfflineTest.php | 664 +++++++ test/Node/OnlineTest.php | 277 +++ test/Node/RootDseTest.php | 175 ++ test/Node/SchemaTest.php | 317 ++++ test/Node/UpdateTest.php | 205 +++ test/OfflineTest.php | 131 ++ test/SearchTest.php | 621 +++++++ test/_files/AttributeTest.input.txt | 1 + test/bootstrap.php | 34 + 85 files changed, 16947 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 phpunit.xml.travis create mode 100644 src/Attribute.php create mode 100644 src/Collection.php create mode 100644 src/Collection/DefaultIterator.php create mode 100644 src/Converter/Converter.php create mode 100644 src/Converter/Exception/ConverterException.php create mode 100644 src/Converter/Exception/ExceptionInterface.php create mode 100644 src/Converter/Exception/InvalidArgumentException.php create mode 100644 src/Converter/Exception/UnexpectedValueException.php create mode 100644 src/Dn.php create mode 100644 src/Exception/BadMethodCallException.php create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/LdapException.php create mode 100644 src/Filter.php create mode 100644 src/Filter/AbstractFilter.php create mode 100644 src/Filter/AbstractLogicalFilter.php create mode 100644 src/Filter/AndFilter.php create mode 100644 src/Filter/Exception/ExceptionInterface.php create mode 100644 src/Filter/Exception/FilterException.php create mode 100644 src/Filter/MaskFilter.php create mode 100644 src/Filter/NotFilter.php create mode 100644 src/Filter/OrFilter.php create mode 100644 src/Filter/StringFilter.php create mode 100644 src/Ldap.php create mode 100644 src/Ldif/Encoder.php create mode 100644 src/Node.php create mode 100644 src/Node/AbstractNode.php create mode 100644 src/Node/ChildrenIterator.php create mode 100644 src/Node/Collection.php create mode 100644 src/Node/RootDse.php create mode 100644 src/Node/RootDse/ActiveDirectory.php create mode 100644 src/Node/RootDse/OpenLdap.php create mode 100644 src/Node/RootDse/eDirectory.php create mode 100644 src/Node/Schema.php create mode 100644 src/Node/Schema/AbstractItem.php create mode 100644 src/Node/Schema/ActiveDirectory.php create mode 100644 src/Node/Schema/AttributeType/ActiveDirectory.php create mode 100644 src/Node/Schema/AttributeType/AttributeTypeInterface.php create mode 100644 src/Node/Schema/AttributeType/OpenLdap.php create mode 100644 src/Node/Schema/ObjectClass/ActiveDirectory.php create mode 100644 src/Node/Schema/ObjectClass/ObjectClassInterface.php create mode 100644 src/Node/Schema/ObjectClass/OpenLdap.php create mode 100644 src/Node/Schema/OpenLdap.php create mode 100644 test/AbstractOnlineTestCase.php create mode 100644 test/AbstractTestCase.php create mode 100644 test/AttributeTest.php create mode 100644 test/BindTest.php create mode 100644 test/CanonTest.php create mode 100644 test/ChangePasswordTest.php create mode 100644 test/ConnectTest.php create mode 100644 test/Converter/ConverterTest.php create mode 100644 test/CopyRenameTest.php create mode 100644 test/CrudTest.php create mode 100644 test/Dn/CreationTest.php create mode 100644 test/Dn/EscapingTest.php create mode 100644 test/Dn/ExplodingTest.php create mode 100644 test/Dn/ImplodingTest.php create mode 100644 test/Dn/MiscTest.php create mode 100644 test/Dn/ModificationTest.php create mode 100644 test/FilterTest.php create mode 100644 test/Ldif/SimpleDecoderTest.php create mode 100644 test/Ldif/SimpleEncoderTest.php create mode 100644 test/Node/AttributeIterationTest.php create mode 100644 test/Node/ChildrenIterationTest.php create mode 100644 test/Node/ChildrenTest.php create mode 100644 test/Node/OfflineTest.php create mode 100644 test/Node/OnlineTest.php create mode 100644 test/Node/RootDseTest.php create mode 100644 test/Node/SchemaTest.php create mode 100644 test/Node/UpdateTest.php create mode 100644 test/OfflineTest.php create mode 100644 test/SearchTest.php create mode 100644 test/_files/AttributeTest.input.txt create mode 100644 test/bootstrap.php diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 000000000..53bda829c --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +coverage_clover: clover.xml +json_path: coveralls-upload.json +src_dir: src diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..85dc9a8c8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/test export-ignore +/vendor export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +.php_cs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4cac0a218 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.buildpath +.DS_Store +.idea +.project +.settings/ +.*.sw* +.*.un~ +nbproject +tmp/ + +clover.xml +coveralls-upload.json +phpunit.xml +vendor diff --git a/.php_cs b/.php_cs new file mode 100644 index 000000000..bf4b799f3 --- /dev/null +++ b/.php_cs @@ -0,0 +1,43 @@ +notPath('TestAsset') + ->notPath('_files') + ->filter(function (SplFileInfo $file) { + if (strstr($file->getPath(), 'compatibility')) { + return false; + } + }); +$config = Symfony\CS\Config\Config::create(); +$config->level(null); +$config->fixers( + array( + 'braces', + 'duplicate_semicolon', + 'elseif', + 'empty_return', + 'encoding', + 'eof_ending', + 'function_call_space', + 'function_declaration', + 'indentation', + 'join_function', + 'line_after_namespace', + 'linefeed', + 'lowercase_keywords', + 'parenthesis', + 'multiple_use', + 'method_argument_space', + 'object_operator', + 'php_closing_tag', + 'psr0', + 'remove_lines_between_uses', + 'short_tag', + 'standardize_not_equal', + 'trailing_spaces', + 'unused_use', + 'visibility', + 'whitespacy_lines', + ) +); +$config->finder($finder); +return $config; diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..fe909ecb1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: false + +language: php + +matrix: + fast_finish: true + include: + - php: 5.5 + - php: 5.6 + env: + - EXECUTE_TEST_COVERALLS=true + - EXECUTE_CS_CHECK=true + - php: 7 + - php: hhvm + allow_failures: + - php: 7 + - php: hhvm + +notifications: + irc: "irc.freenode.org#zftalk.dev" + email: false + +before_install: + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + +install: + - composer install --no-interaction --prefer-source + +script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis --coverage-clover clover.xml ; fi + - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then ./vendor/bin/phpunit -c phpunit.xml.travis ; fi + - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run --config-file=.php_cs ; fi + +after_script: + - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/coveralls ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..77f673050 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,229 @@ +# CONTRIBUTING + +## RESOURCES + +If you wish to contribute to Zend Framework, please be sure to +read/subscribe to the following resources: + + - [Coding Standards](/~https://github.com/zendframework/zf2/wiki/Coding-Standards) + - [Contributor's Guide](http://framework.zend.com/participate/contributor-guide) + - ZF Contributor's mailing list: + Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html + Subscribe: zf-contributors-subscribe@lists.zend.com + - ZF Contributor's IRC channel: + #zftalk.dev on Freenode.net + +If you are working on new features or refactoring [create a proposal](/~https://github.com/zendframework/zend-ldap/issues/new). + +## Reporting Potential Security Issues + +If you have encountered a potential security vulnerability, please **DO NOT** report it on the public +issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead. +We will work with you to verify the vulnerability and patch it as soon as possible. + +When reporting issues, please provide the following information: + +- Component(s) affected +- A description indicating how to reproduce the issue +- A summary of the security vulnerability and impact + +We request that you contact us via the email address above and give the project +contributors a chance to resolve the vulnerability and issue a new release prior +to any public exposure; this helps protect users and provides them with a chance +to upgrade and/or update in order to protect their applications. + +For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc). + +## RUNNING TESTS + +> ### Note: testing versions prior to 2.4 +> +> This component originates with Zend Framework 2. During the lifetime of ZF2, +> testing infrastructure migrated from PHPUnit 3 to PHPUnit 4. In most cases, no +> changes were necessary. However, due to the migration, tests may not run on +> versions < 2.4. As such, you may need to change the PHPUnit dependency if +> attempting a fix on such a version. + +To run tests: + +- Clone the repository: + + ```console + $ git clone git@github.com:zendframework/zend-ldap.git + $ cd + ``` + +- Install dependencies via composer: + + ```console + $ curl -sS https://getcomposer.org/installer | php -- + $ ./composer.phar install + ``` + + If you don't have `curl` installed, you can also download `composer.phar` from https://getcomposer.org/ + +- Run the tests via `phpunit` and the provided PHPUnit config, like in this example: + + ```console + $ ./vendor/bin/phpunit + ``` + +You can turn on conditional tests with the phpunit.xml file. +To do so: + + - Copy `phpunit.xml.dist` file to `phpunit.xml` + - Edit `phpunit.xml` to enable any specific functionality you + want to test, as well as to provide test values to utilize. + +## Running Coding Standards Checks + +This component uses [php-cs-fixer](http://cs.sensiolabs.org/) for coding +standards checks, and provides configuration for our selected checks. +`php-cs-fixer` is installed by default via Composer. + +To run checks only: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --dry-run --config-file=.php_cs +``` + +To have `php-cs-fixer` attempt to fix problems for you, omit the `--dry-run` +flag: + +```console +$ ./vendor/bin/php-cs-fixer fix . -v --diff --config-file=.php_cs +``` + +If you allow php-cs-fixer to fix CS issues, please re-run the tests to ensure +they pass, and make sure you add and commit the changes after verification. + +## Recommended Workflow for Contributions + +Your first step is to establish a public repository from which we can +pull your work into the master repository. We recommend using +[GitHub](https://github.com), as that is where the component is already hosted. + +1. Setup a [GitHub account](http://github.com/), if you haven't yet +2. Fork the repository (http://github.com/zendframework/zend-ldap) +3. Clone the canonical repository locally and enter it. + + ```console + $ git clone git://github.com:zendframework/zend-ldap.git + $ cd zend-ldap + ``` + +4. Add a remote to your fork; substitute your GitHub username in the command + below. + + ```console + $ git remote add {username} git@github.com:{username}/zend-ldap.git + $ git fetch {username} + ``` + +### Keeping Up-to-Date + +Periodically, you should update your fork or personal repository to +match the canonical ZF repository. Assuming you have setup your local repository +per the instructions above, you can do the following: + + +```console +$ git checkout master +$ git fetch origin +$ git rebase origin/master +# OPTIONALLY, to keep your remote up-to-date - +$ git push {username} master:master +``` + +If you're tracking other branches -- for example, the "develop" branch, where +new feature development occurs -- you'll want to do the same operations for that +branch; simply substitute "develop" for "master". + +### Working on a patch + +We recommend you do each new feature or bugfix in a new branch. This simplifies +the task of code review as well as the task of merging your changes into the +canonical repository. + +A typical workflow will then consist of the following: + +1. Create a new local branch based off either your master or develop branch. +2. Switch to your new local branch. (This step can be combined with the + previous step with the use of `git checkout -b`.) +3. Do some work, commit, repeat as necessary. +4. Push the local branch to your remote repository. +5. Send a pull request. + +The mechanics of this process are actually quite trivial. Below, we will +create a branch for fixing an issue in the tracker. + +```console +$ git checkout -b hotfix/9295 +Switched to a new branch 'hotfix/9295' +``` + +... do some work ... + + +```console +$ git commit +``` + +... write your log message ... + + +```console +$ git push {username} hotfix/9295:hotfix/9295 +Counting objects: 38, done. +Delta compression using up to 2 threads. +Compression objects: 100% (18/18), done. +Writing objects: 100% (20/20), 8.19KiB, done. +Total 20 (delta 12), reused 0 (delta 0) +To ssh://git@github.com/{username}/zend-ldap.git + b5583aa..4f51698 HEAD -> master +``` + +To send a pull request, you have two options. + +If using GitHub, you can do the pull request from there. Navigate to +your repository, select the branch you just created, and then select the +"Pull Request" button in the upper right. Select the user/organization +"zendframework" as the recipient. + +If using your own repository - or even if using GitHub - you can use `git +format-patch` to create a patchset for us to apply; in fact, this is +**recommended** for security-related patches. If you use `format-patch`, please +send the patches as attachments to: + +- zf-devteam@zend.com for patches without security implications +- zf-security@zend.com for security patches + +#### What branch to issue the pull request against? + +Which branch should you issue a pull request against? + +- For fixes against the stable release, issue the pull request against the + "master" branch. +- For new features, or fixes that introduce new elements to the public API (such + as new public methods or properties), issue the pull request against the + "develop" branch. + +### Branch Cleanup + +As you might imagine, if you are a frequent contributor, you'll start to +get a ton of branches both locally and on your remote. + +Once you know that your changes have been accepted to the master +repository, we suggest doing some cleanup of these branches. + +- Local branch cleanup + + ```console + $ git branch -d + ``` + +- Remote branch removal + + ```console + $ git push {username} : + ``` diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..6eab5aa14 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2015, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Zend Technologies USA, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..c333d0b8f --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# zend-ldap + +`Zend\Ldap\Ldap` is a class for performing LDAP operations including but not +limited to binding, searching and modifying entries in an LDAP directory. + + +- File issues at /~https://github.com/zendframework/zend-ldap/issues +- Documentation is at http://framework.zend.com/manual/current/en/index.html#zend-ldap diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..db21ae019 --- /dev/null +++ b/composer.json @@ -0,0 +1,40 @@ +{ + "name": "zendframework/zend-ldap", + "description": "provides support for LDAP operations including but not limited to binding, searching and modifying entries in an LDAP directory", + "license": "BSD-3-Clause", + "keywords": [ + "zf2", + "ldap" + ], + "homepage": "/~https://github.com/zendframework/zend-ldap", + "autoload": { + "psr-4": { + "Zend\\Ldap": "src/" + } + }, + "require": { + "php": ">=5.3.3", + "ext-ldap": "*", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "zendframework/zend-eventmanager": "self.version", + "fabpot/php-cs-fixer": "1.7.*", + "satooshi/php-coveralls": "dev-master", + "phpunit/PHPUnit": "~4.0" + }, + "suggest": { + "zendframework/zend-eventmanager": "Zend\\EventManager component" + }, + "extra": { + "branch-alias": { + "dev-master": "2.4-dev", + "dev-develop": "2.5-dev" + } + }, + "autoload-dev": { + "psr-4": { + "ZendTest\\Ldap\\": "test/" + } + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 000000000..a37ef2b98 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,83 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpunit.xml.travis b/phpunit.xml.travis new file mode 100644 index 000000000..a37ef2b98 --- /dev/null +++ b/phpunit.xml.travis @@ -0,0 +1,83 @@ + + + + + ./test/ + + + + + + disable + + + + + + ./src + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Attribute.php b/src/Attribute.php new file mode 100644 index 000000000..fe81e431d --- /dev/null +++ b/src/Attribute.php @@ -0,0 +1,376 @@ += 0 && $index < count($data[$attribName])) { + return self::valueFromLDAP($data[$attribName][$index]); + } else { + return null; + } + } + + return null; + } + + /** + * Checks if the given value(s) exist in the attribute + * + * @param array $data + * @param string $attribName + * @param mixed|array $value + * @return boolean + */ + public static function attributeHasValue(array &$data, $attribName, $value) + { + $attribName = strtolower($attribName); + if (!isset($data[$attribName])) { + return false; + } + + if (is_scalar($value)) { + $value = array($value); + } + + foreach ($value as $v) { + $v = self::valueToLDAP($v); + if (!in_array($v, $data[$attribName], true)) { + return false; + } + } + + return true; + } + + /** + * Removes duplicate values from a LDAP attribute + * + * @param array $data + * @param string $attribName + * @return void + */ + public static function removeDuplicatesFromAttribute(array &$data, $attribName) + { + $attribName = strtolower($attribName); + if (!isset($data[$attribName])) { + return; + } + $data[$attribName] = array_values(array_unique($data[$attribName])); + } + + /** + * Remove given values from a LDAP attribute + * + * @param array $data + * @param string $attribName + * @param mixed|array $value + * @return void + */ + public static function removeFromAttribute(array &$data, $attribName, $value) + { + $attribName = strtolower($attribName); + if (!isset($data[$attribName])) { + return; + } + + if (is_scalar($value)) { + $value = array($value); + } + + $valArray = array(); + foreach ($value as $v) { + $v = self::valueToLDAP($v); + if ($v !== null) { + $valArray[] = $v; + } + } + + $resultArray = $data[$attribName]; + foreach ($valArray as $rv) { + $keys = array_keys($resultArray, $rv); + foreach ($keys as $k) { + unset($resultArray[$k]); + } + } + $resultArray = array_values($resultArray); + $data[$attribName] = $resultArray; + } + + /** + * @param mixed $value + * @return string|null + */ + private static function valueToLdap($value) + { + return Converter\Converter::toLdap($value); + } + + /** + * @param string $value + * @return mixed + */ + private static function valueFromLdap($value) + { + try { + $return = Converter\Converter::fromLdap($value, Converter\Converter::STANDARD, false); + if ($return instanceof DateTime) { + return Converter\Converter::toLdapDateTime($return, false); + } else { + return $return; + } + } catch (Exception\InvalidArgumentException $e) { + return $value; + } + } + + /** + * Sets a LDAP password. + * + * @param array $data + * @param string $password + * @param string $hashType Optional by default MD5 + * @param string $attribName Optional + */ + public static function setPassword( + array &$data, $password, $hashType = self::PASSWORD_HASH_MD5, + $attribName = null + ) + { + if ($attribName === null) { + if ($hashType === self::PASSWORD_UNICODEPWD) { + $attribName = 'unicodePwd'; + } else { + $attribName = 'userPassword'; + } + } + + $hash = self::createPassword($password, $hashType); + self::setAttribute($data, $attribName, $hash, false); + } + + /** + * Creates a LDAP password. + * + * @param string $password + * @param string $hashType + * @return string + */ + public static function createPassword($password, $hashType = self::PASSWORD_HASH_MD5) + { + switch ($hashType) { + case self::PASSWORD_UNICODEPWD: + /* see: + * http://msdn.microsoft.com/en-us/library/cc223248(PROT.10).aspx + */ + $password = '"' . $password . '"'; + if (function_exists('mb_convert_encoding')) { + $password = mb_convert_encoding($password, 'UTF-16LE', 'UTF-8'); + } elseif (function_exists('iconv')) { + $password = iconv('UTF-8', 'UTF-16LE', $password); + } else { + $len = strlen($password); + $new = ''; + for ($i = 0; $i < $len; $i++) { + $new .= $password[$i] . "\x00"; + } + $password = $new; + } + return $password; + case self::PASSWORD_HASH_SSHA: + $salt = substr(sha1(uniqid(mt_rand(), true), true), 0, 4); + $rawHash = sha1($password . $salt, true) . $salt; + $method = '{SSHA}'; + break; + case self::PASSWORD_HASH_SHA: + $rawHash = sha1($password, true); + $method = '{SHA}'; + break; + case self::PASSWORD_HASH_SMD5: + $salt = substr(sha1(uniqid(mt_rand(), true), true), 0, 4); + $rawHash = md5($password . $salt, true) . $salt; + $method = '{SMD5}'; + break; + case self::PASSWORD_HASH_MD5: + default: + $rawHash = md5($password, true); + $method = '{MD5}'; + break; + } + return $method . base64_encode($rawHash); + } + + /** + * Sets a LDAP date/time attribute. + * + * @param array $data + * @param string $attribName + * @param integer|array|\Traversable $value + * @param boolean $utc + * @param boolean $append + */ + public static function setDateTimeAttribute( + array &$data, $attribName, $value, $utc = false, + $append = false + ) + { + $convertedValues = array(); + if (is_array($value) || ($value instanceof \Traversable)) { + foreach ($value as $v) { + $v = self::valueToLdapDateTime($v, $utc); + if ($v !== null) { + $convertedValues[] = $v; + } + } + } elseif ($value !== null) { + $value = self::valueToLdapDateTime($value, $utc); + if ($value !== null) { + $convertedValues[] = $value; + } + } + self::setAttribute($data, $attribName, $convertedValues, $append); + } + + /** + * @param integer $value + * @param boolean $utc + * @return string|null + */ + private static function valueToLdapDateTime($value, $utc) + { + if (is_int($value)) { + return Converter\Converter::toLdapDateTime($value, $utc); + } + + return null; + } + + /** + * Gets a LDAP date/time attribute. + * + * @param array $data + * @param string $attribName + * @param integer $index + * @return array|integer + */ + public static function getDateTimeAttribute(array $data, $attribName, $index = null) + { + $values = self::getAttribute($data, $attribName, $index); + if (is_array($values)) { + for ($i = 0; $i < count($values); $i++) { + $newVal = self::valueFromLdapDateTime($values[$i]); + if ($newVal !== null) { + $values[$i] = $newVal; + } + } + } else { + $newVal = self::valueFromLdapDateTime($values); + if ($newVal !== null) { + $values = $newVal; + } + } + + return $values; + } + + /** + * @param string|DateTime $value + * @return integer|null + */ + private static function valueFromLdapDateTime($value) + { + if ($value instanceof DateTime) { + return $value->format('U'); + } elseif (is_string($value)) { + try { + return Converter\Converter::fromLdapDateTime($value, false)->format('U'); + } catch (Converter\Exception\InvalidArgumentException $e) { + return null; + } + } + + return null; + } +} diff --git a/src/Collection.php b/src/Collection.php new file mode 100644 index 000000000..3f5b12aa4 --- /dev/null +++ b/src/Collection.php @@ -0,0 +1,228 @@ +iterator = $iterator; + } + + public function __destruct() + { + $this->close(); + } + + /** + * Closes the current result set + * + * @return boolean + */ + public function close() + { + return $this->iterator->close(); + } + + /** + * Get all entries as an array + * + * @return array + */ + public function toArray() + { + $data = array(); + foreach ($this as $item) { + $data[] = $item; + } + return $data; + } + + /** + * Get first entry + * + * @return array + */ + public function getFirst() + { + if ($this->count() > 0) { + $this->rewind(); + return $this->current(); + } else { + return null; + } + } + + /** + * Returns the underlying iterator + * + * @return Collection\DefaultIterator + */ + public function getInnerIterator() + { + return $this->iterator; + } + + /** + * Returns the number of items in current result + * Implements Countable + * + * @return int + */ + public function count() + { + return $this->iterator->count(); + } + + /** + * Return the current result item + * Implements Iterator + * + * @return array|null + * @throws Exception\LdapException + */ + 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 { + return null; + } + } + + /** + * Creates the data structure for the given entry data + * + * @param array $data + * @return array + */ + protected function createEntry(array $data) + { + return $data; + } + + /** + * Return the current result item DN + * + * @return string|null + */ + public function dn() + { + if ($this->count() > 0) { + if ($this->current < 0) { + $this->rewind(); + } + return $this->iterator->key(); + } else { + return null; + } + } + + /** + * Return the current result item key + * Implements Iterator + * + * @return int|null + */ + public function key() + { + if ($this->count() > 0) { + if ($this->current < 0) { + $this->rewind(); + } + return $this->current; + } else { + return null; + } + } + + /** + * Move forward to next result item + * Implements Iterator + * + * @throws Exception\LdapException + */ + public function next() + { + $this->iterator->next(); + $this->current++; + } + + /** + * Rewind the Iterator to the first result item + * Implements Iterator + * + * @throws Exception\LdapException + */ + public function rewind() + { + $this->iterator->rewind(); + $this->current = 0; + } + + /** + * Check if there is a current result item + * after calls to rewind() or next() + * Implements Iterator + * + * @return boolean + */ + public function valid() + { + if (isset($this->cache[$this->current])) { + return true; + } else { + return $this->iterator->valid(); + } + } +} diff --git a/src/Collection/DefaultIterator.php b/src/Collection/DefaultIterator.php new file mode 100644 index 000000000..7e2afbd48 --- /dev/null +++ b/src/Collection/DefaultIterator.php @@ -0,0 +1,342 @@ +ldap = $ldap; + $this->resultId = $resultId; + + $resource = $ldap->getResource(); + ErrorHandler::start(); + $this->itemCount = ldap_count_entries($resource, $resultId); + ErrorHandler::stop(); + if ($this->itemCount === false) { + throw new Exception\LdapException($this->ldap, 'counting entries'); + } + } + + public function __destruct() + { + $this->close(); + } + + /** + * Closes the current result set + * + * @return bool + */ + public function close() + { + $isClosed = false; + if (is_resource($this->resultId)) { + ErrorHandler::start(); + $isClosed = ldap_free_result($this->resultId); + ErrorHandler::stop(); + + $this->resultId = null; + $this->current = null; + } + return $isClosed; + } + + /** + * Gets the current LDAP connection. + * + * @return \Zend\Ldap\Ldap + */ + public function getLDAP() + { + return $this->ldap; + } + + /** + * Sets the attribute name treatment. + * + * Can either be one of the following constants + * - Zend\Ldap\Collection\DefaultIterator::ATTRIBUTE_TO_LOWER + * - Zend\Ldap\Collection\DefaultIterator::ATTRIBUTE_TO_UPPER + * - Zend\Ldap\Collection\DefaultIterator::ATTRIBUTE_NATIVE + * or a valid callback accepting the attribute's name as it's only + * argument and returning the new attribute's name. + * + * @param integer|callable $attributeNameTreatment + * @return DefaultIterator Provides a fluent interface + */ + public function setAttributeNameTreatment($attributeNameTreatment) + { + if (is_callable($attributeNameTreatment)) { + if (is_string($attributeNameTreatment) && !function_exists($attributeNameTreatment)) { + $this->attributeNameTreatment = self::ATTRIBUTE_TO_LOWER; + } elseif (is_array($attributeNameTreatment) + && !method_exists($attributeNameTreatment[0], $attributeNameTreatment[1]) + ) { + $this->attributeNameTreatment = self::ATTRIBUTE_TO_LOWER; + } else { + $this->attributeNameTreatment = $attributeNameTreatment; + } + } else { + $attributeNameTreatment = (int)$attributeNameTreatment; + switch ($attributeNameTreatment) { + case self::ATTRIBUTE_TO_LOWER: + case self::ATTRIBUTE_TO_UPPER: + case self::ATTRIBUTE_NATIVE: + $this->attributeNameTreatment = $attributeNameTreatment; + break; + default: + $this->attributeNameTreatment = self::ATTRIBUTE_TO_LOWER; + break; + } + } + + return $this; + } + + /** + * Returns the currently set attribute name treatment + * + * @return integer|callable + */ + public function getAttributeNameTreatment() + { + return $this->attributeNameTreatment; + } + + /** + * Returns the number of items in current result + * Implements Countable + * + * @return int + */ + public function count() + { + return $this->itemCount; + } + + /** + * Return the current result item + * Implements Iterator + * + * @return array|null + * @throws \Zend\Ldap\Exception\LdapException + */ + public function current() + { + if (!is_resource($this->current)) { + $this->rewind(); + } + if (!is_resource($this->current)) { + return null; + } + + $entry = array('dn' => $this->key()); + $ber_identifier = null; + + $resource = $this->ldap->getResource(); + ErrorHandler::start(); + $name = ldap_first_attribute( + $resource, $this->current, + $ber_identifier + ); + ErrorHandler::stop(); + + while ($name) { + ErrorHandler::start(); + $data = ldap_get_values_len($resource, $this->current, $name); + ErrorHandler::stop(); + + if (!$data) { + $data = array(); + } + + if (isset($data['count'])) { + unset($data['count']); + } + + switch ($this->attributeNameTreatment) { + case self::ATTRIBUTE_TO_LOWER: + $attrName = strtolower($name); + break; + case self::ATTRIBUTE_TO_UPPER: + $attrName = strtoupper($name); + break; + case self::ATTRIBUTE_NATIVE: + $attrName = $name; + break; + default: + $attrName = call_user_func($this->attributeNameTreatment, $name); + break; + } + $entry[$attrName] = $data; + + ErrorHandler::start(); + $name = ldap_next_attribute( + $resource, $this->current, + $ber_identifier + ); + ErrorHandler::stop(); + } + ksort($entry, SORT_LOCALE_STRING); + return $entry; + } + + /** + * Return the result item key + * Implements Iterator + * + * @throws \Zend\Ldap\Exception\LdapException + * @return string|null + */ + public function key() + { + if (!is_resource($this->current)) { + $this->rewind(); + } + if (is_resource($this->current)) { + $resource = $this->ldap->getResource(); + ErrorHandler::start(); + $currentDn = ldap_get_dn($resource, $this->current); + ErrorHandler::stop(); + + if ($currentDn === false) { + throw new Exception\LdapException($this->ldap, 'getting dn'); + } + + return $currentDn; + } else { + return null; + } + } + + /** + * Move forward to next result item + * Implements Iterator + * + * @throws \Zend\Ldap\Exception\LdapException + * @return + */ + public function next() + { + $code = 0; + + if (is_resource($this->current) && $this->itemCount > 0) { + $resource = $this->ldap->getResource(); + ErrorHandler::start(); + $this->current = ldap_next_entry($resource, $this->current); + ErrorHandler::stop(); + if ($this->current === false) { + $msg = $this->ldap->getLastError($code); + if ($code === Exception\LdapException::LDAP_SIZELIMIT_EXCEEDED) { + // we have reached the size limit enforced by the server + return; + } elseif ($code > Exception\LdapException::LDAP_SUCCESS) { + throw new Exception\LdapException($this->ldap, 'getting next entry (' . $msg . ')'); + } + } + } else { + $this->current = false; + } + } + + /** + * Rewind the Iterator to the first result item + * Implements Iterator + * + * + * @throws \Zend\Ldap\Exception\LdapException + */ + public function rewind() + { + if (is_resource($this->resultId)) { + $resource = $this->ldap->getResource(); + ErrorHandler::start(); + $this->current = ldap_first_entry($resource, $this->resultId); + ErrorHandler::stop(); + if ($this->current === false + && $this->ldap->getLastErrorCode() > Exception\LdapException::LDAP_SUCCESS + ) { + throw new Exception\LdapException($this->ldap, 'getting first entry'); + } + } + } + + /** + * Check if there is a current result item + * after calls to rewind() or next() + * Implements Iterator + * + * @return boolean + */ + public function valid() + { + return (is_resource($this->current)); + } +} diff --git a/src/Converter/Converter.php b/src/Converter/Converter.php new file mode 100644 index 000000000..30441c810 --- /dev/null +++ b/src/Converter/Converter.php @@ -0,0 +1,395 @@ + + * @link http://pear.php.net/package/Net_LDAP2 + * @author Benedikt Hallinger + * + * @param string $string String to convert + * @return string + */ + public static function ascToHex32($string) + { + for ($i = 0; $i < strlen($string); $i++) { + $char = substr($string, $i, 1); + if (ord($char) < 32) { + $hex = dechex(ord($char)); + if (strlen($hex) == 1) { + $hex = '0' . $hex; + } + $string = str_replace($char, '\\' . $hex, $string); + } + } + return $string; + } + + /** + * Converts all Hex expressions ("\HEX") to their original ASCII characters + * + * @see Net_LDAP2_Util::hex2asc() from Benedikt Hallinger , + * heavily based on work from DavidSmith@byu.net + * @link http://pear.php.net/package/Net_LDAP2 + * @author Benedikt Hallinger , heavily based on work from DavidSmith@byu.net + * + * @param string $string String to convert + * @return string + */ + public static function hex32ToAsc($string) + { + $string = preg_replace('/\\\([0-9A-Fa-f]{2})/e', "''.chr(hexdec('\\1')).''", $string); + return $string; + } + + + /** + * Convert any value to an LDAP-compatible value. + * + * By setting the $type-parameter the conversion of a certain + * type can be forced + * + * @todo write more tests + * + * @param mixed $value The value to convert + * @param int $type The conversion type to use + * @return string|null + * @throws Exception\ConverterException + */ + public static function toLdap($value, $type = self::STANDARD) + { + try { + switch ($type) { + case self::BOOLEAN: + return self::toldapBoolean($value); + break; + case self::GENERALIZED_TIME: + return self::toLdapDatetime($value); + break; + default: + if (is_string($value)) { + return $value; + } elseif (is_int($value) || is_float($value)) { + return (string)$value; + } elseif (is_bool($value)) { + return self::toldapBoolean($value); + } elseif (is_object($value)) { + if ($value instanceof DateTime) { + return self::toLdapDatetime($value); + } else { + return self::toLdapSerialize($value); + } + } elseif (is_array($value)) { + return self::toLdapSerialize($value); + } elseif (is_resource($value) && get_resource_type($value) === 'stream') { + return stream_get_contents($value); + } else { + return null; + } + break; + } + } catch (\Exception $e) { + throw new Exception\ConverterException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Converts a date-entity to an LDAP-compatible date-string + * + * The date-entity $date can be either a timestamp, a + * DateTime Object, a string that is parseable by strtotime(). + * + * @param integer|string|DateTime $date The date-entity + * @param boolean $asUtc Whether to return the LDAP-compatible date-string as UTC or as local value + * @return string + * @throws Exception\InvalidArgumentException + */ + public static function toLdapDateTime($date, $asUtc = true) + { + if (!($date instanceof DateTime)) { + if (is_int($date)) { + $date = new DateTime('@' . $date); + $date->setTimezone(new DateTimeZone(date_default_timezone_get())); + } elseif (is_string($date)) { + $date = new DateTime($date); + } else { + throw new Exception\InvalidArgumentException('Parameter $date is not of the expected type'); + } + } + $timezone = $date->format('O'); + if (true === $asUtc) { + $date->setTimezone(new DateTimeZone('UTC')); + $timezone = 'Z'; + } + if ('+0000' === $timezone) { + $timezone = 'Z'; + } + return $date->format('YmdHis') . $timezone; + } + + /** + * Convert a boolean value to an LDAP-compatible string + * + * This converts a boolean value of TRUE, an integer-value of 1 and a + * case-insensitive string 'true' to an LDAP-compatible 'TRUE'. All other + * other values are converted to an LDAP-compatible 'FALSE'. + * + * @param boolean|integer|string $value The boolean value to encode + * @return string + */ + public static function toLdapBoolean($value) + { + $return = 'FALSE'; + if (!is_scalar($value)) { + return $return; + } + if (true === $value || 'true' === strtolower($value) || 1 === $value) { + $return = 'TRUE'; + } + return $return; + } + + /** + * Serialize any value for storage in LDAP + * + * @param mixed $value The value to serialize + * @return string + */ + public static function toLdapSerialize($value) + { + return serialize($value); + } + + /** + * Convert an LDAP-compatible value to a corresponding PHP-value. + * + * By setting the $type-parameter the conversion of a certain + * type can be forced. + * + * @see Converter::STANDARD + * @see Converter::BOOLEAN + * @see Converter::GENERALIZED_TIME + * @param string $value The value to convert + * @param int $type The conversion type to use + * @param boolean $dateTimeAsUtc Return DateTime values in UTC timezone + * @return mixed + */ + public static function fromLdap($value, $type = self::STANDARD, $dateTimeAsUtc = true) + { + switch ($type) { + case self::BOOLEAN: + return self::fromldapBoolean($value); + break; + case self::GENERALIZED_TIME: + return self::fromLdapDateTime($value); + break; + default: + if (is_numeric($value)) { + // prevent numeric values to be treated as date/time + return $value; + } elseif ('TRUE' === $value || 'FALSE' === $value) { + return self::fromLdapBoolean($value); + } + if (preg_match('/^\d{4}[\d\+\-Z\.]*$/', $value)) { + return self::fromLdapDateTime($value, $dateTimeAsUtc); + } + try { + return self::fromLdapUnserialize($value); + } catch (Exception\UnexpectedValueException $e) { + // Do nothing + } + break; + } + + return $value; + } + + /** + * Convert an LDAP-Generalized-Time-entry into a DateTime-Object + * + * CAVEAT: The DateTime-Object returned will always be set to UTC-Timezone. + * + * @param string $date The generalized-Time + * @param boolean $asUtc Return the DateTime with UTC timezone + * @return DateTime + * @throws Exception\InvalidArgumentException if a non-parseable-format is given + */ + public static function fromLdapDateTime($date, $asUtc = true) + { + $datepart = array(); + if (!preg_match('/^(\d{4})/', $date, $datepart)) { + throw new Exception\InvalidArgumentException('Invalid date format found'); + } + + if ($datepart[1] < 4) { + throw new Exception\InvalidArgumentException('Invalid date format found (too short)'); + } + + $time = array( + // The year is mandatory! + 'year' => $datepart[1], + 'month' => 1, + 'day' => 1, + 'hour' => 0, + 'minute' => 0, + 'second' => 0, + 'offdir' => '+', + 'offsethours' => 0, + 'offsetminutes' => 0 + ); + + $length = strlen($date); + + // Check for month. + if ($length >= 6) { + $month = substr($date, 4, 2); + if ($month < 1 || $month > 12) { + throw new Exception\InvalidArgumentException('Invalid date format found (invalid month)'); + } + $time['month'] = $month; + } + + // Check for day + if ($length >= 8) { + $day = substr($date, 6, 2); + if ($day < 1 || $day > 31) { + throw new Exception\InvalidArgumentException('Invalid date format found (invalid day)'); + } + $time['day'] = $day; + } + + // Check for Hour + if ($length >= 10) { + $hour = substr($date, 8, 2); + if ($hour < 0 || $hour > 23) { + throw new Exception\InvalidArgumentException('Invalid date format found (invalid hour)'); + } + $time['hour'] = $hour; + } + + // Check for minute + if ($length >= 12) { + $minute = substr($date, 10, 2); + if ($minute < 0 || $minute > 59) { + throw new Exception\InvalidArgumentException('Invalid date format found (invalid minute)'); + } + $time['minute'] = $minute; + } + + // Check for seconds + if ($length >= 14) { + $second = substr($date, 12, 2); + if ($second < 0 || $second > 59) { + throw new Exception\InvalidArgumentException('Invalid date format found (invalid second)'); + } + $time['second'] = $second; + } + + // Set Offset + $offsetRegEx = '/([Z\-\+])(\d{2}\'?){0,1}(\d{2}\'?){0,1}$/'; + $off = array(); + if (preg_match($offsetRegEx, $date, $off)) { + $offset = $off[1]; + if ($offset == '+' || $offset == '-') { + $time['offdir'] = $offset; + // we have an offset, so lets calculate it. + if (isset($off[2])) { + $offsetHours = substr($off[2], 0, 2); + if ($offsetHours < 0 || $offsetHours > 12) { + throw new Exception\InvalidArgumentException('Invalid date format found (invalid offset hour)'); + } + $time['offsethours'] = $offsetHours; + } + if (isset($off[3])) { + $offsetMinutes = substr($off[3], 0, 2); + if ($offsetMinutes < 0 || $offsetMinutes > 59) { + throw new Exception\InvalidArgumentException('Invalid date format found (invalid offset minute)'); + } + $time['offsetminutes'] = $offsetMinutes; + } + } + } + + // Raw-Data is present, so lets create a DateTime-Object from it. + $offset = $time['offdir'] + . str_pad($time['offsethours'], 2, '0', STR_PAD_LEFT) + . str_pad($time['offsetminutes'], 2, '0', STR_PAD_LEFT); + $timestring = $time['year'] . '-' + . str_pad($time['month'], 2, '0', STR_PAD_LEFT) . '-' + . str_pad($time['day'], 2, '0', STR_PAD_LEFT) . ' ' + . str_pad($time['hour'], 2, '0', STR_PAD_LEFT) . ':' + . str_pad($time['minute'], 2, '0', STR_PAD_LEFT) . ':' + . str_pad($time['second'], 2, '0', STR_PAD_LEFT) + . $time['offdir'] + . str_pad($time['offsethours'], 2, '0', STR_PAD_LEFT) + . str_pad($time['offsetminutes'], 2, '0', STR_PAD_LEFT); + $date = new DateTime($timestring); + if ($asUtc) { + $date->setTimezone(new DateTimeZone('UTC')); + } + return $date; + } + + /** + * Convert an LDAP-compatible boolean value into a PHP-compatible one + * + * @param string $value The value to convert + * @return boolean + * @throws Exception\InvalidArgumentException + */ + public static function fromLdapBoolean($value) + { + if ('TRUE' === $value) { + return true; + } elseif ('FALSE' === $value) { + return false; + } else { + throw new Exception\InvalidArgumentException('The given value is not a boolean value'); + } + } + + /** + * Unserialize a serialized value to return the corresponding object + * + * @param string $value The value to convert + * @return mixed + * @throws Exception\UnexpectedValueException + */ + public static function fromLdapUnserialize($value) + { + ErrorHandler::start(E_NOTICE); + $v = unserialize($value); + ErrorHandler::stop(); + + if (false === $v && $value != 'b:0;') { + throw new Exception\UnexpectedValueException('The given value could not be unserialized'); + } + return $v; + } +} diff --git a/src/Converter/Exception/ConverterException.php b/src/Converter/Exception/ConverterException.php new file mode 100644 index 000000000..58e668e38 --- /dev/null +++ b/src/Converter/Exception/ConverterException.php @@ -0,0 +1,19 @@ +dn = $dn; + $this->setCaseFold($caseFold); + } + + /** + * Gets the RDN of the current DN + * + * @param string $caseFold + * @return array + * @throws Exception\LdapException if DN has no RDN (empty array) + */ + public function getRdn($caseFold = null) + { + $caseFold = self::sanitizeCaseFold($caseFold, $this->caseFold); + return self::caseFoldRdn($this->get(0, 1, $caseFold), null); + } + + /** + * Gets the RDN of the current DN as a string + * + * @param string $caseFold + * @return string + * @throws Exception\LdapException if DN has no RDN (empty array) + */ + public function getRdnString($caseFold = null) + { + $caseFold = self::sanitizeCaseFold($caseFold, $this->caseFold); + return self::implodeRdn($this->getRdn(), $caseFold); + } + + /** + * Get the parent DN $levelUp levels up the tree + * + * @param int $levelUp + * @throws Exception\LdapException + * @return Dn + */ + public function getParentDn($levelUp = 1) + { + $levelUp = (int)$levelUp; + if ($levelUp < 1 || $levelUp >= count($this->dn)) { + throw new Exception\LdapException(null, 'Cannot retrieve parent DN with given $levelUp'); + } + $newDn = array_slice($this->dn, $levelUp); + return new self($newDn, $this->caseFold); + } + + /** + * Get a DN part + * + * @param int $index + * @param int $length + * @param string $caseFold + * @return array + * @throws Exception\LdapException if index is illegal + */ + public function get($index, $length = 1, $caseFold = null) + { + $caseFold = self::sanitizeCaseFold($caseFold, $this->caseFold); + $this->assertIndex($index); + $length = (int)$length; + if ($length <= 0) { + $length = 1; + } + if ($length === 1) { + return self::caseFoldRdn($this->dn[$index], $caseFold); + } else { + return self::caseFoldDn(array_slice($this->dn, $index, $length, false), $caseFold); + } + } + + /** + * Set a DN part + * + * @param int $index + * @param array $value + * @return Dn Provides a fluent interface + * @throws Exception\LdapException if index is illegal + */ + public function set($index, array $value) + { + $this->assertIndex($index); + self::assertRdn($value); + $this->dn[$index] = $value; + return $this; + } + + /** + * Remove a DN part + * + * @param int $index + * @param int $length + * @return Dn Provides a fluent interface + * @throws Exception\LdapException if index is illegal + */ + public function remove($index, $length = 1) + { + $this->assertIndex($index); + $length = (int)$length; + if ($length <= 0) { + $length = 1; + } + array_splice($this->dn, $index, $length, null); + return $this; + } + + /** + * Append a DN part + * + * @param array $value + * @return Dn Provides a fluent interface + */ + public function append(array $value) + { + self::assertRdn($value); + $this->dn[] = $value; + return $this; + } + + /** + * Prepend a DN part + * + * @param array $value + * @return Dn Provides a fluent interface + */ + public function prepend(array $value) + { + self::assertRdn($value); + array_unshift($this->dn, $value); + return $this; + } + + /** + * Insert a DN part + * + * @param int $index + * @param array $value + * @return Dn Provides a fluent interface + * @throws Exception\LdapException if index is illegal + */ + public function insert($index, array $value) + { + $this->assertIndex($index); + self::assertRdn($value); + $first = array_slice($this->dn, 0, $index + 1); + $second = array_slice($this->dn, $index + 1); + $this->dn = array_merge($first, array($value), $second); + return $this; + } + + /** + * Assert index is correct and usable + * + * @param mixed $index + * @return boolean + * @throws Exception\LdapException + */ + protected function assertIndex($index) + { + if (!is_int($index)) { + throw new Exception\LdapException(null, 'Parameter $index must be an integer'); + } + if ($index < 0 || $index >= count($this->dn)) { + throw new Exception\LdapException(null, 'Parameter $index out of bounds'); + } + return true; + } + + /** + * Assert if value is in a correct RDN format + * + * @param array $value + * @return boolean + * @throws Exception\LdapException + */ + protected static function assertRdn(array $value) + { + if (count($value) < 1) { + throw new Exception\LdapException(null, 'RDN Array is malformed: it must have at least one item'); + } + + foreach (array_keys($value) as $key) { + if (!is_string($key)) { + throw new Exception\LdapException(null, 'RDN Array is malformed: it must use string keys'); + } + } + } + + /** + * Sets the case fold + * + * @param string|null $caseFold + */ + public function setCaseFold($caseFold) + { + $this->caseFold = self::sanitizeCaseFold($caseFold, self::$defaultCaseFold); + } + + /** + * Return DN as a string + * + * @param string $caseFold + * @return string + * @throws Exception\LdapException + */ + public function toString($caseFold = null) + { + $caseFold = self::sanitizeCaseFold($caseFold, $this->caseFold); + return self::implodeDn($this->dn, $caseFold); + } + + /** + * Return DN as an array + * + * @param string $caseFold + * @return array + */ + public function toArray($caseFold = null) + { + $caseFold = self::sanitizeCaseFold($caseFold, $this->caseFold); + + if ($caseFold === self::ATTR_CASEFOLD_NONE) { + return $this->dn; + } else { + return self::caseFoldDn($this->dn, $caseFold); + } + } + + /** + * Do a case folding on a RDN + * + * @param array $part + * @param string $caseFold + * @return array + */ + protected static function caseFoldRdn(array $part, $caseFold) + { + switch ($caseFold) { + case self::ATTR_CASEFOLD_UPPER: + return array_change_key_case($part, CASE_UPPER); + case self::ATTR_CASEFOLD_LOWER: + return array_change_key_case($part, CASE_LOWER); + case self::ATTR_CASEFOLD_NONE: + default: + return $part; + } + } + + /** + * Do a case folding on a DN ort part of it + * + * @param array $dn + * @param string $caseFold + * @return array + */ + protected static function caseFoldDn(array $dn, $caseFold) + { + $return = array(); + foreach ($dn as $part) { + $return[] = self::caseFoldRdn($part, $caseFold); + } + return $return; + } + + /** + * Cast to string representation {@see toString()} + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Required by the ArrayAccess implementation + * + * @param int $offset + * @return boolean + */ + public function offsetExists($offset) + { + $offset = (int)$offset; + if ($offset < 0 || $offset >= count($this->dn)) { + return false; + } else { + return true; + } + } + + /** + * Proxy to {@see get()} + * Required by the ArrayAccess implementation + * + * @param int $offset + * @return array + */ + public function offsetGet($offset) + { + return $this->get($offset, 1, null); + } + + /** + * Proxy to {@see set()} + * Required by the ArrayAccess implementation + * + * @param int $offset + * @param array $value + */ + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } + + /** + * Proxy to {@see remove()} + * Required by the ArrayAccess implementation + * + * @param int $offset + */ + public function offsetUnset($offset) + { + $this->remove($offset, 1); + } + + /** + * Sets the default case fold + * + * @param string $caseFold + */ + public static function setDefaultCaseFold($caseFold) + { + self::$defaultCaseFold = self::sanitizeCaseFold($caseFold, self::ATTR_CASEFOLD_NONE); + } + + /** + * Sanitizes the case fold + * + * @param string $caseFold + * @param string $default + * @return string + */ + protected static function sanitizeCaseFold($caseFold, $default) + { + switch ($caseFold) { + case self::ATTR_CASEFOLD_NONE: + case self::ATTR_CASEFOLD_UPPER: + case self::ATTR_CASEFOLD_LOWER: + return $caseFold; + break; + default: + return $default; + break; + } + } + + /** + * Escapes a DN value according to RFC 2253 + * + * Escapes the given VALUES according to RFC 2253 so that they can be safely used in LDAP DNs. + * The characters ",", "+", """, "\", "<", ">", ";", "#", " = " with a special meaning in RFC 2252 + * are preceeded by ba backslash. Control characters with an ASCII code < 32 are represented as \hexpair. + * Finally all leading and trailing spaces are converted to sequences of \20. + * @see Net_LDAP2_Util::escape_dn_value() from Benedikt Hallinger + * @link http://pear.php.net/package/Net_LDAP2 + * @author Benedikt Hallinger + * + * @param string|array $values An array containing the DN values that should be escaped + * @return array The array $values, but escaped + */ + public static function escapeValue($values = array()) + { + if (!is_array($values)) { + $values = array($values); + } + foreach ($values as $key => $val) { + // Escaping of filter meta characters + $val = str_replace( + array('\\', ',', '+', '"', '<', '>', ';', '#', '=',), + array('\\\\', '\,', '\+', '\"', '\<', '\>', '\;', '\#', '\='), $val + ); + $val = Converter\Converter::ascToHex32($val); + + // Convert all leading and trailing spaces to sequences of \20. + if (preg_match('/^(\s*)(.+?)(\s*)$/', $val, $matches)) { + $val = $matches[2]; + for ($i = 0; $i < strlen($matches[1]); $i++) { + $val = '\20' . $val; + } + for ($i = 0; $i < strlen($matches[3]); $i++) { + $val = $val . '\20'; + } + } + if (null === $val) { + $val = '\0'; + } // apply escaped "null" if string is empty + $values[$key] = $val; + } + return (count($values) == 1) ? $values[0] : $values; + } + + /** + * Undoes the conversion done by {@link escapeValue()}. + * + * Any escape sequence starting with a baskslash - hexpair or special character - + * will be transformed back to the corresponding character. + * @see Net_LDAP2_Util::escape_dn_value() from Benedikt Hallinger + * @link http://pear.php.net/package/Net_LDAP2 + * @author Benedikt Hallinger + * + * @param string|array $values Array of DN Values + * @return array Same as $values, but unescaped + */ + public static function unescapeValue($values = array()) + { + if (!is_array($values)) { + $values = array($values); + } + foreach ($values as $key => $val) { + // strip slashes from special chars + $val = str_replace( + array('\\\\', '\,', '\+', '\"', '\<', '\>', '\;', '\#', '\='), + array('\\', ',', '+', '"', '<', '>', ';', '#', '=',), $val + ); + $values[$key] = Converter\Converter::hex32ToAsc($val); + } + return (count($values) == 1) ? $values[0] : $values; + } + + /** + * Creates an array containing all parts of the given DN. + * + * Array will be of type + * array( + * array("cn" => "name1", "uid" => "user"), + * array("cn" => "name2"), + * array("dc" => "example"), + * array("dc" => "org") + * ) + * for a DN of cn=name1+uid=user,cn=name2,dc=example,dc=org. + * + * @param string $dn + * @param array $keys An optional array to receive DN keys (e.g. CN, OU, DC, ...) + * @param array $vals An optional array to receive DN values + * @param string $caseFold + * @return array + * @throws Exception\LdapException + */ + public static function explodeDn( + $dn, array &$keys = null, array &$vals = null, + $caseFold = self::ATTR_CASEFOLD_NONE + ) { + $k = array(); + $v = array(); + if (!self::checkDn($dn, $k, $v, $caseFold)) { + throw new Exception\LdapException(null, 'DN is malformed'); + } + $ret = array(); + for ($i = 0; $i < count($k); $i++) { + if (is_array($k[$i]) && is_array($v[$i]) && (count($k[$i]) === count($v[$i]))) { + $multi = array(); + for ($j = 0; $j < count($k[$i]); $j++) { + $key = $k[$i][$j]; + $val = $v[$i][$j]; + $multi[$key] = $val; + } + $ret[] = $multi; + } elseif (is_string($k[$i]) && is_string($v[$i])) { + $ret[] = array($k[$i] => $v[$i]); + } + } + if ($keys !== null) { + $keys = $k; + } + if ($vals !== null) { + $vals = $v; + } + return $ret; + } + + /** + * @param string $dn The DN to parse + * @param array $keys An optional array to receive DN keys (e.g. CN, OU, DC, ...) + * @param array $vals An optional array to receive DN values + * @param string $caseFold + * @return boolean True if the DN was successfully parsed or false if the string is not a valid DN. + */ + public static function checkDn( + $dn, array &$keys = null, array &$vals = null, + $caseFold = self::ATTR_CASEFOLD_NONE + ) { + /* This is a classic state machine parser. Each iteration of the + * loop processes one character. State 1 collects the key. When equals ( = ) + * is encountered the state changes to 2 where the value is collected + * until a comma (,) or semicolon (;) is encountered after which we switch back + * to state 1. If a backslash (\) is encountered, state 3 is used to collect the + * following character without engaging the logic of other states. + */ + $key = null; + $value = null; + $slen = strlen($dn); + $state = 1; + $ko = $vo = 0; + $multi = false; + $ka = array(); + $va = array(); + for ($di = 0; $di <= $slen; $di++) { + $ch = ($di == $slen) ? 0 : $dn[$di]; + switch ($state) { + case 1: // collect key + if ($ch === '=') { + $key = trim(substr($dn, $ko, $di - $ko)); + if ($caseFold == self::ATTR_CASEFOLD_LOWER) { + $key = strtolower($key); + } elseif ($caseFold == self::ATTR_CASEFOLD_UPPER) { + $key = strtoupper($key); + } + if (is_array($multi)) { + $keyId = strtolower($key); + if (in_array($keyId, $multi)) { + return false; + } + $ka[count($ka) - 1][] = $key; + $multi[] = $keyId; + } else { + $ka[] = $key; + } + $state = 2; + $vo = $di + 1; + } elseif ($ch === ',' || $ch === ';' || $ch === '+') { + return false; + } + break; + case 2: // collect value + if ($ch === '\\') { + $state = 3; + } elseif ($ch === ',' || $ch === ';' || $ch === 0 || $ch === '+') { + $value = self::unescapeValue(trim(substr($dn, $vo, $di - $vo))); + if (is_array($multi)) { + $va[count($va) - 1][] = $value; + } else { + $va[] = $value; + } + $state = 1; + $ko = $di + 1; + if ($ch === '+' && $multi === false) { + $lastKey = array_pop($ka); + $lastVal = array_pop($va); + $ka[] = array($lastKey); + $va[] = array($lastVal); + $multi = array(strtolower($lastKey)); + } elseif ($ch === ',' || $ch === ';' || $ch === 0) { + $multi = false; + } + } elseif ($ch === '=') { + return false; + } + break; + case 3: // escaped + $state = 2; + break; + } + } + + if ($keys !== null) { + $keys = $ka; + } + if ($vals !== null) { + $vals = $va; + } + + return ($state === 1 && $ko > 0); + } + + /** + * Returns a DN part in the form $attribute = $value + * + * This method supports the creation of multi-valued RDNs + * $part must contain an even number of elements. + * + * @param array $part + * @param string $caseFold + * @return string + * @throws Exception\LdapException + */ + public static function implodeRdn(array $part, $caseFold = null) + { + self::assertRdn($part); + $part = self::caseFoldRdn($part, $caseFold); + $rdnParts = array(); + foreach ($part as $key => $value) { + $value = self::escapeValue($value); + $keyId = strtolower($key); + $rdnParts[$keyId] = implode('=', array($key, $value)); + } + ksort($rdnParts, SORT_STRING); + + return implode('+', $rdnParts); + } + + /** + * Implodes an array in the form delivered by {@link explodeDn()} + * to a DN string. + * + * $dnArray must be of type + * array( + * array("cn" => "name1", "uid" => "user"), + * array("cn" => "name2"), + * array("dc" => "example"), + * array("dc" => "org") + * ) + * + * @param array $dnArray + * @param string $caseFold + * @param string $separator + * @return string + * @throws Exception\LdapException + */ + public static function implodeDn(array $dnArray, $caseFold = null, $separator = ',') + { + $parts = array(); + foreach ($dnArray as $p) { + $parts[] = self::implodeRdn($p, $caseFold); + } + + return implode($separator, $parts); + } + + /** + * Checks if given $childDn is beneath $parentDn subtree. + * + * @param string|Dn $childDn + * @param string|Dn $parentDn + * @return boolean + */ + public static function isChildOf($childDn, $parentDn) + { + try { + $keys = array(); + $vals = array(); + if ($childDn instanceof Dn) { + $cdn = $childDn->toArray(DN::ATTR_CASEFOLD_LOWER); + } else { + $cdn = self::explodeDn($childDn, $keys, $vals, DN::ATTR_CASEFOLD_LOWER); + } + if ($parentDn instanceof Dn) { + $pdn = $parentDn->toArray(DN::ATTR_CASEFOLD_LOWER); + } else { + $pdn = self::explodeDn($parentDn, $keys, $vals, DN::ATTR_CASEFOLD_LOWER); + } + } catch (Exception\LdapException $e) { + return false; + } + + $startIndex = count($cdn) - count($pdn); + if ($startIndex < 0) { + return false; + } + for ($i = 0; $i < count($pdn); $i++) { + if ($cdn[$i + $startIndex] != $pdn[$i]) { + return false; + } + } + return true; + } +} diff --git a/src/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php new file mode 100644 index 000000000..6f1f95409 --- /dev/null +++ b/src/Exception/BadMethodCallException.php @@ -0,0 +1,19 @@ +getLastError($code, $errorMessages) . ': '; + if ($code === 0) { + $message = ''; + $code = $oldCode; + } + } + if (empty($message)) { + if ($code > 0) { + $message = '0x' . dechex($code) . ': '; + } + } + + if (!empty($str)) { + $message .= $str; + } else { + $message .= 'no exception message'; + } + + parent::__construct($message, $code); + } +} diff --git a/src/Filter.php b/src/Filter.php new file mode 100644 index 000000000..2cc794c58 --- /dev/null +++ b/src/Filter.php @@ -0,0 +1,240 @@ +'; + const TYPE_GREATEROREQUAL = '>='; + const TYPE_LESS = '<'; + const TYPE_LESSOREQUAL = '<='; + const TYPE_APPROX = '~='; + + /** + * Creates an 'equals' filter. + * (attr=value) + * + * @param string $attr + * @param string $value + * @return Filter + */ + public static function equals($attr, $value) + { + return new self($attr, $value, self::TYPE_EQUALS, null, null); + } + + /** + * Creates a 'begins with' filter. + * (attr=value*) + * + * @param string $attr + * @param string $value + * @return Filter + */ + public static function begins($attr, $value) + { + return new self($attr, $value, self::TYPE_EQUALS, null, '*'); + } + + /** + * Creates an 'ends with' filter. + * (attr=*value) + * + * @param string $attr + * @param string $value + * @return Filter + */ + public static function ends($attr, $value) + { + return new self($attr, $value, self::TYPE_EQUALS, '*', null); + } + + /** + * Creates a 'contains' filter. + * (attr=*value*) + * + * @param string $attr + * @param string $value + * @return Filter + */ + public static function contains($attr, $value) + { + return new self($attr, $value, self::TYPE_EQUALS, '*', '*'); + } + + /** + * Creates a 'greater' filter. + * (attr>value) + * + * @param string $attr + * @param string $value + * @return Filter + */ + public static function greater($attr, $value) + { + return new self($attr, $value, self::TYPE_GREATER, null, null); + } + + /** + * Creates a 'greater or equal' filter. + * (attr>=value) + * + * @param string $attr + * @param string $value + * @return Filter + */ + public static function greaterOrEqual($attr, $value) + { + return new self($attr, $value, self::TYPE_GREATEROREQUAL, null, null); + } + + /** + * Creates a 'less' filter. + * (attrtoString(); + } + + /** + * Negates the filter. + * + * @return AbstractFilter + */ + public function negate() + { + return new NotFilter($this); + } + + /** + * Creates an 'and' filter. + * + * @param AbstractFilter $filter,... + * @return AndFilter + */ + public function addAnd($filter) + { + $fa = func_get_args(); + $args = array_merge(array($this), $fa); + return new AndFilter($args); + } + + /** + * Creates an 'or' filter. + * + * @param AbstractFilter $filter,... + * @return OrFilter + */ + public function addOr($filter) + { + $fa = func_get_args(); + $args = array_merge(array($this), $fa); + return new OrFilter($args); + } + + /** + * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters. + * + * Any control characters with an ACII code < 32 as well as the characters with special meaning in + * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a + * backslash followed by two hex digits representing the hexadecimal value of the character. + * @see Net_LDAP2_Util::escape_filter_value() from Benedikt Hallinger + * @link http://pear.php.net/package/Net_LDAP2 + * @author Benedikt Hallinger + * + * @param string|array $values Array of values to escape + * @return array Array $values, but escaped + */ + public static function escapeValue($values = array()) + { + if (!is_array($values)) { + $values = array($values); + } + foreach ($values as $key => $val) { + // Escaping of filter meta characters + $val = str_replace(array('\\', '*', '(', ')'), array('\5c', '\2a', '\28', '\29'), $val); + // ASCII < 32 escaping + $val = Converter::ascToHex32($val); + if (null === $val) { + $val = '\0'; // apply escaped "null" if string is empty + } + $values[$key] = $val; + } + return (count($values) == 1) ? $values[0] : $values; + } + + /** + * Undoes the conversion done by {@link escapeValue()}. + * + * Converts any sequences of a backslash followed by two hex digits into the corresponding character. + * @see Net_LDAP2_Util::escape_filter_value() from Benedikt Hallinger + * @link http://pear.php.net/package/Net_LDAP2 + * @author Benedikt Hallinger + * + * @param string|array $values Array of values to escape + * @return array Array $values, but unescaped + */ + public static function unescapeValue($values = array()) + { + if (!is_array($values)) { + $values = array($values); + } + foreach ($values as $key => $value) { + // Translate hex code into ascii + $values[$key] = Converter::hex32ToAsc($value); + } + return (count($values) == 1) ? $values[0] : $values; + } +} diff --git a/src/Filter/AbstractLogicalFilter.php b/src/Filter/AbstractLogicalFilter.php new file mode 100644 index 000000000..a09dadcd1 --- /dev/null +++ b/src/Filter/AbstractLogicalFilter.php @@ -0,0 +1,88 @@ + $s) { + if (is_string($s)) { + $subfilters[$key] = new StringFilter($s); + } elseif (!($s instanceof AbstractFilter)) { + throw new Exception\FilterException('Only strings or Zend\Ldap\Filter\AbstractFilter allowed.'); + } + } + $this->subfilters = $subfilters; + $this->symbol = $symbol; + } + + /** + * Adds a filter to this grouping filter. + * + * @param AbstractFilter $filter + * @return AbstractLogicalFilter + */ + public function addFilter(AbstractFilter $filter) + { + $new = clone $this; + $new->subfilters[] = $filter; + return $new; + } + + /** + * Returns a string representation of the filter. + * + * @return string + */ + public function toString() + { + $return = '(' . $this->symbol; + foreach ($this->subfilters as $sub) { + $return .= $sub->toString(); + } + $return .= ')'; + return $return; + } +} diff --git a/src/Filter/AndFilter.php b/src/Filter/AndFilter.php new file mode 100644 index 000000000..de21d83e7 --- /dev/null +++ b/src/Filter/AndFilter.php @@ -0,0 +1,31 @@ +filter; + } +} diff --git a/src/Filter/NotFilter.php b/src/Filter/NotFilter.php new file mode 100644 index 000000000..e50b8f0b6 --- /dev/null +++ b/src/Filter/NotFilter.php @@ -0,0 +1,58 @@ +filter = $filter; + } + + /** + * Negates the filter. + * + * @return AbstractFilter + */ + public function negate() + { + return $this->filter; + } + + /** + * Returns a string representation of the filter. + * + * @return string + */ + public function toString() + { + return '(!' . $this->filter->toString() . ')'; + } +} diff --git a/src/Filter/OrFilter.php b/src/Filter/OrFilter.php new file mode 100644 index 000000000..d53f8b671 --- /dev/null +++ b/src/Filter/OrFilter.php @@ -0,0 +1,31 @@ +filter = $filter; + } + + /** + * Returns a string representation of the filter. + * + * @return string + */ + public function toString() + { + return '(' . $this->filter . ')'; + } +} diff --git a/src/Ldap.php b/src/Ldap.php new file mode 100644 index 000000000..f3d997740 --- /dev/null +++ b/src/Ldap.php @@ -0,0 +1,1540 @@ +setOptions($options); + } + + /** + * Destructor. + * + * @return void + */ + public function __destruct() + { + $this->disconnect(); + } + + /** + * @return resource The raw LDAP extension resource. + */ + public function getResource() + { + if (!is_resource($this->resource) || $this->boundUser === false) { + $this->bind(); + } + + return $this->resource; + } + + /** + * Return the LDAP error number of the last LDAP command + * + * @return int + */ + public function getLastErrorCode() + { + ErrorHandler::start(E_WARNING); + $ret = ldap_get_option($this->resource, LDAP_OPT_ERROR_NUMBER, $err); + ErrorHandler::stop(); + if ($ret === true) { + if ($err <= -1 && $err >= -17) { + /* For some reason draft-ietf-ldapext-ldap-c-api-xx.txt error + * codes in OpenLDAP are negative values from -1 to -17. + */ + $err = Exception\LdapException::LDAP_SERVER_DOWN + (-$err - 1); + } + return $err; + } + + return 0; + } + + /** + * Return the LDAP error message of the last LDAP command + * + * @param int $errorCode + * @param array $errorMessages + * @return string + */ + public function getLastError(&$errorCode = null, array &$errorMessages = null) + { + $errorCode = $this->getLastErrorCode(); + $errorMessages = array(); + + /* The various error retrieval functions can return + * different things so we just try to collect what we + * can and eliminate dupes. + */ + ErrorHandler::start(E_WARNING); + $estr1 = ldap_error($this->resource); + ErrorHandler::stop(); + if ($errorCode !== 0 && $estr1 === 'Success') { + ErrorHandler::start(E_WARNING); + $estr1 = ldap_err2str($errorCode); + ErrorHandler::stop(); + } + if (!empty($estr1)) { + $errorMessages[] = $estr1; + } + + ErrorHandler::start(E_WARNING); + ldap_get_option($this->resource, LDAP_OPT_ERROR_STRING, $estr2); + ErrorHandler::stop(); + if (!empty($estr2) && !in_array($estr2, $errorMessages)) { + $errorMessages[] = $estr2; + } + + $message = ''; + if ($errorCode > 0) { + $message = '0x' . dechex($errorCode) . ' '; + } + + if (count($errorMessages) > 0) { + $message .= '(' . implode('; ', $errorMessages) . ')'; + } else { + $message .= '(no error message from LDAP)'; + } + + return $message; + } + + /** + * Get the currently bound user + * + * FALSE if no user is bound to the LDAP resource + * NULL if there has been an anonymous bind + * username of the currently bound user + * + * @return bool|null|string + */ + public function getBoundUser() + { + return $this->boundUser; + } + + /** + * Sets the options used in connecting, binding, etc. + * + * Valid option keys: + * host + * port + * useSsl + * username + * password + * bindRequiresDn + * baseDn + * accountCanonicalForm + * accountDomainName + * accountDomainNameShort + * accountFilterFormat + * allowEmptyPassword + * useStartTls + * optReferrals + * tryUsernameSplit + * networkTimeout + * + * @param array|Traversable $options Options used in connecting, binding, etc. + * @return Ldap Provides a fluent interface + * @throws Exception\LdapException + */ + public function setOptions($options) + { + if ($options instanceof Traversable) { + $options = iterator_to_array($options); + } + + $permittedOptions = array( + 'host' => null, + 'port' => 0, + 'useSsl' => false, + 'username' => null, + 'password' => null, + 'bindRequiresDn' => false, + 'baseDn' => null, + 'accountCanonicalForm' => null, + 'accountDomainName' => null, + 'accountDomainNameShort' => null, + 'accountFilterFormat' => null, + 'allowEmptyPassword' => false, + 'useStartTls' => false, + 'optReferrals' => false, + 'tryUsernameSplit' => true, + 'networkTimeout' => null, + ); + + foreach ($permittedOptions as $key => $val) { + if (array_key_exists($key, $options)) { + $val = $options[$key]; + unset($options[$key]); + /* Enforce typing. This eliminates issues like Zend\Config\Reader\Ini + * returning '1' as a string (ZF-3163). + */ + switch ($key) { + case 'port': + case 'accountCanonicalForm': + case 'networkTimeout': + $permittedOptions[$key] = (int)$val; + break; + case 'useSsl': + case 'bindRequiresDn': + case 'allowEmptyPassword': + case 'useStartTls': + case 'optReferrals': + case 'tryUsernameSplit': + $permittedOptions[$key] = ($val === true + || $val === '1' + || strcasecmp($val, 'true') == 0); + break; + default: + $permittedOptions[$key] = trim($val); + break; + } + } + } + if (count($options) > 0) { + $key = key($options); + throw new Exception\LdapException(null, "Unknown Zend\\Ldap\\Ldap option: $key"); + } + $this->options = $permittedOptions; + + return $this; + } + + /** + * @return array The current options. + */ + public function getOptions() + { + return $this->options; + } + + /** + * @return string The hostname of the LDAP server being used to + * authenticate accounts + */ + protected function getHost() + { + return $this->options['host']; + } + + /** + * @return int The port of the LDAP server or 0 to indicate that no port + * value is set + */ + protected function getPort() + { + return $this->options['port']; + } + + /** + * @return boolean The default SSL / TLS encrypted transport control + */ + protected function getUseSsl() + { + return $this->options['useSsl']; + } + + /** + * @return string The default acctname for binding + */ + protected function getUsername() + { + return $this->options['username']; + } + + /** + * @return string The default password for binding + */ + protected function getPassword() + { + return $this->options['password']; + } + + /** + * @return boolean Bind requires DN + */ + protected function getBindRequiresDn() + { + return $this->options['bindRequiresDn']; + } + + /** + * Gets the base DN under which objects of interest are located + * + * @return string + */ + public function getBaseDn() + { + return $this->options['baseDn']; + } + + /** + * @return integer Either ACCTNAME_FORM_BACKSLASH, ACCTNAME_FORM_PRINCIPAL or + * ACCTNAME_FORM_USERNAME indicating the form usernames should be canonicalized to. + */ + protected function getAccountCanonicalForm() + { + /* Account names should always be qualified with a domain. In some scenarios + * using non-qualified account names can lead to security vulnerabilities. If + * no account canonical form is specified, we guess based in what domain + * names have been supplied. + */ + $accountCanonicalForm = $this->options['accountCanonicalForm']; + if (!$accountCanonicalForm) { + $accountDomainName = $this->getAccountDomainName(); + $accountDomainNameShort = $this->getAccountDomainNameShort(); + if ($accountDomainNameShort) { + $accountCanonicalForm = self::ACCTNAME_FORM_BACKSLASH; + } else { + if ($accountDomainName) { + $accountCanonicalForm = self::ACCTNAME_FORM_PRINCIPAL; + } else { + $accountCanonicalForm = self::ACCTNAME_FORM_USERNAME; + } + } + } + + return $accountCanonicalForm; + } + + /** + * @return string The account domain name + */ + protected function getAccountDomainName() + { + return $this->options['accountDomainName']; + } + + /** + * @return string The short account domain name + */ + protected function getAccountDomainNameShort() + { + return $this->options['accountDomainNameShort']; + } + + /** + * @return string A format string for building an LDAP search filter to match + * an account + */ + protected function getAccountFilterFormat() + { + return $this->options['accountFilterFormat']; + } + + /** + * @return boolean Allow empty passwords + */ + protected function getAllowEmptyPassword() + { + return $this->options['allowEmptyPassword']; + } + + /** + * @return boolean The default SSL / TLS encrypted transport control + */ + protected function getUseStartTls() + { + return $this->options['useStartTls']; + } + + /** + * @return boolean Opt. Referrals + */ + protected function getOptReferrals() + { + return $this->options['optReferrals']; + } + + /** + * @return boolean Try splitting the username into username and domain + */ + protected function getTryUsernameSplit() + { + return $this->options['tryUsernameSplit']; + } + + /** + * @return int The value for network timeout when connect to the LDAP server. + */ + protected function getNetworkTimeout() + { + return $this->options['networkTimeout']; + } + + /** + * @param string $acctname + * @return string The LDAP search filter for matching directory accounts + */ + protected function getAccountFilter($acctname) + { + $dname = ''; + $aname = ''; + $this->splitName($acctname, $dname, $aname); + $accountFilterFormat = $this->getAccountFilterFormat(); + $aname = Filter\AbstractFilter::escapeValue($aname); + if ($accountFilterFormat) { + return sprintf($accountFilterFormat, $aname); + } + if (!$this->getBindRequiresDn()) { + // is there a better way to detect this? + return sprintf("(&(objectClass=user)(sAMAccountName=%s))", $aname); + } + + return sprintf("(&(objectClass=posixAccount)(uid=%s))", $aname); + } + + /** + * @param string $name The name to split + * @param string $dname The resulting domain name (this is an out parameter) + * @param string $aname The resulting account name (this is an out parameter) + * @return void + */ + protected function splitName($name, &$dname, &$aname) + { + $dname = null; + $aname = $name; + + if (!$this->getTryUsernameSplit()) { + return; + } + + $pos = strpos($name, '@'); + if ($pos) { + $dname = substr($name, $pos + 1); + $aname = substr($name, 0, $pos); + } else { + $pos = strpos($name, '\\'); + if ($pos) { + $dname = substr($name, 0, $pos); + $aname = substr($name, $pos + 1); + } + } + } + + /** + * @param string $acctname The name of the account + * @return string The DN of the specified account + * @throws Exception\LdapException + */ + protected function getAccountDn($acctname) + { + if (Dn::checkDn($acctname)) { + return $acctname; + } + $acctname = $this->getCanonicalAccountName($acctname, self::ACCTNAME_FORM_USERNAME); + $acct = $this->getAccount($acctname, array('dn')); + + return $acct['dn']; + } + + /** + * @param string $dname The domain name to check + * @return boolean + */ + protected function isPossibleAuthority($dname) + { + if ($dname === null) { + return true; + } + $accountDomainName = $this->getAccountDomainName(); + $accountDomainNameShort = $this->getAccountDomainNameShort(); + if ($accountDomainName === null && $accountDomainNameShort === null) { + return true; + } + if (strcasecmp($dname, $accountDomainName) == 0) { + return true; + } + if (strcasecmp($dname, $accountDomainNameShort) == 0) { + return true; + } + + return false; + } + + /** + * @param string $acctname The name to canonicalize + * @param int $form The desired form of canonicalization + * @return string The canonicalized name in the desired form + * @throws Exception\LdapException + */ + public function getCanonicalAccountName($acctname, $form = 0) + { + $dname = ''; + $uname = ''; + + $this->splitName($acctname, $dname, $uname); + + if (!$this->isPossibleAuthority($dname)) { + throw new Exception\LdapException(null, + "Binding domain is not an authority for user: $acctname", + Exception\LdapException::LDAP_X_DOMAIN_MISMATCH); + } + + if (!$uname) { + throw new Exception\LdapException(null, "Invalid account name syntax: $acctname"); + } + + if (function_exists('mb_strtolower')) { + $uname = mb_strtolower($uname, 'UTF-8'); + } else { + $uname = strtolower($uname); + } + + if ($form === 0) { + $form = $this->getAccountCanonicalForm(); + } + + switch ($form) { + case self::ACCTNAME_FORM_DN: + return $this->getAccountDn($acctname); + case self::ACCTNAME_FORM_USERNAME: + return $uname; + case self::ACCTNAME_FORM_BACKSLASH: + $accountDomainNameShort = $this->getAccountDomainNameShort(); + if (!$accountDomainNameShort) { + throw new Exception\LdapException(null, 'Option required: accountDomainNameShort'); + } + return "$accountDomainNameShort\\$uname"; + case self::ACCTNAME_FORM_PRINCIPAL: + $accountDomainName = $this->getAccountDomainName(); + if (!$accountDomainName) { + throw new Exception\LdapException(null, 'Option required: accountDomainName'); + } + return "$uname@$accountDomainName"; + default: + throw new Exception\LdapException(null, "Unknown canonical name form: $form"); + } + } + + /** + * @param string $acctname + * @param array $attrs An array of names of desired attributes + * @return array An array of the attributes representing the account + * @throws Exception\LdapException + */ + protected function getAccount($acctname, array $attrs = null) + { + $baseDn = $this->getBaseDn(); + if (!$baseDn) { + throw new Exception\LdapException(null, 'Base DN not set'); + } + + $accountFilter = $this->getAccountFilter($acctname); + if (!$accountFilter) { + throw new Exception\LdapException(null, 'Invalid account filter'); + } + + if (!is_resource($this->getResource())) { + $this->bind(); + } + + $accounts = $this->search($accountFilter, $baseDn, self::SEARCH_SCOPE_SUB, $attrs); + $count = $accounts->count(); + if ($count === 1) { + $acct = $accounts->getFirst(); + $accounts->close(); + + return $acct; + } else { + if ($count === 0) { + $code = Exception\LdapException::LDAP_NO_SUCH_OBJECT; + $str = "No object found for: $accountFilter"; + } else { + $code = Exception\LdapException::LDAP_OPERATIONS_ERROR; + $str = "Unexpected result count ($count) for: $accountFilter"; + } + } + $accounts->close(); + + throw new Exception\LdapException($this, $str, $code); + } + + /** + * @return Ldap Provides a fluent interface + */ + public function disconnect() + { + if (is_resource($this->resource)) { + ErrorHandler::start(E_WARNING); + ldap_unbind($this->resource); + ErrorHandler::stop(); + } + $this->resource = null; + $this->boundUser = false; + + return $this; + } + + /** + * To connect using SSL it seems the client tries to verify the server + * certificate by default. One way to disable this behavior is to set + * 'TLS_REQCERT never' in OpenLDAP's ldap.conf and restarting Apache. Or, + * if you really care about the server's cert you can put a cert on the + * web server. + * + * @param string $host The hostname of the LDAP server to connect to + * @param int $port The port number of the LDAP server to connect to + * @param boolean $useSsl Use SSL + * @param boolean $useStartTls Use STARTTLS + * @param int $networkTimeout The value for network timeout when connect to the LDAP server. + * @return Ldap Provides a fluent interface + * @throws Exception\LdapException + */ + public function connect($host = null, $port = null, $useSsl = null, $useStartTls = null, $networkTimeout = null) + { + if ($host === null) { + $host = $this->getHost(); + } + if ($port === null) { + $port = $this->getPort(); + } else { + $port = (int)$port; + } + if ($useSsl === null) { + $useSsl = $this->getUseSsl(); + } else { + $useSsl = (bool)$useSsl; + } + if ($useStartTls === null) { + $useStartTls = $this->getUseStartTls(); + } else { + $useStartTls = (bool)$useStartTls; + } + if ($networkTimeout === null) { + $networkTimeout = $this->getNetworkTimeout(); + } else { + $networkTimeout = (int)$networkTimeout; + } + + if (!$host) { + throw new Exception\LdapException(null, 'A host parameter is required'); + } + + $useUri = false; + /* Because ldap_connect doesn't really try to connect, any connect error + * will actually occur during the ldap_bind call. Therefore, we save the + * connect string here for reporting it in error handling in bind(). + */ + $hosts = array(); + if (preg_match_all('~ldap(?:i|s)?://~', $host, $hosts, PREG_SET_ORDER) > 0) { + $this->connectString = $host; + $useUri = true; + $useSsl = false; + } else { + if ($useSsl) { + $this->connectString = 'ldaps://' . $host; + $useUri = true; + } else { + $this->connectString = 'ldap://' . $host; + } + if ($port) { + $this->connectString .= ':' . $port; + } + } + + $this->disconnect(); + + + /* Only OpenLDAP 2.2 + supports URLs so if SSL is not requested, just + * use the old form. + */ + ErrorHandler::start(); + $resource = ($useUri) ? ldap_connect($this->connectString) : ldap_connect($host, $port); + ErrorHandler::stop(); + + if (is_resource($resource) === true) { + $this->resource = $resource; + $this->boundUser = false; + + $optReferrals = ($this->getOptReferrals()) ? 1 : 0; + ErrorHandler::start(E_WARNING); + if (ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3) + && ldap_set_option($resource, LDAP_OPT_REFERRALS, $optReferrals) + ) { + if ($networkTimeout) { + ldap_set_option($resource, LDAP_OPT_NETWORK_TIMEOUT, $networkTimeout); + } + if ($useSsl || !$useStartTls || ldap_start_tls($resource)) { + ErrorHandler::stop(); + return $this; + } + } + ErrorHandler::stop(); + + $zle = new Exception\LdapException($this, "$host:$port"); + $this->disconnect(); + throw $zle; + } + + throw new Exception\LdapException(null, "Failed to connect to LDAP server: $host:$port"); + } + + /** + * @param string $username The username for authenticating the bind + * @param string $password The password for authenticating the bind + * @return Ldap Provides a fluent interface + * @throws Exception\LdapException + */ + public function bind($username = null, $password = null) + { + $moreCreds = true; + + if ($username === null) { + $username = $this->getUsername(); + $password = $this->getPassword(); + $moreCreds = false; + } + + if (empty($username)) { + /* Perform anonymous bind + */ + $username = null; + $password = null; + } else { + /* Check to make sure the username is in DN form. + */ + if (!Dn::checkDn($username)) { + if ($this->getBindRequiresDn()) { + /* moreCreds stops an infinite loop if getUsername does not + * return a DN and the bind requires it + */ + if ($moreCreds) { + try { + $username = $this->getAccountDn($username); + } catch (Exception\LdapException $zle) { + switch ($zle->getCode()) { + case Exception\LdapException::LDAP_NO_SUCH_OBJECT: + case Exception\LdapException::LDAP_X_DOMAIN_MISMATCH: + case Exception\LdapException::LDAP_X_EXTENSION_NOT_LOADED: + throw $zle; + } + throw new Exception\LdapException(null, + 'Failed to retrieve DN for account: ' . $username . + ' [' . $zle->getMessage() . ']', + Exception\LdapException::LDAP_OPERATIONS_ERROR); + } + } else { + throw new Exception\LdapException(null, 'Binding requires username in DN form'); + } + } else { + $username = $this->getCanonicalAccountName( + $username, + $this->getAccountCanonicalForm() + ); + } + } + } + + if (!is_resource($this->resource)) { + $this->connect(); + } + + if ($username !== null && $password === '' && $this->getAllowEmptyPassword() !== true) { + $zle = new Exception\LdapException(null, + 'Empty password not allowed - see allowEmptyPassword option.'); + } else { + ErrorHandler::start(E_WARNING); + $bind = ldap_bind($this->resource, $username, $password); + ErrorHandler::stop(); + if ($bind) { + $this->boundUser = $username; + return $this; + } + + $message = ($username === null) ? $this->connectString : $username; + switch ($this->getLastErrorCode()) { + case Exception\LdapException::LDAP_SERVER_DOWN: + /* If the error is related to establishing a connection rather than binding, + * the connect string is more informative than the username. + */ + $message = $this->connectString; + } + + $zle = new Exception\LdapException($this, $message); + } + $this->disconnect(); + + throw $zle; + } + + /** + * A global LDAP search routine for finding information. + * + * Options can be either passed as single parameters according to the + * method signature or as an array with one or more of the following keys + * - filter + * - baseDn + * - scope + * - attributes + * - sort + * - collectionClass + * - sizelimit + * - timelimit + * + * @param string|Filter\AbstractFilter|array $filter + * @param string|Dn|null $basedn + * @param integer $scope + * @param array $attributes + * @param string|null $sort + * @param string|null $collectionClass + * @param integer $sizelimit + * @param integer $timelimit + * @return Collection + * @throws Exception\LdapException + */ + public function search($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB, array $attributes = array(), + $sort = null, $collectionClass = null, $sizelimit = 0, $timelimit = 0 + ) + { + if (is_array($filter)) { + $options = array_change_key_case($filter, CASE_LOWER); + foreach ($options as $key => $value) { + switch ($key) { + case 'filter': + case 'basedn': + case 'scope': + case 'sort': + $$key = $value; + break; + case 'attributes': + if (is_array($value)) { + $attributes = $value; + } + break; + case 'collectionclass': + $collectionClass = $value; + break; + case 'sizelimit': + case 'timelimit': + $$key = (int)$value; + break; + } + } + } + + if ($basedn === null) { + $basedn = $this->getBaseDn(); + } elseif ($basedn instanceof Dn) { + $basedn = $basedn->toString(); + } + + if ($filter instanceof Filter\AbstractFilter) { + $filter = $filter->toString(); + } + + $resource = $this->getResource(); + ErrorHandler::start(E_WARNING); + switch ($scope) { + case self::SEARCH_SCOPE_ONE: + $search = ldap_list($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit); + break; + case self::SEARCH_SCOPE_BASE: + $search = ldap_read($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit); + break; + case self::SEARCH_SCOPE_SUB: + default: + $search = ldap_search($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit); + break; + } + ErrorHandler::stop(); + + if ($search === false) { + throw new Exception\LdapException($this, 'searching: ' . $filter); + } + if ($sort !== null && is_string($sort)) { + ErrorHandler::start(E_WARNING); + $isSorted = ldap_sort($resource, $search, $sort); + ErrorHandler::stop(); + if ($isSorted === false) { + throw new Exception\LdapException($this, 'sorting: ' . $sort); + } + } + + $iterator = new Collection\DefaultIterator($this, $search); + + return $this->createCollection($iterator, $collectionClass); + } + + /** + * Extension point for collection creation + * + * @param Collection\DefaultIterator $iterator + * @param string|null $collectionClass + * @return Collection + * @throws Exception\LdapException + */ + protected function createCollection(Collection\DefaultIterator $iterator, $collectionClass) + { + if ($collectionClass === null) { + return new Collection($iterator); + } else { + $collectionClass = (string)$collectionClass; + if (!class_exists($collectionClass)) { + throw new Exception\LdapException(null, + "Class '$collectionClass' can not be found"); + } + if (!is_subclass_of($collectionClass, 'Zend\Ldap\Collection')) { + throw new Exception\LdapException(null, + "Class '$collectionClass' must subclass 'Zend\\Ldap\\Collection'"); + } + + return new $collectionClass($iterator); + } + } + + /** + * Count items found by given filter. + * + * @param string|Filter\AbstractFilter $filter + * @param string|Dn|null $basedn + * @param integer $scope + * @return integer + * @throws Exception\LdapException + */ + public function count($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB) + { + try { + $result = $this->search($filter, $basedn, $scope, array('dn'), null); + } catch (Exception\LdapException $e) { + if ($e->getCode() === Exception\LdapException::LDAP_NO_SUCH_OBJECT) { + return 0; + } else { + throw $e; + } + } + + return $result->count(); + } + + /** + * Count children for a given DN. + * + * @param string|Dn $dn + * @return integer + * @throws Exception\LdapException + */ + public function countChildren($dn) + { + return $this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_ONE); + } + + /** + * Check if a given DN exists. + * + * @param string|Dn $dn + * @return boolean + * @throws Exception\LdapException + */ + public function exists($dn) + { + return ($this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_BASE) == 1); + } + + /** + * Search LDAP registry for entries matching filter and optional attributes + * + * Options can be either passed as single parameters according to the + * method signature or as an array with one or more of the following keys + * - filter + * - baseDn + * - scope + * - attributes + * - sort + * - reverseSort + * - sizelimit + * - timelimit + * + * @param string|Filter\AbstractFilter|array $filter + * @param string|Dn|null $basedn + * @param integer $scope + * @param array $attributes + * @param string|null $sort + * @param boolean $reverseSort + * @param integer $sizelimit + * @param integer $timelimit + * @return array + * @throws Exception\LdapException + */ + public function searchEntries($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB, + array $attributes = array(), $sort = null, $reverseSort = false, $sizelimit = 0, + $timelimit = 0) + { + if (is_array($filter)) { + $filter = array_change_key_case($filter, CASE_LOWER); + if (isset($filter['collectionclass'])) { + unset($filter['collectionclass']); + } + if (isset($filter['reversesort'])) { + $reverseSort = $filter['reversesort']; + unset($filter['reversesort']); + } + } + $result = $this->search($filter, $basedn, $scope, $attributes, $sort, null, $sizelimit, $timelimit); + $items = $result->toArray(); + if ((bool)$reverseSort === true) { + $items = array_reverse($items, false); + } + + return $items; + } + + /** + * Get LDAP entry by DN + * + * @param string|Dn $dn + * @param array $attributes + * @param boolean $throwOnNotFound + * @return array + * @throws null|Exception\LdapException + */ + public function getEntry($dn, array $attributes = array(), $throwOnNotFound = false) + { + try { + $result = $this->search( + "(objectClass=*)", $dn, self::SEARCH_SCOPE_BASE, + $attributes, null + ); + + return $result->getFirst(); + } catch (Exception\LdapException $e) { + if ($throwOnNotFound !== false) { + throw $e; + } + } + + return null; + } + + /** + * Prepares an ldap data entry array for insert/update operation + * + * @param array $entry + * @throws Exception\InvalidArgumentException + * @return void + */ + public static function prepareLdapEntryArray(array &$entry) + { + if (array_key_exists('dn', $entry)) { + unset($entry['dn']); + } + foreach ($entry as $key => $value) { + if (is_array($value)) { + foreach ($value as $i => $v) { + if ($v === null) { + unset($value[$i]); + } elseif (!is_scalar($v)) { + throw new Exception\InvalidArgumentException('Only scalar values allowed in LDAP data'); + } else { + $v = (string)$v; + if (strlen($v) == 0) { + unset($value[$i]); + } else { + $value[$i] = $v; + } + } + } + $entry[$key] = array_values($value); + } else { + if ($value === null) { + $entry[$key] = array(); + } elseif (!is_scalar($value)) { + throw new Exception\InvalidArgumentException('Only scalar values allowed in LDAP data'); + } else { + $value = (string)$value; + if (strlen($value) == 0) { + $entry[$key] = array(); + } else { + $entry[$key] = array($value); + } + } + } + } + $entry = array_change_key_case($entry, CASE_LOWER); + } + + /** + * Add new information to the LDAP repository + * + * @param string|Dn $dn + * @param array $entry + * @return Ldap Provides a fluid interface + * @throws Exception\LdapException + */ + public function add($dn, array $entry) + { + if (!($dn instanceof Dn)) { + $dn = Dn::factory($dn, null); + } + self::prepareLdapEntryArray($entry); + foreach ($entry as $key => $value) { + if (is_array($value) && count($value) === 0) { + unset($entry[$key]); + } + } + + $rdnParts = $dn->getRdn(Dn::ATTR_CASEFOLD_LOWER); + foreach ($rdnParts as $key => $value) { + $value = Dn::unescapeValue($value); + if (!array_key_exists($key, $entry)) { + $entry[$key] = array($value); + } elseif (!in_array($value, $entry[$key])) { + $entry[$key] = array_merge(array($value), $entry[$key]); + } + } + $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory', + 'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated'); + foreach ($adAttributes as $attr) { + if (array_key_exists($attr, $entry)) { + unset($entry[$attr]); + } + } + + $resource = $this->getResource(); + ErrorHandler::start(E_WARNING); + $isAdded = ldap_add($resource, $dn->toString(), $entry); + ErrorHandler::stop(); + if ($isAdded === false) { + throw new Exception\LdapException($this, 'adding: ' . $dn->toString()); + } + + return $this; + } + + /** + * Update LDAP registry + * + * @param string|Dn $dn + * @param array $entry + * @return Ldap Provides a fluid interface + * @throws Exception\LdapException + */ + public function update($dn, array $entry) + { + if (!($dn instanceof Dn)) { + $dn = Dn::factory($dn, null); + } + self::prepareLdapEntryArray($entry); + + $rdnParts = $dn->getRdn(Dn::ATTR_CASEFOLD_LOWER); + foreach ($rdnParts as $key => $value) { + $value = Dn::unescapeValue($value); + if (array_key_exists($key, $entry) && !in_array($value, $entry[$key])) { + $entry[$key] = array_merge(array($value), $entry[$key]); + } + } + $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory', + 'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated'); + foreach ($adAttributes as $attr) { + if (array_key_exists($attr, $entry)) { + unset($entry[$attr]); + } + } + + if (count($entry) > 0) { + $resource = $this->getResource(); + ErrorHandler::start(E_WARNING); + $isModified = ldap_modify($resource, $dn->toString(), $entry); + ErrorHandler::stop(); + if ($isModified === false) { + throw new Exception\LdapException($this, 'updating: ' . $dn->toString()); + } + } + + return $this; + } + + /** + * Save entry to LDAP registry. + * + * Internally decides if entry will be updated to added by calling + * {@link exists()}. + * + * @param string|Dn $dn + * @param array $entry + * @return Ldap Provides a fluid interface + * @throws Exception\LdapException + */ + public function save($dn, array $entry) + { + if ($dn instanceof Dn) { + $dn = $dn->toString(); + } + if ($this->exists($dn)) { + $this->update($dn, $entry); + } else { + $this->add($dn, $entry); + } + + return $this; + } + + /** + * Delete an LDAP entry + * + * @param string|Dn $dn + * @param boolean $recursively + * @return Ldap Provides a fluid interface + * @throws Exception\LdapException + */ + public function delete($dn, $recursively = false) + { + if ($dn instanceof Dn) { + $dn = $dn->toString(); + } + if ($recursively === true) { + if ($this->countChildren($dn) > 0) { + $children = $this->getChildrenDns($dn); + foreach ($children as $c) { + $this->delete($c, true); + } + } + } + + $resource = $this->getResource(); + ErrorHandler::start(E_WARNING); + $isDeleted = ldap_delete($resource, $dn); + ErrorHandler::stop(); + if ($isDeleted === false) { + throw new Exception\LdapException($this, 'deleting: ' . $dn); + } + + return $this; + } + + /** + * Retrieve the immediate children DNs of the given $parentDn + * + * This method is used in recursive methods like {@see delete()} + * or {@see copy()} + * + * @param string|Dn $parentDn + * @throws Exception\LdapException + * @return array of DNs + */ + protected function getChildrenDns($parentDn) + { + if ($parentDn instanceof Dn) { + $parentDn = $parentDn->toString(); + } + $children = array(); + + $resource = $this->getResource(); + ErrorHandler::start(E_WARNING); + $search = ldap_list($resource, $parentDn, '(objectClass=*)', array('dn')); + for ( + $entry = ldap_first_entry($resource, $search); + $entry !== false; + $entry = ldap_next_entry($resource, $entry) + ) { + $childDn = ldap_get_dn($resource, $entry); + if ($childDn === false) { + ErrorHandler::stop(); + throw new Exception\LdapException($this, 'getting dn'); + } + $children[] = $childDn; + } + ldap_free_result($search); + ErrorHandler::stop(); + + return $children; + } + + /** + * Moves a LDAP entry from one DN to another subtree. + * + * @param string|Dn $from + * @param string|Dn $to + * @param boolean $recursively + * @param boolean $alwaysEmulate + * @return Ldap Provides a fluid interface + * @throws Exception\LdapException + */ + public function moveToSubtree($from, $to, $recursively = false, $alwaysEmulate = false) + { + if ($from instanceof Dn) { + $orgDnParts = $from->toArray(); + } else { + $orgDnParts = Dn::explodeDn($from); + } + + if ($to instanceof Dn) { + $newParentDnParts = $to->toArray(); + } else { + $newParentDnParts = Dn::explodeDn($to); + } + + $newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts); + $newDn = Dn::fromArray($newDnParts); + + return $this->rename($from, $newDn, $recursively, $alwaysEmulate); + } + + /** + * Moves a LDAP entry from one DN to another DN. + * + * This is an alias for {@link rename()} + * + * @param string|Dn $from + * @param string|Dn $to + * @param boolean $recursively + * @param boolean $alwaysEmulate + * @return Ldap Provides a fluid interface + * @throws Exception\LdapException + */ + public function move($from, $to, $recursively = false, $alwaysEmulate = false) + { + return $this->rename($from, $to, $recursively, $alwaysEmulate); + } + + /** + * Renames a LDAP entry from one DN to another DN. + * + * This method implicitly moves the entry to another location within the tree. + * + * @param string|Dn $from + * @param string|Dn $to + * @param boolean $recursively + * @param boolean $alwaysEmulate + * @return Ldap Provides a fluid interface + * @throws Exception\LdapException + */ + public function rename($from, $to, $recursively = false, $alwaysEmulate = false) + { + $emulate = (bool)$alwaysEmulate; + if (!function_exists('ldap_rename')) { + $emulate = true; + } elseif ($recursively) { + $emulate = true; + } + + if ($emulate === false) { + if ($from instanceof Dn) { + $from = $from->toString(); + } + + if ($to instanceof Dn) { + $newDnParts = $to->toArray(); + } else { + $newDnParts = Dn::explodeDn($to); + } + + $newRdn = Dn::implodeRdn(array_shift($newDnParts)); + $newParent = Dn::implodeDn($newDnParts); + + $resource = $this->getResource(); + ErrorHandler::start(E_WARNING); + $isOK = ldap_rename($resource, $from, $newRdn, $newParent, true); + ErrorHandler::stop(); + if ($isOK === false) { + throw new Exception\LdapException($this, 'renaming ' . $from . ' to ' . $to); + } elseif (!$this->exists($to)) { + $emulate = true; + } + } + if ($emulate) { + $this->copy($from, $to, $recursively); + $this->delete($from, $recursively); + } + + return $this; + } + + /** + * Copies a LDAP entry from one DN to another subtree. + * + * @param string|Dn $from + * @param string|Dn $to + * @param boolean $recursively + * @return Ldap Provides a fluid interface + * @throws Exception\LdapException + */ + public function copyToSubtree($from, $to, $recursively = false) + { + if ($from instanceof Dn) { + $orgDnParts = $from->toArray(); + } else { + $orgDnParts = Dn::explodeDn($from); + } + + if ($to instanceof Dn) { + $newParentDnParts = $to->toArray(); + } else { + $newParentDnParts = Dn::explodeDn($to); + } + + $newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts); + $newDn = Dn::fromArray($newDnParts); + + return $this->copy($from, $newDn, $recursively); + } + + /** + * Copies a LDAP entry from one DN to another DN. + * + * @param string|Dn $from + * @param string|Dn $to + * @param boolean $recursively + * @return Ldap Provides a fluid interface + * @throws Exception\LdapException + */ + public function copy($from, $to, $recursively = false) + { + $entry = $this->getEntry($from, array(), true); + + if ($to instanceof Dn) { + $toDnParts = $to->toArray(); + } else { + $toDnParts = Dn::explodeDn($to); + } + $this->add($to, $entry); + + if ($recursively === true && $this->countChildren($from) > 0) { + $children = $this->getChildrenDns($from); + foreach ($children as $c) { + $cDnParts = Dn::explodeDn($c); + $newChildParts = array_merge(array(array_shift($cDnParts)), $toDnParts); + $newChild = Dn::implodeDn($newChildParts); + $this->copy($c, $newChild, true); + } + } + + return $this; + } + + /** + * Returns the specified DN as a Zend\Ldap\Node + * + * @param string|Dn $dn + * @return Node|null + * @throws Exception\LdapException + */ + public function getNode($dn) + { + return Node::fromLdap($dn, $this); + } + + /** + * Returns the base node as a Zend\Ldap\Node + * + * @return Node + * @throws Exception\LdapException + */ + public function getBaseNode() + { + return $this->getNode($this->getBaseDn(), $this); + } + + /** + * Returns the RootDse + * + * @return Node\RootDse + * @throws Exception\LdapException + */ + public function getRootDse() + { + if ($this->rootDse === null) { + $this->rootDse = Node\RootDse::create($this); + } + + return $this->rootDse; + } + + /** + * Returns the schema + * + * @return Node\Schema + * @throws Exception\LdapException + */ + public function getSchema() + { + if ($this->schema === null) { + $this->schema = Node\Schema::create($this); + } + + return $this->schema; + } +} diff --git a/src/Ldif/Encoder.php b/src/Ldif/Encoder.php new file mode 100644 index 000000000..681f92cbf --- /dev/null +++ b/src/Ldif/Encoder.php @@ -0,0 +1,299 @@ + true, + 'version' => 1, + 'wrap' => 78 + ); + + /** + * @var boolean + */ + protected $versionWritten = false; + + /** + * Constructor. + * + * @param array $options Additional options used during encoding + */ + protected function __construct(array $options = array()) + { + $this->options = array_merge($this->options, $options); + } + + /** + * Decodes the string $string into an array of Ldif items + * + * @param string $string + * @return array + */ + public static function decode($string) + { + $encoder = new self(array()); + return $encoder->_decode($string); + } + + /** + * Decodes the string $string into an array of Ldif items + * + * @param string $string + * @return array + */ + protected function _decode($string) + { + $items = array(); + $item = array(); + $last = null; + foreach (explode("\n", $string) as $line) { + $line = rtrim($line, "\x09\x0A\x0D\x00\x0B"); + $matches = array(); + if (substr($line, 0, 1) === ' ' && $last !== null) { + $last[2] .= substr($line, 1); + } elseif (substr($line, 0, 1) === '#') { + continue; + } elseif (preg_match('/^([a-z0-9;-]+)(:[:<]?\s*)([^:<]*)$/i', $line, $matches)) { + $name = strtolower($matches[1]); + $type = trim($matches[2]); + $value = $matches[3]; + if ($last !== null) { + $this->pushAttribute($last, $item); + } + if ($name === 'version') { + continue; + } elseif (count($item) > 0 && $name === 'dn') { + $items[] = $item; + $item = array(); + $last = null; + } + $last = array($name, $type, $value); + } elseif (trim($line) === '') { + continue; + } + } + if ($last !== null) { + $this->pushAttribute($last, $item); + } + $items[] = $item; + + return (count($items) > 1) ? $items : $items[0]; + } + + /** + * Pushes a decoded attribute to the stack + * + * @param array $attribute + * @param array $entry + */ + protected function pushAttribute(array $attribute, array &$entry) + { + $name = $attribute[0]; + $type = $attribute[1]; + $value = $attribute[2]; + if ($type === '::') { + $value = base64_decode($value); + } + if ($name === 'dn') { + $entry[$name] = $value; + } elseif (isset($entry[$name]) && $value !== '') { + $entry[$name][] = $value; + } else { + $entry[$name] = ($value !== '') ? array($value) : array(); + } + } + + /** + * Encode $value into a Ldif representation + * + * @param mixed $value The value to be encoded + * @param array $options Additional options used during encoding + * @return string The encoded value + */ + public static function encode($value, array $options = array()) + { + $encoder = new self($options); + + return $encoder->_encode($value); + } + + /** + * Recursive driver which determines the type of value to be encoded + * and then dispatches to the appropriate method. + * + * @param mixed $value The value to be encoded + * @return string Encoded value + */ + protected function _encode($value) + { + if (is_scalar($value)) { + return $this->encodeString($value); + } elseif (is_array($value)) { + return $this->encodeAttributes($value); + } elseif ($value instanceof Ldap\Node) { + return $value->toLdif($this->options); + } + + return null; + } + + /** + * Encodes $string according to RFC2849 + * + * @link http://www.faqs.org/rfcs/rfc2849.html + * + * @param string $string + * @param boolean $base64 + * @return string + */ + protected function encodeString($string, &$base64 = null) + { + $string = (string)$string; + if (!is_numeric($string) && empty($string)) { + return ''; + } + + /* + * SAFE-INIT-CHAR = %x01-09 / %x0B-0C / %x0E-1F / + * %x21-39 / %x3B / %x3D-7F + * ; any value <= 127 except NUL, LF, CR, + * ; SPACE, colon (":", ASCII 58 decimal) + * ; and less-than ("<" , ASCII 60 decimal) + * + */ + $unsafe_init_char = array(0, 10, 13, 32, 58, 60); + /* + * SAFE-CHAR = %x01-09 / %x0B-0C / %x0E-7F + * ; any value <= 127 decimal except NUL, LF, + * ; and CR + */ + $unsafe_char = array(0, 10, 13); + + $base64 = false; + for ($i = 0; $i < strlen($string); $i++) { + $char = ord(substr($string, $i, 1)); + if ($char >= 127) { + $base64 = true; + break; + } elseif ($i === 0 && in_array($char, $unsafe_init_char)) { + $base64 = true; + break; + } elseif (in_array($char, $unsafe_char)) { + $base64 = true; + break; + } + } + // Test for ending space + if (substr($string, -1) == ' ') { + $base64 = true; + } + + if ($base64 === true) { + $string = base64_encode($string); + } + + return $string; + } + + /** + * Encodes an attribute with $name and $value according to RFC2849 + * + * @link http://www.faqs.org/rfcs/rfc2849.html + * + * @param string $name + * @param array|string $value + * @return string + */ + protected function encodeAttribute($name, $value) + { + if (!is_array($value)) { + $value = array($value); + } + + $output = ''; + + if (count($value) < 1) { + return $name . ': '; + } + + foreach ($value as $v) { + $base64 = null; + $v = $this->encodeString($v, $base64); + $attribute = $name . ':'; + if ($base64 === true) { + $attribute .= ': ' . $v; + } else { + $attribute .= ' ' . $v; + } + if (isset($this->options['wrap']) && strlen($attribute) > $this->options['wrap']) { + $attribute = trim(chunk_split($attribute, $this->options['wrap'], PHP_EOL . ' ')); + } + $output .= $attribute . PHP_EOL; + } + + return trim($output, PHP_EOL); + } + + /** + * Encodes a collection of attributes according to RFC2849 + * + * @link http://www.faqs.org/rfcs/rfc2849.html + * + * @param array $attributes + * @return string + */ + protected function encodeAttributes(array $attributes) + { + $string = ''; + $attributes = array_change_key_case($attributes, CASE_LOWER); + if (!$this->versionWritten && array_key_exists('dn', $attributes) && isset($this->options['version']) + && array_key_exists('objectclass', $attributes) + ) { + $string .= sprintf('version: %d', $this->options['version']) . PHP_EOL; + $this->versionWritten = true; + } + + if (isset($this->options['sort']) && $this->options['sort'] === true) { + ksort($attributes, SORT_STRING); + if (array_key_exists('objectclass', $attributes)) { + $oc = $attributes['objectclass']; + unset($attributes['objectclass']); + $attributes = array_merge(array('objectclass' => $oc), $attributes); + } + if (array_key_exists('dn', $attributes)) { + $dn = $attributes['dn']; + unset($attributes['dn']); + $attributes = array_merge(array('dn' => $dn), $attributes); + } + } + foreach ($attributes as $key => $value) { + $string .= $this->encodeAttribute($key, $value) . PHP_EOL; + } + + return trim($string, PHP_EOL); + } +} diff --git a/src/Node.php b/src/Node.php new file mode 100644 index 000000000..b3ab434df --- /dev/null +++ b/src/Node.php @@ -0,0 +1,1098 @@ +attachLdap($ldap); + } else { + $this->detachLdap(); + } + } + + /** + * Serialization callback + * + * Only Dn and attributes will be serialized. + * + * @return array + */ + public function __sleep() + { + return array('dn', 'currentData', 'newDn', 'originalData', + 'new', 'delete', 'children'); + } + + /** + * Deserialization callback + * + * Enforces a detached node. + */ + public function __wakeup() + { + $this->detachLdap(); + } + + /** + * Gets the current LDAP connection. + * + * @return Ldap + * @throws Exception\LdapException + */ + public function getLdap() + { + if ($this->ldap === null) { + throw new Exception\LdapException(null, 'No LDAP connection specified.', + Exception\LdapException::LDAP_OTHER); + } else { + return $this->ldap; + } + } + + /** + * Attach node to an LDAP connection + * + * This is an offline method. + * + * @param Ldap $ldap + * @return Node Provides a fluid interface + * @throws Exception\LdapException + */ + public function attachLdap(Ldap $ldap) + { + if (!Dn::isChildOf($this->_getDn(), $ldap->getBaseDn())) { + throw new Exception\LdapException(null, 'LDAP connection is not responsible for given node.', + Exception\LdapException::LDAP_OTHER); + } + + if ($ldap !== $this->ldap) { + $this->ldap = $ldap; + if (is_array($this->children)) { + foreach ($this->children as $child) { + $child->attachLdap($ldap); + } + } + } + + return $this; + } + + /** + * Detach node from LDAP connection + * + * This is an offline method. + * + * @return Node Provides a fluid interface + */ + public function detachLdap() + { + $this->ldap = null; + if (is_array($this->children)) { + foreach ($this->children as $child) { + $child->detachLdap(); + } + } + + return $this; + } + + /** + * Checks if the current node is attached to a LDAP server. + * + * This is an offline method. + * + * @return boolean + */ + public function isAttached() + { + return ($this->ldap !== null); + } + + /** + * Trigger an event + * + * @param string $event Event name + * @param array|\ArrayAccess $argv Array of arguments; typically, should be associative + */ + protected function triggerEvent($event, $argv = array()) + { + if (null === $this->events) { + if (class_exists('\Zend\EventManager\EventManager')) { + $this->events = new EventManager(__CLASS__); + } else { + return; + } + } + $this->events->trigger($event, $this, $argv); + } + + /** + * @param array $data + * @param boolean $fromDataSource + * @throws Exception\LdapException + */ + protected function loadData(array $data, $fromDataSource) + { + parent::loadData($data, $fromDataSource); + if ($fromDataSource === true) { + $this->originalData = $data; + } else { + $this->originalData = array(); + } + $this->children = null; + $this->markAsNew(($fromDataSource === true) ? false : true); + $this->markAsToBeDeleted(false); + } + + /** + * Factory method to create a new detached Zend\Ldap\Node for a given DN. + * + * @param string|array|Dn $dn + * @param array $objectClass + * @return Node + * @throws Exception\LdapException + */ + public static function create($dn, array $objectClass = array()) + { + if (is_string($dn) || is_array($dn)) { + $dn = Dn::factory($dn); + } elseif ($dn instanceof Dn) { + $dn = clone $dn; + } else { + throw new Exception\LdapException(null, '$dn is of a wrong data type.'); + } + $new = new self($dn, array(), false, null); + $new->ensureRdnAttributeValues(); + $new->setAttribute('objectClass', $objectClass); + + return $new; + } + + /** + * Factory method to create an attached Zend\Ldap\Node for a given DN. + * + * @param string|array|Dn $dn + * @param Ldap $ldap + * @return Node|null + * @throws Exception\LdapException + */ + public static function fromLdap($dn, Ldap $ldap) + { + if (is_string($dn) || is_array($dn)) { + $dn = Dn::factory($dn); + } elseif ($dn instanceof Dn) { + $dn = clone $dn; + } else { + throw new Exception\LdapException(null, '$dn is of a wrong data type.'); + } + $data = $ldap->getEntry($dn, array('*', '+'), true); + if ($data === null) { + return null; + } + $entry = new self($dn, $data, true, $ldap); + + return $entry; + } + + /** + * Factory method to create a detached Zend\Ldap\Node from array data. + * + * @param array $data + * @param boolean $fromDataSource + * @return Node + * @throws Exception\LdapException + */ + public static function fromArray(array $data, $fromDataSource = false) + { + if (!array_key_exists('dn', $data)) { + throw new Exception\LdapException(null, '\'dn\' key is missing in array.'); + } + if (is_string($data['dn']) || is_array($data['dn'])) { + $dn = Dn::factory($data['dn']); + } elseif ($data['dn'] instanceof Dn) { + $dn = clone $data['dn']; + } else { + throw new Exception\LdapException(null, '\'dn\' key is of a wrong data type.'); + } + $fromDataSource = ($fromDataSource === true) ? true : false; + $new = new self($dn, $data, $fromDataSource, null); + $new->ensureRdnAttributeValues(); + + return $new; + } + + /** + * Ensures that teh RDN attributes are correctly set. + * + * @param boolean $overwrite True to overwrite the RDN attributes + * @return void + */ + protected function ensureRdnAttributeValues($overwrite = false) + { + foreach ($this->getRdnArray() as $key => $value) { + if (!array_key_exists($key, $this->currentData) || $overwrite) { + Attribute::setAttribute($this->currentData, $key, $value, false); + } elseif (!in_array($value, $this->currentData[$key])) { + Attribute::setAttribute($this->currentData, $key, $value, true); + } + } + } + + /** + * Marks this node as new. + * + * Node will be added (instead of updated) on calling update() if $new is true. + * + * @param boolean $new + */ + protected function markAsNew($new) + { + $this->new = ($new === false) ? false : true; + } + + /** + * Tells if the node is considered as new (not present on the server) + * + * Please note, that this doesn't tell you if the node is present on the server. + * Use {@link exits()} to see if a node is already there. + * + * @return boolean + */ + public function isNew() + { + return $this->new; + } + + /** + * Marks this node as to be deleted. + * + * Node will be deleted on calling update() if $delete is true. + * + * @param boolean $delete + */ + protected function markAsToBeDeleted($delete) + { + $this->delete = ($delete === true) ? true : false; + } + + + /** + * Is this node going to be deleted once update() is called? + * + * @return boolean + */ + public function willBeDeleted() + { + return $this->delete; + } + + /** + * Marks this node as to be deleted + * + * Node will be deleted on calling update() if $delete is true. + * + * @return Node Provides a fluid interface + */ + public function delete() + { + $this->markAsToBeDeleted(true); + + return $this; + } + + /** + * Is this node going to be moved once update() is called? + * + * @return boolean + */ + public function willBeMoved() + { + if ($this->isNew() || $this->willBeDeleted()) { + return false; + } elseif ($this->newDn !== null) { + return ($this->dn != $this->newDn); + } else { + return false; + } + } + + /** + * Sends all pending changes to the LDAP server + * + * @param Ldap $ldap + * @return Node Provides a fluid interface + * @throws Exception\LdapException + * @trigger pre-delete + * @trigger post-delete + * @trigger pre-add + * @trigger post-add + * @trigger pre-rename + * @trigger post-rename + * @trigger pre-update + * @trigger post-update + */ + public function update(Ldap $ldap = null) + { + if ($ldap !== null) { + $this->attachLdap($ldap); + } + $ldap = $this->getLdap(); + if (!($ldap instanceof Ldap)) { + throw new Exception\LdapException(null, 'No LDAP connection available'); + } + + if ($this->willBeDeleted()) { + if ($ldap->exists($this->dn)) { + $this->triggerEvent('pre-delete'); + $ldap->delete($this->dn); + $this->triggerEvent('post-delete'); + } + return $this; + } + + if ($this->isNew()) { + $this->triggerEvent('pre-add'); + $data = $this->getData(); + $ldap->add($this->_getDn(), $data); + $this->loadData($data, true); + $this->triggerEvent('post-add'); + + return $this; + } + + $changedData = $this->getChangedData(); + if ($this->willBeMoved()) { + $this->triggerEvent('pre-rename'); + $recursive = $this->hasChildren(); + $ldap->rename($this->dn, $this->newDn, $recursive, false); + foreach ($this->newDn->getRdn() as $key => $value) { + if (array_key_exists($key, $changedData)) { + unset($changedData[$key]); + } + } + $this->dn = $this->newDn; + $this->newDn = null; + $this->triggerEvent('post-rename'); + } + if (count($changedData) > 0) { + $this->triggerEvent('pre-update'); + $ldap->update($this->_getDn(), $changedData); + $this->triggerEvent('post-update'); + } + $this->originalData = $this->currentData; + + return $this; + } + + /** + * Gets the DN of the current node as a Zend\Ldap\Dn. + * + * This is an offline method. + * + * @return Dn + */ + protected function _getDn() + { + return ($this->newDn === null) ? parent::_getDn() : $this->newDn; + } + + /** + * Gets the current DN of the current node as a Zend\Ldap\Dn. + * The method returns a clone of the node's DN to prohibit modification. + * + * This is an offline method. + * + * @return Dn + */ + public function getCurrentDn() + { + $dn = clone parent::_getDn(); + + return $dn; + } + + /** + * Sets the new DN for this node + * + * This is an offline method. + * + * @param Dn|string|array $newDn + * @throws Exception\LdapException + * @return Node Provides a fluid interface + */ + public function setDn($newDn) + { + if ($newDn instanceof Dn) { + $this->newDn = clone $newDn; + } else { + $this->newDn = Dn::factory($newDn); + } + $this->ensureRdnAttributeValues(true); + + return $this; + } + + /** + * {@see setDn()} + * + * This is an offline method. + * + * @param Dn|string|array $newDn + * @throws Exception\LdapException + * @return Node Provides a fluid interface + */ + public function move($newDn) + { + return $this->setDn($newDn); + } + + /** + * {@see setDn()} + * + * This is an offline method. + * + * @param Dn|string|array $newDn + * @throws Exception\LdapException + * @return Node Provides a fluid interface + */ + public function rename($newDn) + { + return $this->setDn($newDn); + } + + /** + * Sets the objectClass. + * + * This is an offline method. + * + * @param array|string $value + * @return Node Provides a fluid interface + * @throws Exception\LdapException + */ + public function setObjectClass($value) + { + $this->setAttribute('objectClass', $value); + + return $this; + } + + /** + * Appends to the objectClass. + * + * This is an offline method. + * + * @param array|string $value + * @return Node Provides a fluid interface + * @throws Exception\LdapException + */ + public function appendObjectClass($value) + { + $this->appendToAttribute('objectClass', $value); + + return $this; + } + + /** + * Returns a LDIF representation of the current node + * + * @param array $options Additional options used during encoding + * @return string + */ + public function toLdif(array $options = array()) + { + $attributes = array_merge(array('dn' => $this->getDnString()), $this->getData(false)); + + return Ldif\Encoder::encode($attributes, $options); + } + + /** + * Gets changed node data. + * + * The array contains all changed attributes. + * This format can be used in {@link Zend\Ldap\Ldap::add()} and {@link Zend\Ldap\Ldap::update()}. + * + * This is an offline method. + * + * @return array + */ + public function getChangedData() + { + $changed = array(); + foreach ($this->currentData as $key => $value) { + if (!array_key_exists($key, $this->originalData) && !empty($value)) { + $changed[$key] = $value; + } elseif ($this->originalData[$key] !== $this->currentData[$key]) { + $changed[$key] = $value; + } + } + + return $changed; + } + + /** + * Returns all changes made. + * + * This is an offline method. + * + * @return array + */ + public function getChanges() + { + $changes = array( + 'add' => array(), + 'delete' => array(), + 'replace' => array()); + foreach ($this->currentData as $key => $value) { + if (!array_key_exists($key, $this->originalData) && !empty($value)) { + $changes['add'][$key] = $value; + } elseif (count($this->originalData[$key]) === 0 && !empty($value)) { + $changes['add'][$key] = $value; + } elseif ($this->originalData[$key] !== $this->currentData[$key]) { + if (empty($value)) { + $changes['delete'][$key] = $value; + } else { + $changes['replace'][$key] = $value; + } + } + } + + return $changes; + } + + /** + * Sets a LDAP attribute. + * + * This is an offline method. + * + * @param string $name + * @param mixed $value + * @return Node Provides a fluid interface + * @throws Exception\LdapException + */ + public function setAttribute($name, $value) + { + $this->_setAttribute($name, $value, false); + return $this; + } + + /** + * Appends to a LDAP attribute. + * + * This is an offline method. + * + * @param string $name + * @param mixed $value + * @return Node Provides a fluid interface + * @throws Exception\LdapException + */ + public function appendToAttribute($name, $value) + { + $this->_setAttribute($name, $value, true); + + return $this; + } + + /** + * Checks if the attribute can be set and sets it accordingly. + * + * @param string $name + * @param mixed $value + * @param boolean $append + * @throws Exception\LdapException + */ + protected function _setAttribute($name, $value, $append) + { + $this->assertChangeableAttribute($name); + Attribute::setAttribute($this->currentData, $name, $value, $append); + } + + /** + * Sets a LDAP date/time attribute. + * + * This is an offline method. + * + * @param string $name + * @param integer|array $value + * @param boolean $utc + * @return Node Provides a fluid interface + * @throws Exception\LdapException + */ + public function setDateTimeAttribute($name, $value, $utc = false) + { + $this->_setDateTimeAttribute($name, $value, $utc, false); + return $this; + } + + /** + * Appends to a LDAP date/time attribute. + * + * This is an offline method. + * + * @param string $name + * @param integer|array $value + * @param boolean $utc + * @return Node Provides a fluid interface + * @throws Exception\LdapException + */ + public function appendToDateTimeAttribute($name, $value, $utc = false) + { + $this->_setDateTimeAttribute($name, $value, $utc, true); + + return $this; + } + + /** + * Checks if the attribute can be set and sets it accordingly. + * + * @param string $name + * @param integer|array $value + * @param boolean $utc + * @param boolean $append + * @throws Exception\LdapException + */ + protected function _setDateTimeAttribute($name, $value, $utc, $append) + { + $this->assertChangeableAttribute($name); + Attribute::setDateTimeAttribute($this->currentData, $name, $value, $utc, $append); + } + + /** + * Sets a LDAP password. + * + * @param string $password + * @param string $hashType + * @param string $attribName + * @return Node Provides a fluid interface + * @throws Exception\LdapException + */ + public function setPasswordAttribute($password, $hashType = Attribute::PASSWORD_HASH_MD5, + $attribName = 'userPassword' + ) { + $this->assertChangeableAttribute($attribName); + Attribute::setPassword($this->currentData, $password, $hashType, $attribName); + + return $this; + } + + /** + * Deletes a LDAP attribute. + * + * This method deletes the attribute. + * + * This is an offline method. + * + * @param string $name + * @return Node Provides a fluid interface + * @throws Exception\LdapException + */ + public function deleteAttribute($name) + { + if ($this->existsAttribute($name, true)) { + $this->_setAttribute($name, null, false); + } + + return $this; + } + + /** + * Removes duplicate values from a LDAP attribute + * + * @param string $attribName + * @return void + */ + public function removeDuplicatesFromAttribute($attribName) + { + Attribute::removeDuplicatesFromAttribute($this->currentData, $attribName); + } + + /** + * Remove given values from a LDAP attribute + * + * @param string $attribName + * @param mixed|array $value + * @return void + */ + public function removeFromAttribute($attribName, $value) + { + Attribute::removeFromAttribute($this->currentData, $attribName, $value); + } + + /** + * @param string $name + * @return boolean + * @throws Exception\LdapException + */ + protected function assertChangeableAttribute($name) + { + $name = strtolower($name); + $rdn = $this->getRdnArray(Dn::ATTR_CASEFOLD_LOWER); + if ($name == 'dn') { + throw new Exception\LdapException(null, 'DN cannot be changed.'); + } elseif (array_key_exists($name, $rdn)) { + throw new Exception\LdapException(null, 'Cannot change attribute because it\'s part of the RDN'); + } elseif (in_array($name, self::$systemAttributes)) { + throw new Exception\LdapException(null, 'Cannot change attribute because it\'s read-only'); + } else { + return true; + } + } + + /** + * Sets a LDAP attribute. + * + * This is an offline method. + * + * @param string $name + * @param $value + */ + public function __set($name, $value) + { + $this->setAttribute($name, $value); + } + + /** + * Deletes a LDAP attribute. + * + * This method deletes the attribute. + * + * This is an offline method. + * + * @param string $name + * @throws Exception\LdapException + */ + public function __unset($name) + { + $this->deleteAttribute($name); + } + + /** + * Sets a LDAP attribute. + * Implements ArrayAccess. + * + * This is an offline method. + * + * @param string $name + * @param mixed $value + * @throws Exception\LdapException + */ + public function offsetSet($name, $value) + { + $this->setAttribute($name, $value); + } + + /** + * Deletes a LDAP attribute. + * Implements ArrayAccess. + * + * This method deletes the attribute. + * + * This is an offline method. + * + * @param string $name + * @throws Exception\LdapException + */ + public function offsetUnset($name) + { + $this->deleteAttribute($name); + } + + /** + * Check if node exists on LDAP. + * + * This is an online method. + * + * @param Ldap $ldap + * @return boolean + * @throws Exception\LdapException + */ + public function exists(Ldap $ldap = null) + { + if ($ldap !== null) { + $this->attachLdap($ldap); + } + $ldap = $this->getLdap(); + + return $ldap->exists($this->_getDn()); + } + + /** + * Reload node attributes from LDAP. + * + * This is an online method. + * + * @param Ldap $ldap + * @return Node Provides a fluid interface + * @throws Exception\LdapException + */ + public function reload(Ldap $ldap = null) + { + if ($ldap !== null) { + $this->attachLdap($ldap); + } + $ldap = $this->getLdap(); + parent::reload($ldap); + + return $this; + } + + /** + * Search current subtree with given options. + * + * This is an online method. + * + * @param string|Filter\AbstractFilter $filter + * @param integer $scope + * @param string $sort + * @return Node\Collection + * @throws Exception\LdapException + */ + public function searchSubtree($filter, $scope = Ldap::SEARCH_SCOPE_SUB, $sort = null) + { + return $this->getLdap()->search( + $filter, $this->_getDn(), $scope, array('*', '+'), $sort, + 'Zend\Ldap\Node\Collection' + ); + } + + /** + * Count items in current subtree found by given filter. + * + * This is an online method. + * + * @param string|Filter\AbstractFilter $filter + * @param integer $scope + * @return integer + * @throws Exception\LdapException + */ + public function countSubtree($filter, $scope = Ldap::SEARCH_SCOPE_SUB) + { + return $this->getLdap()->count($filter, $this->_getDn(), $scope); + } + + /** + * Count children of current node. + * + * This is an online method. + * + * @return integer + * @throws Exception\LdapException + */ + public function countChildren() + { + return $this->countSubtree('(objectClass=*)', Ldap::SEARCH_SCOPE_ONE); + } + + /** + * Gets children of current node. + * + * This is an online method. + * + * @param string|Filter\AbstractFilter $filter + * @param string $sort + * @return Node\Collection + * @throws Exception\LdapException + */ + public function searchChildren($filter, $sort = null) + { + return $this->searchSubtree($filter, Ldap::SEARCH_SCOPE_ONE, $sort); + } + + /** + * Checks if current node has children. + * Returns whether the current element has children. + * + * Can be used offline but returns false if children have not been retrieved yet. + * + * @return boolean + * @throws Exception\LdapException + */ + public function hasChildren() + { + if (!is_array($this->children)) { + if ($this->isAttached()) { + return ($this->countChildren() > 0); + } else { + return false; + } + } else { + return (count($this->children) > 0); + } + } + + /** + * Returns the children for the current node. + * + * Can be used offline but returns an empty array if children have not been retrieved yet. + * + * @return Node\ChildrenIterator + * @throws Exception\LdapException + */ + public function getChildren() + { + if (!is_array($this->children)) { + $this->children = array(); + if ($this->isAttached()) { + $children = $this->searchChildren('(objectClass=*)', null); + foreach ($children as $child) { + $this->children[$child->getRdnString(Dn::ATTR_CASEFOLD_LOWER)] = $child; + } + } + } + + return new Node\ChildrenIterator($this->children); + } + + /** + * Returns the parent of the current node. + * + * @param Ldap $ldap + * @return Node + * @throws Exception\LdapException + */ + public function getParent(Ldap $ldap = null) + { + if ($ldap !== null) { + $this->attachLdap($ldap); + } + $ldap = $this->getLdap(); + $parentDn = $this->_getDn()->getParentDn(1); + + return self::fromLdap($parentDn, $ldap); + } + + /** + * Return the current attribute. + * Implements Iterator + * + * @return array + */ + public function current() + { + return $this; + } + + /** + * Return the attribute name. + * Implements Iterator + * + * @return string + */ + public function key() + { + return $this->getRdnString(); + } + + /** + * Move forward to next attribute. + * Implements Iterator + */ + public function next() + { + $this->iteratorRewind = false; + } + + /** + * Rewind the Iterator to the first attribute. + * Implements Iterator + */ + public function rewind() + { + $this->iteratorRewind = true; + } + + /** + * Check if there is a current attribute + * after calls to rewind() or next(). + * Implements Iterator + * + * @return boolean + */ + public function valid() + { + return $this->iteratorRewind; + } +} diff --git a/src/Node/AbstractNode.php b/src/Node/AbstractNode.php new file mode 100644 index 000000000..d2955c142 --- /dev/null +++ b/src/Node/AbstractNode.php @@ -0,0 +1,468 @@ +dn = $dn; + $this->loadData($data, $fromDataSource); + } + + /** + * @param array $data + * @param boolean $fromDataSource + */ + protected function loadData(array $data, $fromDataSource) + { + if (array_key_exists('dn', $data)) { + unset($data['dn']); + } + ksort($data, SORT_STRING); + $this->currentData = $data; + } + + /** + * Reload node attributes from LDAP. + * + * This is an online method. + * + * @param \Zend\Ldap\Ldap $ldap + * @return AbstractNode Provides a fluid interface + */ + public function reload(Ldap\Ldap $ldap = null) + { + if ($ldap !== null) { + $data = $ldap->getEntry($this->_getDn(), array('*', '+'), true); + $this->loadData($data, true); + } + + return $this; + } + + /** + * Gets the DN of the current node as a Zend\Ldap\Dn. + * + * This is an offline method. + * + * @return \Zend\Ldap\Dn + */ + protected function _getDn() + { + return $this->dn; + } + + /** + * Gets the DN of the current node as a Zend\Ldap\Dn. + * The method returns a clone of the node's DN to prohibit modification. + * + * This is an offline method. + * + * @return \Zend\Ldap\Dn + */ + public function getDn() + { + $dn = clone $this->_getDn(); + return $dn; + } + + /** + * Gets the DN of the current node as a string. + * + * This is an offline method. + * + * @param string $caseFold + * @return string + */ + public function getDnString($caseFold = null) + { + return $this->_getDn()->toString($caseFold); + } + + /** + * Gets the DN of the current node as an array. + * + * This is an offline method. + * + * @param string $caseFold + * @return array + */ + public function getDnArray($caseFold = null) + { + return $this->_getDn()->toArray($caseFold); + } + + /** + * Gets the RDN of the current node as a string. + * + * This is an offline method. + * + * @param string $caseFold + * @return string + */ + public function getRdnString($caseFold = null) + { + return $this->_getDn()->getRdnString($caseFold); + } + + /** + * Gets the RDN of the current node as an array. + * + * This is an offline method. + * + * @param string $caseFold + * @return array + */ + public function getRdnArray($caseFold = null) + { + return $this->_getDn()->getRdn($caseFold); + } + + /** + * Gets the objectClass of the node + * + * @return array + */ + public function getObjectClass() + { + return $this->getAttribute('objectClass', null); + } + + /** + * Gets all attributes of node. + * + * The collection contains all attributes. + * + * This is an offline method. + * + * @param boolean $includeSystemAttributes + * @return array + */ + public function getAttributes($includeSystemAttributes = true) + { + $data = array(); + foreach ($this->getData($includeSystemAttributes) as $name => $value) { + $data[$name] = $this->getAttribute($name, null); + } + return $data; + } + + /** + * Returns the DN of the current node. {@see getDnString()} + * + * @return string + */ + public function toString() + { + return $this->getDnString(); + } + + /** + * Cast to string representation {@see toString()} + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Returns an array representation of the current node + * + * @param boolean $includeSystemAttributes + * @return array + */ + public function toArray($includeSystemAttributes = true) + { + $attributes = $this->getAttributes($includeSystemAttributes); + return array_merge(array('dn' => $this->getDnString()), $attributes); + } + + /** + * Returns a JSON representation of the current node + * + * @param boolean $includeSystemAttributes + * @return string + */ + public function toJson($includeSystemAttributes = true) + { + return json_encode($this->toArray($includeSystemAttributes)); + } + + /** + * Gets node attributes. + * + * The array contains all attributes in its internal format (no conversion). + * + * This is an offline method. + * + * @param boolean $includeSystemAttributes + * @return array + */ + public function getData($includeSystemAttributes = true) + { + if ($includeSystemAttributes === false) { + $data = array(); + foreach ($this->currentData as $key => $value) { + if (!in_array($key, self::$systemAttributes)) { + $data[$key] = $value; + } + } + return $data; + } else { + return $this->currentData; + } + } + + /** + * Checks whether a given attribute exists. + * + * If $emptyExists is false empty attributes (containing only array()) are + * treated as non-existent returning false. + * If $emptyExists is true empty attributes are treated as existent returning + * true. In this case method returns false only if the attribute name is + * missing in the key-collection. + * + * @param string $name + * @param boolean $emptyExists + * @return boolean + */ + public function existsAttribute($name, $emptyExists = false) + { + $name = strtolower($name); + if (isset($this->currentData[$name])) { + if ($emptyExists) { + return true; + } + + return count($this->currentData[$name]) > 0; + } else { + return false; + } + } + + /** + * Checks if the given value(s) exist in the attribute + * + * @param string $attribName + * @param mixed|array $value + * @return boolean + */ + public function attributeHasValue($attribName, $value) + { + return Ldap\Attribute::attributeHasValue($this->currentData, $attribName, $value); + } + + /** + * Gets a LDAP attribute. + * + * This is an offline method. + * + * @param string $name + * @param integer $index + * @return mixed + * @throws \Zend\Ldap\Exception\LdapException + */ + public function getAttribute($name, $index = null) + { + if ($name == 'dn') { + return $this->getDnString(); + } else { + return Ldap\Attribute::getAttribute($this->currentData, $name, $index); + } + } + + /** + * Gets a LDAP date/time attribute. + * + * This is an offline method. + * + * @param string $name + * @param integer $index + * @return array|integer + * @throws \Zend\Ldap\Exception\LdapException + */ + public function getDateTimeAttribute($name, $index = null) + { + return Ldap\Attribute::getDateTimeAttribute($this->currentData, $name, $index); + } + + /** + * Sets a LDAP attribute. + * + * This is an offline method. + * + * @param string $name + * @param mixed $value + * @throws \Zend\Ldap\Exception\BadMethodCallException + */ + public function __set($name, $value) + { + throw new Exception\BadMethodCallException(); + } + + /** + * Gets a LDAP attribute. + * + * This is an offline method. + * + * @param string $name + * @return mixed + * @throws \Zend\Ldap\Exception\LdapException + */ + public function __get($name) + { + return $this->getAttribute($name, null); + } + + /** + * Deletes a LDAP attribute. + * + * This method deletes the attribute. + * + * This is an offline method. + * + * @param $name + * @throws \Zend\Ldap\Exception\BadMethodCallException + */ + public function __unset($name) + { + throw new Exception\BadMethodCallException(); + } + + /** + * Checks whether a given attribute exists. + * + * Empty attributes will be treated as non-existent. + * + * @param string $name + * @return boolean + */ + public function __isset($name) + { + return $this->existsAttribute($name, false); + } + + /** + * Sets a LDAP attribute. + * Implements ArrayAccess. + * + * This is an offline method. + * + * @param string $name + * @param $value + * @throws \Zend\Ldap\Exception\BadMethodCallException + * @param mixed $value + * @throws \Zend\Ldap\Exception\BadMethodCallException + */ + public function offsetSet($name, $value) + { + throw new Exception\BadMethodCallException(); + } + + /** + * Gets a LDAP attribute. + * Implements ArrayAccess. + * + * This is an offline method. + * + * @param string $name + * @return mixed + * @throws \Zend\Ldap\Exception\LdapException + */ + public function offsetGet($name) + { + return $this->getAttribute($name, null); + } + + /** + * Deletes a LDAP attribute. + * Implements ArrayAccess. + * + * This method deletes the attribute. + * + * This is an offline method. + * + * @param $name + * @throws \Zend\Ldap\Exception\BadMethodCallException + */ + public function offsetUnset($name) + { + throw new Exception\BadMethodCallException(); + } + + /** + * Checks whether a given attribute exists. + * Implements ArrayAccess. + * + * Empty attributes will be treated as non-existent. + * + * @param string $name + * @return boolean + */ + public function offsetExists($name) + { + return $this->existsAttribute($name, false); + } + + /** + * Returns the number of attributes in node. + * Implements Countable + * + * @return int + */ + public function count() + { + return count($this->currentData); + } +} diff --git a/src/Node/ChildrenIterator.php b/src/Node/ChildrenIterator.php new file mode 100644 index 000000000..7f7cc4ed4 --- /dev/null +++ b/src/Node/ChildrenIterator.php @@ -0,0 +1,196 @@ +data = $data; + } + + /** + * Returns the number of child nodes. + * Implements Countable + * + * @return int + */ + public function count() + { + return count($this->data); + } + + /** + * Return the current child. + * Implements Iterator + * + * @return \Zend\Ldap\Node + */ + public function current() + { + return current($this->data); + } + + /** + * Return the child'd RDN. + * Implements Iterator + * + * @return string + */ + public function key() + { + return key($this->data); + } + + /** + * Move forward to next child. + * Implements Iterator + */ + public function next() + { + next($this->data); + } + + /** + * Rewind the Iterator to the first child. + * Implements Iterator + */ + public function rewind() + { + reset($this->data); + } + + /** + * Check if there is a current child + * after calls to rewind() or next(). + * Implements Iterator + * + * @return boolean + */ + public function valid() + { + return (current($this->data) !== false); + } + + /** + * Checks if current node has children. + * Returns whether the current element has children. + * + * @return boolean + */ + public function hasChildren() + { + if ($this->current() instanceof Ldap\Node) { + return $this->current()->hasChildren(); + } else { + return false; + } + } + + /** + * Returns the children for the current node. + * + * @return ChildrenIterator + */ + public function getChildren() + { + if ($this->current() instanceof Ldap\Node) { + return $this->current()->getChildren(); + } else { + return null; + } + } + + /** + * Returns a child with a given RDN. + * Implements ArrayAccess. + * + * @param string $rdn + * @return array|null + */ + public function offsetGet($rdn) + { + if ($this->offsetExists($rdn)) { + return $this->data[$rdn]; + } else { + return null; + } + } + + /** + * Checks whether a given rdn exists. + * Implements ArrayAccess. + * + * @param string $rdn + * @return boolean + */ + public function offsetExists($rdn) + { + return (array_key_exists($rdn, $this->data)); + } + + /** + * Does nothing. + * Implements ArrayAccess. + * + * @param $name + */ + public function offsetUnset($name) + { + } + + /** + * Does nothing. + * Implements ArrayAccess. + * + * @param string $name + * @param $value + */ + public function offsetSet($name, $value) + { + } + + /** + * Get all children as an array + * + * @return array + */ + public function toArray() + { + $data = array(); + foreach ($this as $rdn => $node) { + $data[$rdn] = $node; + } + return $data; + } +} diff --git a/src/Node/Collection.php b/src/Node/Collection.php new file mode 100644 index 000000000..7206a6858 --- /dev/null +++ b/src/Node/Collection.php @@ -0,0 +1,47 @@ +attachLDAP($this->iterator->getLDAP()); + return $node; + } + + /** + * Return the child key (DN). + * Implements Iterator and RecursiveIterator + * + * @return string + */ + public function key() + { + return $this->iterator->key(); + } +} diff --git a/src/Node/RootDse.php b/src/Node/RootDse.php new file mode 100644 index 000000000..e1e94c7ec --- /dev/null +++ b/src/Node/RootDse.php @@ -0,0 +1,127 @@ +getEntry($dn, array('*', '+'), true); + if (isset($data['domainfunctionality'])) { + return new RootDse\ActiveDirectory($dn, $data); + } elseif (isset($data['dsaname'])) { + return new RootDse\eDirectory($dn, $data); + } elseif (isset($data['structuralobjectclass']) + && $data['structuralobjectclass'][0] === 'OpenLDAProotDSE' + ) { + return new RootDse\OpenLdap($dn, $data); + } else { + return new self($dn, $data); + } + } + + /** + * Constructor. + * + * Constructor is protected to enforce the use of factory methods. + * + * @param \Zend\Ldap\Dn $dn + * @param array $data + */ + protected function __construct(Ldap\Dn $dn, array $data) + { + parent::__construct($dn, $data, true); + } + + /** + * Gets the namingContexts. + * + * @return array + */ + public function getNamingContexts() + { + return $this->getAttribute('namingContexts', null); + } + + /** + * Gets the subschemaSubentry. + * + * @return string|null + */ + public function getSubschemaSubentry() + { + return $this->getAttribute('subschemaSubentry', 0); + } + + /** + * Determines if the version is supported + * + * @param string|int|array $versions version(s) to check + * @return boolean + */ + public function supportsVersion($versions) + { + return $this->attributeHasValue('supportedLDAPVersion', $versions); + } + + /** + * Determines if the sasl mechanism is supported + * + * @param string|array $mechlist SASL mechanisms to check + * @return boolean + */ + public function supportsSaslMechanism($mechlist) + { + return $this->attributeHasValue('supportedSASLMechanisms', $mechlist); + } + + /** + * Gets the server type + * + * @return int + */ + public function getServerType() + { + return self::SERVER_TYPE_GENERIC; + } + + /** + * Returns the schema DN + * + * @return \Zend\Ldap\Dn + */ + public function getSchemaDn() + { + $schemaDn = $this->getSubschemaSubentry(); + return Ldap\Dn::fromString($schemaDn); + } +} diff --git a/src/Node/RootDse/ActiveDirectory.php b/src/Node/RootDse/ActiveDirectory.php new file mode 100644 index 000000000..171ce855e --- /dev/null +++ b/src/Node/RootDse/ActiveDirectory.php @@ -0,0 +1,229 @@ +getAttribute('configurationNamingContext', 0); + } + + /** + * Gets the currentTime. + * + * @return string|null + */ + public function getCurrentTime() + { + return $this->getAttribute('currentTime', 0); + } + + /** + * Gets the defaultNamingContext. + * + * @return string|null + */ + public function getDefaultNamingContext() + { + return $this->getAttribute('defaultNamingContext', 0); + } + + /** + * Gets the dnsHostName. + * + * @return string|null + */ + public function getDnsHostName() + { + return $this->getAttribute('dnsHostName', 0); + } + + /** + * Gets the domainControllerFunctionality. + * + * @return string|null + */ + public function getDomainControllerFunctionality() + { + return $this->getAttribute('domainControllerFunctionality', 0); + } + + /** + * Gets the domainFunctionality. + * + * @return string|null + */ + public function getDomainFunctionality() + { + return $this->getAttribute('domainFunctionality', 0); + } + + /** + * Gets the dsServiceName. + * + * @return string|null + */ + public function getDsServiceName() + { + return $this->getAttribute('dsServiceName', 0); + } + + /** + * Gets the forestFunctionality. + * + * @return string|null + */ + public function getForestFunctionality() + { + return $this->getAttribute('forestFunctionality', 0); + } + + /** + * Gets the highestCommittedUSN. + * + * @return string|null + */ + public function getHighestCommittedUSN() + { + return $this->getAttribute('highestCommittedUSN', 0); + } + + /** + * Gets the isGlobalCatalogReady. + * + * @return string|null + */ + public function getIsGlobalCatalogReady() + { + return $this->getAttribute('isGlobalCatalogReady', 0); + } + + /** + * Gets the isSynchronized. + * + * @return string|null + */ + public function getIsSynchronized() + { + return $this->getAttribute('isSynchronized', 0); + } + + /** + * Gets the ldapServiceName. + * + * @return string|null + */ + public function getLDAPServiceName() + { + return $this->getAttribute('ldapServiceName', 0); + } + + /** + * Gets the rootDomainNamingContext. + * + * @return string|null + */ + public function getRootDomainNamingContext() + { + return $this->getAttribute('rootDomainNamingContext', 0); + } + + /** + * Gets the schemaNamingContext. + * + * @return string|null + */ + public function getSchemaNamingContext() + { + return $this->getAttribute('schemaNamingContext', 0); + } + + /** + * Gets the serverName. + * + * @return string|null + */ + public function getServerName() + { + return $this->getAttribute('serverName', 0); + } + + /** + * Determines if the capability is supported + * + * @param string|string|array $oids capability(s) to check + * @return boolean + */ + public function supportsCapability($oids) + { + return $this->attributeHasValue('supportedCapabilities', $oids); + } + + /** + * Determines if the control is supported + * + * @param string|array $oids control oid(s) to check + * @return boolean + */ + public function supportsControl($oids) + { + return $this->attributeHasValue('supportedControl', $oids); + } + + /** + * Determines if the version is supported + * + * @param string|array $policies policy(s) to check + * @return boolean + */ + public function supportsPolicy($policies) + { + return $this->attributeHasValue('supportedLDAPPolicies', $policies); + } + + /** + * Gets the server type + * + * @return int + */ + public function getServerType() + { + return self::SERVER_TYPE_ACTIVEDIRECTORY; + } + + /** + * Returns the schema DN + * + * @return \Zend\Ldap\Dn + */ + public function getSchemaDn() + { + $schemaDn = $this->getSchemaNamingContext(); + return Ldap\Dn::fromString($schemaDn); + } +} diff --git a/src/Node/RootDse/OpenLdap.php b/src/Node/RootDse/OpenLdap.php new file mode 100644 index 000000000..ff3204f3d --- /dev/null +++ b/src/Node/RootDse/OpenLdap.php @@ -0,0 +1,87 @@ +getAttribute('configContext', 0); + } + + /** + * Gets the monitorContext. + * + * @return string|null + */ + public function getMonitorContext() + { + return $this->getAttribute('monitorContext', 0); + } + + /** + * Determines if the control is supported + * + * @param string|array $oids control oid(s) to check + * @return boolean + */ + public function supportsControl($oids) + { + return $this->attributeHasValue('supportedControl', $oids); + } + + /** + * Determines if the extension is supported + * + * @param string|array $oids oid(s) to check + * @return boolean + */ + public function supportsExtension($oids) + { + return $this->attributeHasValue('supportedExtension', $oids); + } + + /** + * Determines if the feature is supported + * + * @param string|array $oids feature oid(s) to check + * @return boolean + */ + public function supportsFeature($oids) + { + return $this->attributeHasValue('supportedFeatures', $oids); + } + + /** + * Gets the server type + * + * @return int + */ + public function getServerType() + { + return self::SERVER_TYPE_OPENLDAP; + } +} diff --git a/src/Node/RootDse/eDirectory.php b/src/Node/RootDse/eDirectory.php new file mode 100644 index 000000000..d659f08fa --- /dev/null +++ b/src/Node/RootDse/eDirectory.php @@ -0,0 +1,145 @@ +attributeHasValue('supportedExtension', $oids); + } + + /** + * Gets the vendorName. + * + * @return string|null + */ + public function getVendorName() + { + return $this->getAttribute('vendorName', 0); + } + + /** + * Gets the vendorVersion. + * + * @return string|null + */ + public function getVendorVersion() + { + return $this->getAttribute('vendorVersion', 0); + } + + /** + * Gets the dsaName. + * + * @return string|null + */ + public function getDsaName() + { + return $this->getAttribute('dsaName', 0); + } + + /** + * Gets the server statistics "errors". + * + * @return string|null + */ + public function getStatisticsErrors() + { + return $this->getAttribute('errors', 0); + } + + /** + * Gets the server statistics "securityErrors". + * + * @return string|null + */ + public function getStatisticsSecurityErrors() + { + return $this->getAttribute('securityErrors', 0); + } + + /** + * Gets the server statistics "chainings". + * + * @return string|null + */ + public function getStatisticsChainings() + { + return $this->getAttribute('chainings', 0); + } + + /** + * Gets the server statistics "referralsReturned". + * + * @return string|null + */ + public function getStatisticsReferralsReturned() + { + return $this->getAttribute('referralsReturned', 0); + } + + /** + * Gets the server statistics "extendedOps". + * + * @return string|null + */ + public function getStatisticsExtendedOps() + { + return $this->getAttribute('extendedOps', 0); + } + + /** + * Gets the server statistics "abandonOps". + * + * @return string|null + */ + public function getStatisticsAbandonOps() + { + return $this->getAttribute('abandonOps', 0); + } + + /** + * Gets the server statistics "wholeSubtreeSearchOps". + * + * @return string|null + */ + public function getStatisticsWholeSubtreeSearchOps() + { + return $this->getAttribute('wholeSubtreeSearchOps', 0); + } + + /** + * Gets the server type + * + * @return int + */ + public function getServerType() + { + return self::SERVER_TYPE_EDIRECTORY; + } +} diff --git a/src/Node/Schema.php b/src/Node/Schema.php new file mode 100644 index 000000000..b75d97193 --- /dev/null +++ b/src/Node/Schema.php @@ -0,0 +1,96 @@ +getRootDse()->getSchemaDn(); + $data = $ldap->getEntry($dn, array('*', '+'), true); + switch ($ldap->getRootDse()->getServerType()) { + case RootDse::SERVER_TYPE_ACTIVEDIRECTORY: + return new Schema\ActiveDirectory($dn, $data, $ldap); + case RootDse::SERVER_TYPE_OPENLDAP: + return new Schema\OpenLdap($dn, $data, $ldap); + case RootDse::SERVER_TYPE_EDIRECTORY: + default: + return new self($dn, $data, $ldap); + } + } + + /** + * Constructor. + * + * Constructor is protected to enforce the use of factory methods. + * + * @param \Zend\Ldap\Dn $dn + * @param array $data + * @param \Zend\Ldap\Ldap $ldap + */ + protected function __construct(Ldap\Dn $dn, array $data, Ldap\Ldap $ldap) + { + parent::__construct($dn, $data, true); + $this->parseSchema($dn, $ldap); + } + + /** + * Parses the schema + * + * @param \Zend\Ldap\Dn $dn + * @param \Zend\Ldap\Ldap $ldap + * @return Schema Provides a fluid interface + */ + protected function parseSchema(Ldap\Dn $dn, Ldap\Ldap $ldap) + { + return $this; + } + + /** + * Gets the attribute Types + * + * @return array + */ + public function getAttributeTypes() + { + return array(); + } + + /** + * Gets the object classes + * + * @return array + */ + public function getObjectClasses() + { + return array(); + } +} diff --git a/src/Node/Schema/AbstractItem.php b/src/Node/Schema/AbstractItem.php new file mode 100644 index 000000000..12b703b0f --- /dev/null +++ b/src/Node/Schema/AbstractItem.php @@ -0,0 +1,151 @@ +setData($data); + } + + /** + * Sets the data + * + * @param array $data + * @return AbstractItem Provides a fluid interface + */ + public function setData(array $data) + { + $this->data = $data; + return $this; + } + + /** + * Gets the data + * + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * Gets a specific attribute from this item + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } else { + return null; + } + } + + /** + * Checks whether a specific attribute exists. + * + * @param string $name + * @return boolean + */ + public function __isset($name) + { + return (array_key_exists($name, $this->data)); + } + + /** + * Always throws Zend\Ldap\Exception\BadMethodCallException + * Implements ArrayAccess. + * + * This method is needed for a full implementation of ArrayAccess + * + * @param string $name + * @param mixed $value + * @throws \Zend\Ldap\Exception\BadMethodCallException + */ + public function offsetSet($name, $value) + { + throw new Exception\BadMethodCallException(); + } + + /** + * Gets a specific attribute from this item + * + * @param string $name + * @return mixed + */ + public function offsetGet($name) + { + return $this->__get($name); + } + + /** + * Always throws Zend\Ldap\Exception\BadMethodCallException + * Implements ArrayAccess. + * + * This method is needed for a full implementation of ArrayAccess + * + * @param string $name + * @throws \Zend\Ldap\Exception\BadMethodCallException + */ + public function offsetUnset($name) + { + throw new Exception\BadMethodCallException(); + } + + /** + * Checks whether a specific attribute exists. + * + * @param string $name + * @return boolean + */ + public function offsetExists($name) + { + return $this->__isset($name); + } + + /** + * Returns the number of attributes. + * Implements Countable + * + * @return int + */ + public function count() + { + return count($this->data); + } +} diff --git a/src/Node/Schema/ActiveDirectory.php b/src/Node/Schema/ActiveDirectory.php new file mode 100644 index 000000000..c8574dd36 --- /dev/null +++ b/src/Node/Schema/ActiveDirectory.php @@ -0,0 +1,86 @@ +search( + '(objectClass=classSchema)', $dn, + Ldap\Ldap::SEARCH_SCOPE_ONE + ) as $node) { + $val = new ObjectClass\ActiveDirectory($node); + $this->objectClasses[$val->getName()] = $val; + } + foreach ($ldap->search( + '(objectClass=attributeSchema)', $dn, + Ldap\Ldap::SEARCH_SCOPE_ONE + ) as $node) { + $val = new AttributeType\ActiveDirectory($node); + $this->attributeTypes[$val->getName()] = $val; + } + + return $this; + } + + /** + * Gets the attribute Types + * + * @return array + */ + public function getAttributeTypes() + { + return $this->attributeTypes; + } + + /** + * Gets the object classes + * + * @return array + */ + public function getObjectClasses() + { + return $this->objectClasses; + } +} diff --git a/src/Node/Schema/AttributeType/ActiveDirectory.php b/src/Node/Schema/AttributeType/ActiveDirectory.php new file mode 100644 index 000000000..dbe4fa229 --- /dev/null +++ b/src/Node/Schema/AttributeType/ActiveDirectory.php @@ -0,0 +1,84 @@ +ldapdisplayname[0]; + } + + /** + * Gets the attribute OID + * + * @return string + */ + public function getOid() + { + + } + + /** + * Gets the attribute syntax + * + * @return string + */ + public function getSyntax() + { + + } + + /** + * Gets the attribute maximum length + * + * @return int|null + */ + public function getMaxLength() + { + + } + + /** + * Returns if the attribute is single-valued. + * + * @return boolean + */ + public function isSingleValued() + { + + } + + /** + * Gets the attribute description + * + * @return string + */ + public function getDescription() + { + + } +} diff --git a/src/Node/Schema/AttributeType/AttributeTypeInterface.php b/src/Node/Schema/AttributeType/AttributeTypeInterface.php new file mode 100644 index 000000000..6ab309a06 --- /dev/null +++ b/src/Node/Schema/AttributeType/AttributeTypeInterface.php @@ -0,0 +1,63 @@ +name; + } + + /** + * Gets the attribute OID + * + * @return string + */ + public function getOid() + { + return $this->oid; + } + + /** + * Gets the attribute syntax + * + * @return string + */ + public function getSyntax() + { + if ($this->syntax === null) { + $parent = $this->getParent(); + if ($parent === null) { + return null; + } else { + return $parent->getSyntax(); + } + } else { + return $this->syntax; + } + } + + /** + * Gets the attribute maximum length + * + * @return int|null + */ + public function getMaxLength() + { + $maxLength = $this->{'max-length'}; + if ($maxLength === null) { + $parent = $this->getParent(); + if ($parent === null) { + return null; + } else { + return $parent->getMaxLength(); + } + } else { + return (int)$maxLength; + } + } + + /** + * Returns if the attribute is single-valued. + * + * @return boolean + */ + public function isSingleValued() + { + return $this->{'single-value'}; + } + + /** + * Gets the attribute description + * + * @return string + */ + public function getDescription() + { + return $this->desc; + } + + /** + * Returns the parent attribute type in the inheritance tree if one exists + * + * @return OpenLdap|null + */ + public function getParent() + { + if (count($this->_parents) === 1) { + return $this->_parents[0]; + } + + return null; + } +} diff --git a/src/Node/Schema/ObjectClass/ActiveDirectory.php b/src/Node/Schema/ObjectClass/ActiveDirectory.php new file mode 100644 index 000000000..61a8a00d8 --- /dev/null +++ b/src/Node/Schema/ObjectClass/ActiveDirectory.php @@ -0,0 +1,95 @@ +ldapdisplayname[0]; + } + + /** + * Gets the objectClass OID + * + * @return string + */ + public function getOid() + { + + } + + /** + * Gets the attributes that this objectClass must contain + * + * @return array + */ + public function getMustContain() + { + + } + + /** + * Gets the attributes that this objectClass may contain + * + * @return array + */ + public function getMayContain() + { + + } + + /** + * Gets the objectClass description + * + * @return string + */ + public function getDescription() + { + + } + + /** + * Gets the objectClass type + * + * @return integer + */ + public function getType() + { + + } + + /** + * Returns the parent objectClasses of this class. + * This includes structural, abstract and auxiliary objectClasses + * + * @return array + */ + public function getParentClasses() + { + + } +} diff --git a/src/Node/Schema/ObjectClass/ObjectClassInterface.php b/src/Node/Schema/ObjectClass/ObjectClassInterface.php new file mode 100644 index 000000000..8bf77cfab --- /dev/null +++ b/src/Node/Schema/ObjectClass/ObjectClassInterface.php @@ -0,0 +1,71 @@ +name; + } + + /** + * Gets the objectClass OID + * + * @return string + */ + public function getOid() + { + return $this->oid; + } + + /** + * Gets the attributes that this objectClass must contain + * + * @return array + */ + public function getMustContain() + { + if ($this->inheritedMust === null) { + $this->resolveInheritance(); + } + return $this->inheritedMust; + } + + /** + * Gets the attributes that this objectClass may contain + * + * @return array + */ + public function getMayContain() + { + if ($this->inheritedMay === null) { + $this->resolveInheritance(); + } + return $this->inheritedMay; + } + + /** + * Resolves the inheritance tree + * + * @return void + */ + protected function resolveInheritance() + { + $must = $this->must; + $may = $this->may; + foreach ($this->getParents() as $p) { + $must = array_merge($must, $p->getMustContain()); + $may = array_merge($may, $p->getMayContain()); + } + $must = array_unique($must); + $may = array_unique($may); + $may = array_diff($may, $must); + sort($must, SORT_STRING); + sort($may, SORT_STRING); + $this->inheritedMust = $must; + $this->inheritedMay = $may; + } + + /** + * Gets the objectClass description + * + * @return string + */ + public function getDescription() + { + return $this->desc; + } + + /** + * Gets the objectClass type + * + * @return integer + */ + public function getType() + { + if ($this->structural) { + return Schema::OBJECTCLASS_TYPE_STRUCTURAL; + } elseif ($this->abstract) { + return Schema::OBJECTCLASS_TYPE_ABSTRACT; + } elseif ($this->auxiliary) { + return Schema::OBJECTCLASS_TYPE_AUXILIARY; + } else { + return Schema::OBJECTCLASS_TYPE_UNKNOWN; + } + } + + /** + * Returns the parent objectClasses of this class. + * This includes structural, abstract and auxiliary objectClasses + * + * @return array + */ + public function getParentClasses() + { + return $this->sup; + } + + /** + * Returns the parent object classes in the inheritance tree if one exists + * + * @return array of OpenLdap + */ + public function getParents() + { + return $this->_parents; + } +} diff --git a/src/Node/Schema/OpenLdap.php b/src/Node/Schema/OpenLdap.php new file mode 100644 index 000000000..c0f34440c --- /dev/null +++ b/src/Node/Schema/OpenLdap.php @@ -0,0 +1,499 @@ +loadAttributeTypes(); + $this->loadLdapSyntaxes(); + $this->loadMatchingRules(); + $this->loadMatchingRuleUse(); + $this->loadObjectClasses(); + return $this; + } + + /** + * Gets the attribute Types + * + * @return array + */ + public function getAttributeTypes() + { + return $this->attributeTypes; + } + + /** + * Gets the object classes + * + * @return array + */ + public function getObjectClasses() + { + return $this->objectClasses; + } + + /** + * Gets the LDAP syntaxes + * + * @return array + */ + public function getLdapSyntaxes() + { + return $this->ldapSyntaxes; + } + + /** + * Gets the matching rules + * + * @return array + */ + public function getMatchingRules() + { + return $this->matchingRules; + } + + /** + * Gets the matching rule use + * + * @return array + */ + public function getMatchingRuleUse() + { + return $this->matchingRuleUse; + } + + /** + * Loads the attribute Types + * + * @return void + */ + protected function loadAttributeTypes() + { + $this->attributeTypes = array(); + foreach ($this->getAttribute('attributeTypes') as $value) { + $val = $this->parseAttributeType($value); + $val = new AttributeType\OpenLdap($val); + $this->attributeTypes[$val->getName()] = $val; + + } + foreach ($this->attributeTypes as $val) { + if (count($val->sup) > 0) { + $this->resolveInheritance($val, $this->attributeTypes); + } + foreach ($val->aliases as $alias) { + $this->attributeTypes[$alias] = $val; + } + } + ksort($this->attributeTypes, SORT_STRING); + } + + /** + * Parses an attributeType value + * + * @param string $value + * @return array + */ + protected function parseAttributeType($value) + { + $attributeType = array( + 'oid' => null, + 'name' => null, + 'desc' => null, + 'obsolete' => false, + 'sup' => null, + 'equality' => null, + 'ordering' => null, + 'substr' => null, + 'syntax' => null, + 'max-length' => null, + 'single-value' => false, + 'collective' => false, + 'no-user-modification' => false, + 'usage' => 'userApplications', + '_string' => $value, + '_parents' => array()); + + $tokens = $this->tokenizeString($value); + $attributeType['oid'] = array_shift($tokens); // first token is the oid + $this->parseLdapSchemaSyntax($attributeType, $tokens); + + if (array_key_exists('syntax', $attributeType)) { + // get max length from syntax + if (preg_match('/^(.+){(\d+)}$/', $attributeType['syntax'], $matches)) { + $attributeType['syntax'] = $matches[1]; + $attributeType['max-length'] = $matches[2]; + } + } + + $this->ensureNameAttribute($attributeType); + + return $attributeType; + } + + /** + * Loads the object classes + * + * @return void + */ + protected function loadObjectClasses() + { + $this->objectClasses = array(); + foreach ($this->getAttribute('objectClasses') as $value) { + $val = $this->parseObjectClass($value); + $val = new ObjectClass\OpenLdap($val); + $this->objectClasses[$val->getName()] = $val; + } + foreach ($this->objectClasses as $val) { + if (count($val->sup) > 0) { + $this->resolveInheritance($val, $this->objectClasses); + } + foreach ($val->aliases as $alias) { + $this->objectClasses[$alias] = $val; + } + } + ksort($this->objectClasses, SORT_STRING); + } + + /** + * Parses an objectClasses value + * + * @param string $value + * @return array + */ + protected function parseObjectClass($value) + { + $objectClass = array( + 'oid' => null, + 'name' => null, + 'desc' => null, + 'obsolete' => false, + 'sup' => array(), + 'abstract' => false, + 'structural' => false, + 'auxiliary' => false, + 'must' => array(), + 'may' => array(), + '_string' => $value, + '_parents' => array()); + + $tokens = $this->tokenizeString($value); + $objectClass['oid'] = array_shift($tokens); // first token is the oid + $this->parseLdapSchemaSyntax($objectClass, $tokens); + + $this->ensureNameAttribute($objectClass); + + return $objectClass; + } + + /** + * Resolves inheritance in objectClasses and attributes + * + * @param AbstractItem $node + * @param array $repository + */ + protected function resolveInheritance(AbstractItem $node, array $repository) + { + $data = $node->getData(); + $parents = $data['sup']; + if ($parents === null || !is_array($parents) || count($parents) < 1) { + return; + } + foreach ($parents as $parent) { + if (!array_key_exists($parent, $repository)) { + continue; + } + if (!array_key_exists('_parents', $data) || !is_array($data['_parents'])) { + $data['_parents'] = array(); + } + $data['_parents'][] = $repository[$parent]; + } + $node->setData($data); + } + + /** + * Loads the LDAP syntaxes + * + * @return void + */ + protected function loadLdapSyntaxes() + { + $this->ldapSyntaxes = array(); + foreach ($this->getAttribute('ldapSyntaxes') as $value) { + $val = $this->parseLdapSyntax($value); + $this->ldapSyntaxes[$val['oid']] = $val; + } + ksort($this->ldapSyntaxes, SORT_STRING); + } + + /** + * Parses an ldapSyntaxes value + * + * @param string $value + * @return array + */ + protected function parseLdapSyntax($value) + { + $ldapSyntax = array( + 'oid' => null, + 'desc' => null, + '_string' => $value); + + $tokens = $this->tokenizeString($value); + $ldapSyntax['oid'] = array_shift($tokens); // first token is the oid + $this->parseLdapSchemaSyntax($ldapSyntax, $tokens); + + return $ldapSyntax; + } + + /** + * Loads the matching rules + * + * @return void + */ + protected function loadMatchingRules() + { + $this->matchingRules = array(); + foreach ($this->getAttribute('matchingRules') as $value) { + $val = $this->parseMatchingRule($value); + $this->matchingRules[$val['name']] = $val; + } + ksort($this->matchingRules, SORT_STRING); + } + + /** + * Parses an matchingRules value + * + * @param string $value + * @return array + */ + protected function parseMatchingRule($value) + { + $matchingRule = array( + 'oid' => null, + 'name' => null, + 'desc' => null, + 'obsolete' => false, + 'syntax' => null, + '_string' => $value); + + $tokens = $this->tokenizeString($value); + $matchingRule['oid'] = array_shift($tokens); // first token is the oid + $this->parseLdapSchemaSyntax($matchingRule, $tokens); + + $this->ensureNameAttribute($matchingRule); + + return $matchingRule; + } + + /** + * Loads the matching rule use + * + * @return void + */ + protected function loadMatchingRuleUse() + { + $this->matchingRuleUse = array(); + foreach ($this->getAttribute('matchingRuleUse') as $value) { + $val = $this->parseMatchingRuleUse($value); + $this->matchingRuleUse[$val['name']] = $val; + } + ksort($this->matchingRuleUse, SORT_STRING); + } + + /** + * Parses an matchingRuleUse value + * + * @param string $value + * @return array + */ + protected function parseMatchingRuleUse($value) + { + $matchingRuleUse = array( + 'oid' => null, + 'name' => null, + 'desc' => null, + 'obsolete' => false, + 'applies' => array(), + '_string' => $value); + + $tokens = $this->tokenizeString($value); + $matchingRuleUse['oid'] = array_shift($tokens); // first token is the oid + $this->parseLdapSchemaSyntax($matchingRuleUse, $tokens); + + $this->ensureNameAttribute($matchingRuleUse); + + return $matchingRuleUse; + } + + /** + * Ensures that a name element is present and that it is single-values. + * + * @param array $data + */ + protected function ensureNameAttribute(array &$data) + { + if (!array_key_exists('name', $data) || empty($data['name'])) { + // force a name + $data['name'] = $data['oid']; + } + if (is_array($data['name'])) { + // make one name the default and put the other ones into aliases + $aliases = $data['name']; + $data['name'] = array_shift($aliases); + $data['aliases'] = $aliases; + } else { + $data['aliases'] = array(); + } + } + + /** + * Parse the given tokens into a data structure + * + * @param array $data + * @param array $tokens + * @return void + */ + protected function parseLdapSchemaSyntax(array &$data, array $tokens) + { + // tokens that have no value associated + $noValue = array('single-value', + 'obsolete', + 'collective', + 'no-user-modification', + 'abstract', + 'structural', + 'auxiliary'); + // tokens that can have multiple values + $multiValue = array('must', 'may', 'sup'); + + while (count($tokens) > 0) { + $token = strtolower(array_shift($tokens)); + if (in_array($token, $noValue)) { + $data[$token] = true; // single value token + } else { + $data[$token] = array_shift($tokens); + // this one follows a string or a list if it is multivalued + if ($data[$token] == '(') { + // this creates the list of values and cycles through the tokens + // until the end of the list is reached ')' + $data[$token] = array(); + + $tmp = array_shift($tokens); + while ($tmp) { + if ($tmp == ')') { + break; + } + if ($tmp != '$') { + $data[$token][] = Converter\Converter::fromLdap($tmp); + } + $tmp = array_shift($tokens); + } + } else { + $data[$token] = Converter\Converter::fromLdap($data[$token]); + } + // create a array if the value should be multivalued but was not + if (in_array($token, $multiValue) && !is_array($data[$token])) { + $data[$token] = array($data[$token]); + } + } + } + } + + /** + * Tokenizes the given value into an array + * + * @param string $value + * @return array tokens + */ + protected function tokenizeString($value) + { + $tokens = array(); + $matches = array(); + // this one is taken from PEAR::Net_LDAP2 + $pattern = "/\\s* (?:([()]) | ([^'\\s()]+) | '((?:[^']+|'[^\\s)])*)') \\s*/x"; + preg_match_all($pattern, $value, $matches); + $cMatches = count($matches[0]); + $cPattern = count($matches); + for ($i = 0; $i < $cMatches; $i++) { // number of tokens (full pattern match) + for ($j = 1; $j < $cPattern; $j++) { // each subpattern + $tok = trim($matches[$j][$i]); + if (!empty($tok)) { // pattern match in this subpattern + $tokens[$i] = $tok; // this is the token + } + } + } + if ($tokens[0] == '(') { + array_shift($tokens); + } + if ($tokens[count($tokens) - 1] == ')') { + array_pop($tokens); + } + + return $tokens; + } +} diff --git a/test/AbstractOnlineTestCase.php b/test/AbstractOnlineTestCase.php new file mode 100644 index 000000000..08994829c --- /dev/null +++ b/test/AbstractOnlineTestCase.php @@ -0,0 +1,148 @@ +ldap; + } + + protected function setUp() + { + if (!constant('TESTS_ZEND_LDAP_ONLINE_ENABLED')) { + $this->markTestSkipped("Zend_Ldap online tests are not enabled"); + } + + $options = array( + 'host' => TESTS_ZEND_LDAP_HOST, + 'username' => TESTS_ZEND_LDAP_USERNAME, + 'password' => TESTS_ZEND_LDAP_PASSWORD, + 'baseDn' => TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, + ); + if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389) { + $options['port'] = TESTS_ZEND_LDAP_PORT; + } + if (defined('TESTS_ZEND_LDAP_USE_START_TLS')) { + $options['useStartTls'] = TESTS_ZEND_LDAP_USE_START_TLS; + } + if (defined('TESTS_ZEND_LDAP_USE_SSL')) { + $options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL; + } + if (defined('TESTS_ZEND_LDAP_BIND_REQUIRES_DN')) { + $options['bindRequiresDn'] = TESTS_ZEND_LDAP_BIND_REQUIRES_DN; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT')) { + $options['accountFilterFormat'] = TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME')) { + $options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT')) { + $options['accountDomainNameShort'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT; + } + + $this->ldap = new Ldap\Ldap($options); + $this->ldap->bind(); + } + + protected function tearDown() + { + if ($this->ldap !== null) { + $this->ldap->disconnect(); + $this->ldap = null; + } + } + + protected function createDn($dn) + { + if (substr($dn, -1) !== ',') { + $dn .= ','; + } + $dn = $dn . TESTS_ZEND_LDAP_WRITEABLE_SUBTREE; + + return Ldap\Dn::fromString($dn)->toString(Ldap\Dn::ATTR_CASEFOLD_LOWER); + } + + protected function prepareLDAPServer() + { + $this->nodes = array( + $this->createDn('ou=Node,') => + array("objectClass" => "organizationalUnit", + "ou" => "Node", + "postalCode" => "1234"), + $this->createDn('ou=Test1,ou=Node,') => + array("objectClass" => "organizationalUnit", + "ou" => "Test1"), + $this->createDn('ou=Test2,ou=Node,') => + array("objectClass" => "organizationalUnit", + "ou" => "Test2"), + $this->createDn('ou=Test1,') => + array("objectClass" => "organizationalUnit", + "ou" => "Test1", + "l" => "e"), + $this->createDn('ou=Test2,') => + array("objectClass" => "organizationalUnit", + "ou" => "Test2", + "l" => "d"), + $this->createDn('ou=Test3,') => + array("objectClass" => "organizationalUnit", + "ou" => "Test3", + "l" => "c"), + $this->createDn('ou=Test4,') => + array("objectClass" => "organizationalUnit", + "ou" => "Test4", + "l" => "b"), + $this->createDn('ou=Test5,') => + array("objectClass" => "organizationalUnit", + "ou" => "Test5", + "l" => "a"), + ); + + $ldap = $this->ldap->getResource(); + foreach ($this->nodes as $dn => $entry) { + ldap_add($ldap, $dn, $entry); + } + } + + protected function cleanupLDAPServer() + { + if (!constant('TESTS_ZEND_LDAP_ONLINE_ENABLED')) { + return; + } + $ldap = $this->ldap->getResource(); + foreach (array_reverse($this->nodes) as $dn => $entry) { + ldap_delete($ldap, $dn); + } + } +} diff --git a/test/AbstractTestCase.php b/test/AbstractTestCase.php new file mode 100644 index 000000000..493d8dc2c --- /dev/null +++ b/test/AbstractTestCase.php @@ -0,0 +1,46 @@ + 'cn=name,dc=example,dc=org', + 'cn' => array('name'), + 'host' => array('a', 'b', 'c'), + 'empty' => array(), + 'boolean' => array('TRUE', 'FALSE'), + 'objectclass' => array('account', 'top'), + ); + return $data; + } + + /** + * @return Node + */ + protected function createTestNode() + { + return Node::fromArray($this->createTestArrayData(), true); + } +} diff --git a/test/AttributeTest.php b/test/AttributeTest.php new file mode 100644 index 000000000..6e3675abe --- /dev/null +++ b/test/AttributeTest.php @@ -0,0 +1,507 @@ +assertEquals($tsValue, $value); + } + + protected function assertUtcDateTimeString($localTimestamp, $value) + { + $localOffset = date('Z', $localTimestamp); + $utcTimestamp = $localTimestamp - $localOffset; + $this->assertEquals(date('YmdHis', $utcTimestamp) . 'Z', $value); + } + + public function testGetAttributeValue() + { + $data = array('uid' => array('value')); + $value = Attribute::getAttribute($data, 'uid', 0); + $this->assertEquals('value', $value); + } + + public function testGetNonExistentAttributeValue() + { + $data = array('uid' => array('value')); + $value = Attribute::getAttribute($data, 'uid', 1); + $this->assertNull($value); + } + + public function testGetNonExistentAttribute() + { + $data = array('uid' => array('value')); + $value = Attribute::getAttribute($data, 'uid2', 0); + $this->assertNull($value); + $array = Attribute::getAttribute($data, 'uid2'); + $this->assertInternalType('array', $array); + $this->assertEquals(0, count($array)); + } + + public function testGetAttributeWithWrongIndexType() + { + $data = array('uid' => array('value')); + $value = Attribute::getAttribute($data, 'uid', 'index'); + $this->assertNull($value); + $value = Attribute::getAttribute($data, 'uid', 3.1415); + $this->assertNull($value); + } + + public function testGetAttributeArray() + { + $data = array('uid' => array('value')); + $value = Attribute::getAttribute($data, 'uid'); + $this->assertInternalType('array', $value); + $this->assertEquals(1, count($value)); + $this->assertContains('value', $value); + } + + public function testSimpleSetAttribute() + { + $data = array(); + Attribute::setAttribute($data, 'uid', 'new', false); + $this->assertArrayHasKey('uid', $data); + $this->assertInternalType('array', $data['uid']); + $this->assertEquals(1, count($data['uid'])); + $this->assertContains('new', $data['uid']); + } + + public function testSimpleOverwriteAttribute() + { + $data = array('uid' => array('old')); + Attribute::setAttribute($data, 'uid', 'new', false); + $this->assertArrayHasKey('uid', $data); + $this->assertInternalType('array', $data['uid']); + $this->assertEquals(1, count($data['uid'])); + $this->assertContains('new', $data['uid']); + } + + public function testSimpleAppendAttribute() + { + $data = array('uid' => array('old')); + Attribute::setAttribute($data, 'uid', 'new', true); + $this->assertArrayHasKey('uid', $data); + $this->assertInternalType('array', $data['uid']); + $this->assertEquals(2, count($data['uid'])); + $this->assertContains('old', $data['uid']); + $this->assertContains('new', $data['uid']); + $this->assertEquals('old', $data['uid'][0]); + $this->assertEquals('new', $data['uid'][1]); + } + + public function testBooleanAttributeHandling() + { + $data = array( + 'p1_true' => array('TRUE'), + 'p1_false' => array('FALSE') + ); + Attribute::setAttribute($data, 'p2_true', true); + Attribute::setAttribute($data, 'p2_false', false); + $this->assertEquals('TRUE', $data['p2_true'][0]); + $this->assertEquals('FALSE', $data['p2_false'][0]); + $this->assertEquals(true, Attribute::getAttribute($data, 'p1_true', 0)); + $this->assertEquals(false, Attribute::getAttribute($data, 'p1_false', 0)); + } + + public function testArraySetAttribute() + { + $data = array(); + Attribute::setAttribute($data, 'uid', array('new1', 'new2'), false); + $this->assertArrayHasKey('uid', $data); + $this->assertInternalType('array', $data['uid']); + $this->assertEquals(2, count($data['uid'])); + $this->assertContains('new1', $data['uid']); + $this->assertContains('new2', $data['uid']); + $this->assertEquals('new1', $data['uid'][0]); + $this->assertEquals('new2', $data['uid'][1]); + } + + public function testArrayOverwriteAttribute() + { + $data = array('uid' => array('old')); + Attribute::setAttribute($data, 'uid', array('new1', 'new2'), false); + $this->assertArrayHasKey('uid', $data); + $this->assertInternalType('array', $data['uid']); + $this->assertEquals(2, count($data['uid'])); + $this->assertContains('new1', $data['uid']); + $this->assertContains('new2', $data['uid']); + $this->assertEquals('new1', $data['uid'][0]); + $this->assertEquals('new2', $data['uid'][1]); + } + + public function testArrayAppendAttribute() + { + $data = array('uid' => array('old')); + Attribute::setAttribute($data, 'uid', array('new1', 'new2'), true); + $this->assertArrayHasKey('uid', $data); + $this->assertInternalType('array', $data['uid']); + $this->assertEquals(3, count($data['uid'])); + $this->assertContains('old', $data['uid']); + $this->assertContains('new1', $data['uid']); + $this->assertContains('new2', $data['uid']); + $this->assertEquals('old', $data['uid'][0]); + $this->assertEquals('new1', $data['uid'][1]); + $this->assertEquals('new2', $data['uid'][2]); + } + + public function testPasswordSettingSHA() + { + $data = array(); + Attribute::setPassword($data, 'pa$$w0rd', Attribute::PASSWORD_HASH_SHA); + $password = Attribute::getAttribute($data, 'userPassword', 0); + $this->assertEquals('{SHA}vi3X+3ptD4ulrdErXo+3W72mRyE=', $password); + } + + public function testPasswordSettingMD5() + { + $data = array(); + Attribute::setPassword($data, 'pa$$w0rd', Attribute::PASSWORD_HASH_MD5); + $password = Attribute::getAttribute($data, 'userPassword', 0); + $this->assertEquals('{MD5}bJuLJ96h3bhF+WqiVnxnVA==', $password); + } + + public function testPasswordSettingUnicodePwd() + { + $data = array(); + Attribute::setPassword($data, 'new', Attribute::PASSWORD_UNICODEPWD); + $password = Attribute::getAttribute($data, 'unicodePwd', 0); + $this->assertEquals("\x22\x00\x6E\x00\x65\x00\x77\x00\x22\x00", $password); + } + + public function testPasswordSettingCustomAttribute() + { + $data = array(); + Attribute::setPassword($data, 'pa$$w0rd', + Attribute::PASSWORD_HASH_SHA, 'myAttribute' + ); + $password = Attribute::getAttribute($data, 'myAttribute', 0); + $this->assertNotNull($password); + } + + public function testSetAttributeWithObject() + { + $data = array(); + $object = new \stdClass(); + $object->a = 1; + $object->b = 1.23; + $object->c = 'string'; + Attribute::setAttribute($data, 'object', $object); + $this->assertEquals(serialize($object), $data['object'][0]); + } + + public function testSetAttributeWithFilestream() + { + $data = array(); + $stream = fopen(__DIR__ . '/_files/AttributeTest.input.txt', 'r'); + Attribute::setAttribute($data, 'file', $stream); + fclose($stream); + $this->assertEquals('String from file', $data['file'][0]); + } + + public function testSetDateTimeValueLocal() + { + $ts = mktime(12, 30, 30, 6, 25, 2008); + $data = array(); + Attribute::setDateTimeAttribute($data, 'ts', $ts, false); + $this->assertLocalDateTimeString($ts, $data['ts'][0]); + } + + public function testSetDateTimeValueUtc() + { + $ts = mktime(12, 30, 30, 6, 25, 2008); + $data = array(); + Attribute::setDateTimeAttribute($data, 'ts', $ts, true); + $this->assertUtcDateTimeString($ts, $data['ts'][0]); + } + + public function testSetDateTimeValueLocalArray() + { + $ts = array(); + $ts[] = mktime(12, 30, 30, 6, 25, 2008); + $ts[] = mktime(1, 25, 30, 1, 2, 2008); + $data = array(); + Attribute::setDateTimeAttribute($data, 'ts', $ts, false); + $this->assertLocalDateTimeString($ts[0], $data['ts'][0]); + $this->assertLocalDateTimeString($ts[1], $data['ts'][1]); + } + + public function testSetDateTimeValueIllegal() + { + $ts = 'dummy'; + $data = array(); + Attribute::setDateTimeAttribute($data, 'ts', $ts, false); + $this->assertEquals(0, count($data['ts'])); + } + + public function testGetDateTimeValueFromLocal() + { + $ts = mktime(12, 30, 30, 6, 25, 2008); + $data = array(); + Attribute::setDateTimeAttribute($data, 'ts', $ts, false); + $this->assertLocalDateTimeString($ts, $data['ts'][0]); + $retTs = Attribute::getDateTimeAttribute($data, 'ts', 0); + $this->assertEquals($ts, $retTs); + } + + public function testGetDateTimeValueFromUtc() + { + $ts = mktime(12, 30, 30, 6, 25, 2008); + $data = array(); + Attribute::setDateTimeAttribute($data, 'ts', $ts, true); + $this->assertUtcDateTimeString($ts, $data['ts'][0]); + $retTs = Attribute::getDateTimeAttribute($data, 'ts', 0); + $this->assertEquals($ts, $retTs); + } + + public function testGetDateTimeValueFromArray() + { + $ts = array(); + $ts[] = mktime(12, 30, 30, 6, 25, 2008); + $ts[] = mktime(1, 25, 30, 1, 2, 2008); + $data = array(); + Attribute::setDateTimeAttribute($data, 'ts', $ts, false); + $this->assertLocalDateTimeString($ts[0], $data['ts'][0]); + $this->assertLocalDateTimeString($ts[1], $data['ts'][1]); + $retTs = Attribute::getDateTimeAttribute($data, 'ts'); + $this->assertEquals($ts[0], $retTs[0]); + $this->assertEquals($ts[1], $retTs[1]); + } + + public function testGetDateTimeValueIllegal() + { + $data = array('ts' => array('dummy')); + $retTs = Attribute::getDateTimeAttribute($data, 'ts', 0); + $this->assertEquals('dummy', $retTs); + } + + public function testGetDateTimeValueNegativeOffet() + { + $data = array('ts' => array('20080612143045-0700')); + $retTs = Attribute::getDateTimeAttribute($data, 'ts', 0); + $tsCompare = gmmktime(21, 30, 45, 6, 12, 2008); + $this->assertEquals($tsCompare, $retTs); + } + + public function testGetDateTimeValueNegativeOffet2() + { + $data = array('ts' => array('20080612143045-0715')); + $retTs = Attribute::getDateTimeAttribute($data, 'ts', 0); + $tsCompare = gmmktime(21, 45, 45, 6, 12, 2008); + $this->assertEquals($tsCompare, $retTs); + } + + public function testRemoveAttributeValueSimple() + { + $data = array('test' => array('value1', 'value2', 'value3', 'value3')); + Attribute::removeFromAttribute($data, 'test', 'value2'); + $this->assertArrayHasKey('test', $data); + $this->assertInternalType('array', $data['test']); + $this->assertEquals(3, count($data['test'])); + $this->assertContains('value1', $data['test']); + $this->assertContains('value3', $data['test']); + $this->assertNotContains('value2', $data['test']); + } + + public function testRemoveAttributeValueArray() + { + $data = array('test' => array('value1', 'value2', 'value3', 'value3')); + Attribute::removeFromAttribute($data, 'test', array('value1', 'value2')); + $this->assertArrayHasKey('test', $data); + $this->assertInternalType('array', $data['test']); + $this->assertEquals(2, count($data['test'])); + $this->assertContains('value3', $data['test']); + $this->assertNotContains('value1', $data['test']); + $this->assertNotContains('value2', $data['test']); + } + + public function testRemoveAttributeMultipleValueSimple() + { + $data = array('test' => array('value1', 'value2', 'value3', 'value3')); + Attribute::removeFromAttribute($data, 'test', 'value3'); + $this->assertArrayHasKey('test', $data); + $this->assertInternalType('array', $data['test']); + $this->assertEquals(2, count($data['test'])); + $this->assertContains('value1', $data['test']); + $this->assertContains('value2', $data['test']); + $this->assertNotContains('value3', $data['test']); + } + + public function testRemoveAttributeMultipleValueArray() + { + $data = array('test' => array('value1', 'value2', 'value3', 'value3')); + Attribute::removeFromAttribute($data, 'test', array('value1', 'value3')); + $this->assertArrayHasKey('test', $data); + $this->assertInternalType('array', $data['test']); + $this->assertEquals(1, count($data['test'])); + $this->assertContains('value2', $data['test']); + $this->assertNotContains('value1', $data['test']); + $this->assertNotContains('value3', $data['test']); + } + + public function testRemoveAttributeValueBoolean() + { + $data = array('test' => array('TRUE', 'FALSE', 'TRUE', 'FALSE')); + Attribute::removeFromAttribute($data, 'test', false); + $this->assertArrayHasKey('test', $data); + $this->assertInternalType('array', $data['test']); + $this->assertEquals(2, count($data['test'])); + $this->assertContains('TRUE', $data['test']); + $this->assertNotContains('FALSE', $data['test']); + } + + public function testRemoveAttributeValueInteger() + { + $data = array('test' => array('1', '2', '3', '4')); + Attribute::removeFromAttribute($data, 'test', array(2, 4)); + $this->assertArrayHasKey('test', $data); + $this->assertInternalType('array', $data['test']); + $this->assertEquals(2, count($data['test'])); + $this->assertContains('1', $data['test']); + $this->assertContains('3', $data['test']); + $this->assertNotContains('2', $data['test']); + $this->assertNotContains('4', $data['test']); + } + + public function testRemoveDuplicates() + { + $data = array( + 'strings1' => array('value1', 'value2', 'value2', 'value3'), + 'strings2' => array('value1', 'value2', 'value3', 'value4'), + 'boolean1' => array('TRUE', 'TRUE', 'TRUE', 'TRUE'), + 'boolean2' => array('TRUE', 'FALSE', 'TRUE', 'FALSE'), + ); + $expected = array( + 'strings1' => array('value1', 'value2', 'value3'), + 'strings2' => array('value1', 'value2', 'value3', 'value4'), + 'boolean1' => array('TRUE'), + 'boolean2' => array('TRUE', 'FALSE'), + ); + Attribute::removeDuplicatesFromAttribute($data, 'strings1'); + Attribute::removeDuplicatesFromAttribute($data, 'strings2'); + Attribute::removeDuplicatesFromAttribute($data, 'boolean1'); + Attribute::removeDuplicatesFromAttribute($data, 'boolean2'); + $this->assertEquals($expected, $data); + } + + public function testHasValue() + { + $data = array( + 'strings1' => array('value1', 'value2', 'value2', 'value3'), + 'strings2' => array('value1', 'value2', 'value3', 'value4'), + 'boolean1' => array('TRUE', 'TRUE', 'TRUE', 'TRUE'), + 'boolean2' => array('TRUE', 'FALSE', 'TRUE', 'FALSE'), + ); + + $this->assertTrue(Attribute::attributeHasValue($data, 'strings1', 'value1')); + $this->assertFalse(Attribute::attributeHasValue($data, 'strings1', 'value4')); + $this->assertTrue(Attribute::attributeHasValue($data, 'boolean1', true)); + $this->assertFalse(Attribute::attributeHasValue($data, 'boolean1', false)); + + $this->assertTrue(Attribute::attributeHasValue($data, 'strings1', + array('value1', 'value2') + ) + ); + $this->assertTrue(Attribute::attributeHasValue($data, 'strings1', + array('value1', 'value2', 'value3') + ) + ); + $this->assertFalse(Attribute::attributeHasValue($data, 'strings1', + array('value1', 'value2', 'value3', 'value4') + ) + ); + $this->assertTrue(Attribute::attributeHasValue($data, 'strings2', + array('value1', 'value2', 'value3', 'value4') + ) + ); + + $this->assertTrue(Attribute::attributeHasValue($data, 'boolean2', + array(true, false) + ) + ); + $this->assertFalse(Attribute::attributeHasValue($data, 'boolean1', + array(true, false) + ) + ); + } + + public function testPasswordGenerationSSHA() + { + $password = 'pa$$w0rd'; + $ssha = Attribute::createPassword($password, Attribute::PASSWORD_HASH_SSHA); + $encoded = substr($ssha, strpos($ssha, '}')); + $binary = base64_decode($encoded); + $this->assertEquals(24, strlen($binary)); + $hash = substr($binary, 0, 20); + $salt = substr($binary, 20); + $this->assertEquals(4, strlen($salt)); + $this->assertEquals(sha1($password . $salt, true), $hash); + } + + public function testPasswordGenerationSHA() + { + $password = 'pa$$w0rd'; + $sha = Attribute::createPassword($password, Attribute::PASSWORD_HASH_SHA); + $encoded = substr($sha, strpos($sha, '}')); + $binary = base64_decode($encoded); + $this->assertEquals(20, strlen($binary)); + $this->assertEquals(sha1($password, true), $binary); + } + + public function testPasswordGenerationSMD5() + { + $password = 'pa$$w0rd'; + $smd5 = Attribute::createPassword($password, Attribute::PASSWORD_HASH_SMD5); + $encoded = substr($smd5, strpos($smd5, '}')); + $binary = base64_decode($encoded); + $this->assertEquals(20, strlen($binary)); + $hash = substr($binary, 0, 16); + $salt = substr($binary, 16); + $this->assertEquals(4, strlen($salt)); + $this->assertEquals(md5($password . $salt, true), $hash); + } + + public function testPasswordGenerationMD5() + { + $password = 'pa$$w0rd'; + $md5 = Attribute::createPassword($password, Attribute::PASSWORD_HASH_MD5); + $encoded = substr($md5, strpos($md5, '}')); + $binary = base64_decode($encoded); + $this->assertEquals(16, strlen($binary)); + $this->assertEquals(md5($password, true), $binary); + } + + public function testPasswordGenerationUnicodePwd() + { + $password = 'new'; + $unicodePwd = Attribute::createPassword($password, Attribute::PASSWORD_UNICODEPWD); + $this->assertEquals(10, strlen($unicodePwd)); + $this->assertEquals("\x22\x00\x6E\x00\x65\x00\x77\x00\x22\x00", $unicodePwd); + } +} diff --git a/test/BindTest.php b/test/BindTest.php new file mode 100644 index 000000000..c55480627 --- /dev/null +++ b/test/BindTest.php @@ -0,0 +1,274 @@ +markTestSkipped("Zend_Ldap online tests are not enabled"); + } + + $this->options = array( + 'host' => TESTS_ZEND_LDAP_HOST, + 'username' => TESTS_ZEND_LDAP_USERNAME, + 'password' => TESTS_ZEND_LDAP_PASSWORD, + 'baseDn' => TESTS_ZEND_LDAP_BASE_DN, + ); + if (defined('TESTS_ZEND_LDAP_PORT')) { + $this->options['port'] = TESTS_ZEND_LDAP_PORT; + } + if (defined('TESTS_ZEND_LDAP_USE_START_TLS')) { + $this->options['useStartTls'] = TESTS_ZEND_LDAP_USE_START_TLS; + } + if (defined('TESTS_ZEND_LDAP_USE_SSL')) { + $this->options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL; + } + if (defined('TESTS_ZEND_LDAP_BIND_REQUIRES_DN')) { + $this->options['bindRequiresDn'] = TESTS_ZEND_LDAP_BIND_REQUIRES_DN; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT')) { + $this->options['accountFilterFormat'] = TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME')) { + $this->options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT')) { + $this->options['accountDomainNameShort'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT; + } + if (defined('TESTS_ZEND_LDAP_ALT_USERNAME')) { + $this->altUsername = TESTS_ZEND_LDAP_ALT_USERNAME; + } + + if (isset($this->options['bindRequiresDn'])) { + $this->bindRequiresDn = $this->options['bindRequiresDn']; + } + } + + public function testEmptyOptionsBind() + { + $ldap = new Ldap\Ldap(array()); + try { + $ldap->bind(); + $this->fail('Expected exception for empty options'); + } catch (Exception\LdapException $zle) { + $this->assertContains('A host parameter is required', $zle->getMessage()); + } + } + + public function testAnonymousBind() + { + $options = $this->options; + unset($options['password']); + + $ldap = new Ldap\Ldap($options); + try { + $ldap->bind(); + } catch (Exception\LdapException $zle) { + // or I guess the server doesn't allow unauthenticated binds + $this->assertContains('unauthenticated bind', $zle->getMessage()); + } + } + + public function testNoBaseDnBind() + { + $options = $this->options; + unset($options['baseDn']); + $options['bindRequiresDn'] = true; + + $ldap = new Ldap\Ldap($options); + try { + $ldap->bind('invalid', 'ignored'); + $this->fail('Expected exception for baseDn missing'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Base DN not set', $zle->getMessage()); + } + } + + public function testNoDomainNameBind() + { + $options = $this->options; + unset($options['accountDomainName']); + $options['bindRequiresDn'] = false; + $options['accountCanonicalForm'] = Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL; + + $ldap = new Ldap\Ldap($options); + try { + $ldap->bind('invalid', 'ignored'); + $this->fail('Expected exception for missing accountDomainName'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Option required: accountDomainName', $zle->getMessage()); + } + } + + public function testPlainBind() + { + $ldap = new Ldap\Ldap($this->options); + $ldap->bind(); + $this->assertNotNull($ldap->getResource()); + } + + public function testConnectBind() + { + $ldap = new Ldap\Ldap($this->options); + $ldap->connect()->bind(); + $this->assertNotNull($ldap->getResource()); + } + + public function testExplicitParamsBind() + { + $options = $this->options; + $username = $options['username']; + $password = $options['password']; + + unset($options['username']); + unset($options['password']); + + $ldap = new Ldap\Ldap($options); + $ldap->bind($username, $password); + $this->assertNotNull($ldap->getResource()); + } + + public function testRequiresDnBind() + { + $options = $this->options; + + $options['bindRequiresDn'] = true; + + $ldap = new Ldap\Ldap($options); + try { + $ldap->bind($this->altUsername, 'invalid'); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid credentials', $zle->getMessage()); + } + } + + public function testRequiresDnWithoutDnBind() + { + $options = $this->options; + + $options['bindRequiresDn'] = true; + + unset($options['username']); + + $ldap = new Ldap\Ldap($options); + try { + $ldap->bind($this->principalName); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + /* Note that if your server actually allows anonymous binds this test will fail. + */ + $this->assertContains('Failed to retrieve DN', $zle->getMessage()); + } + } + + public function testBindWithEmptyPassword() + { + $options = $this->options; + $options['allowEmptyPassword'] = false; + $ldap = new Ldap\Ldap($options); + try { + $ldap->bind($this->altUsername, ''); + $this->fail('Expected exception for empty password'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Empty password not allowed - see allowEmptyPassword option.', + $zle->getMessage() + ); + } + + $options['allowEmptyPassword'] = true; + $ldap = new Ldap\Ldap($options); + try { + $ldap->bind($this->altUsername, ''); + } catch (Exception\LdapException $zle) { + if ($zle->getMessage() === + 'Empty password not allowed - see allowEmptyPassword option.' + ) { + $this->fail('Exception for empty password'); + } else { + $message = $zle->getMessage(); + $this->assertTrue(strstr($message, 'Invalid credentials') + || strstr($message, 'Server is unwilling to perform') + ); + return; + } + } + $this->assertNotNull($ldap->getResource()); + } + + public function testBindWithoutDnUsernameAndDnRequired() + { + $options = $this->options; + $options['username'] = TESTS_ZEND_LDAP_ALT_USERNAME; + $options['bindRequiresDn'] = true; + $ldap = new Ldap\Ldap($options); + try { + $ldap->bind(); + $this->fail('Expected exception for empty password'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Binding requires username in DN form', + $zle->getMessage() + ); + } + } + + /** + * @group ZF-8259 + */ + public function testBoundUserIsFalseIfNotBoundToLDAP() + { + $ldap = new Ldap\Ldap($this->options); + $this->assertFalse($ldap->getBoundUser()); + } + + /** + * @group ZF-8259 + */ + public function testBoundUserIsReturnedAfterBinding() + { + $ldap = new Ldap\Ldap($this->options); + $ldap->bind(); + $this->assertEquals(TESTS_ZEND_LDAP_USERNAME, $ldap->getBoundUser()); + } + + /** + * @group ZF-8259 + */ + public function testResourceIsAlwaysReturned() + { + $ldap = new Ldap\Ldap($this->options); + $this->assertNotNull($ldap->getResource()); + $this->assertTrue(is_resource($ldap->getResource())); + $this->assertEquals(TESTS_ZEND_LDAP_USERNAME, $ldap->getBoundUser()); + } +} diff --git a/test/CanonTest.php b/test/CanonTest.php new file mode 100644 index 000000000..9991c0b97 --- /dev/null +++ b/test/CanonTest.php @@ -0,0 +1,456 @@ +markTestSkipped("Zend_Ldap online tests are not enabled"); + } + + $this->options = array( + 'host' => TESTS_ZEND_LDAP_HOST, + 'username' => TESTS_ZEND_LDAP_USERNAME, + 'password' => TESTS_ZEND_LDAP_PASSWORD, + 'baseDn' => TESTS_ZEND_LDAP_BASE_DN, + ); + if (defined('TESTS_ZEND_LDAP_PORT')) { + $this->options['port'] = TESTS_ZEND_LDAP_PORT; + } + if (defined('TESTS_ZEND_LDAP_USE_START_TLS')) { + $this->options['useStartTls'] = TESTS_ZEND_LDAP_USE_START_TLS; + } + if (defined('TESTS_ZEND_LDAP_USE_SSL')) { + $this->options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL; + } + if (defined('TESTS_ZEND_LDAP_BIND_REQUIRES_DN')) { + $this->options['bindRequiresDn'] = TESTS_ZEND_LDAP_BIND_REQUIRES_DN; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT')) { + $this->options['accountFilterFormat'] = TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME')) { + $this->options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT')) { + $this->options['accountDomainNameShort'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT; + } + } + + public function testPlainCanon() + { + $ldap = new Ldap\Ldap($this->options); + /* This test tries to canonicalize each name (uname, uname@example.com, + * EXAMPLE\uname) to each of the 3 forms (username, principal and backslash) + * for a total of canonicalizations. + */ + if (defined('TESTS_ZEND_LDAP_ALT_USERNAME')) { + $names[Ldap\Ldap::ACCTNAME_FORM_USERNAME] = TESTS_ZEND_LDAP_ALT_USERNAME; + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME')) { + $names[Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL] + = TESTS_ZEND_LDAP_ALT_USERNAME . '@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT')) { + $names[Ldap\Ldap::ACCTNAME_FORM_BACKSLASH] + = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\' . TESTS_ZEND_LDAP_ALT_USERNAME; + } + } + + foreach ($names as $form => $name) { + $ret = $ldap->getCanonicalAccountName($name, $form); + $this->assertEquals($names[$form], $ret); + } + } + + public function testInvalidAccountCanon() + { + $ldap = new Ldap\Ldap($this->options); + try { + $ldap->bind('invalid', 'invalid'); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $msg = $zle->getMessage(); + $this->assertTrue(strstr($msg, 'Invalid credentials') + || strstr($msg, 'No such object') + || strstr($msg, 'No object found') + ); + } + } + + public function testDnCanon() + { + $ldap = new Ldap\Ldap($this->options); + $name = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME, Ldap\Ldap::ACCTNAME_FORM_DN); + $this->assertEquals(TESTS_ZEND_LDAP_ALT_DN, $name); + } + + public function testMismatchDomainBind() + { + $ldap = new Ldap\Ldap($this->options); + try { + $ldap->bind('BOGUS\\doesntmatter', 'doesntmatter'); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertTrue($zle->getCode() == Exception\LdapException::LDAP_X_DOMAIN_MISMATCH); + } + } + + public function testAccountCanonization() + { + $options = $this->options; + $ldap = new Ldap\Ldap($options); + + $canonDn = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME, + Ldap\Ldap::ACCTNAME_FORM_DN + ); + $this->assertEquals(TESTS_ZEND_LDAP_ALT_DN, $canonDn); + $canonUsername = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME, + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->assertEquals(TESTS_ZEND_LDAP_ALT_USERNAME, $canonUsername); + $canonBackslash = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME, + Ldap\Ldap::ACCTNAME_FORM_BACKSLASH + ); + $this->assertEquals( + TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\' . TESTS_ZEND_LDAP_ALT_USERNAME, + $canonBackslash + ); + $canonPrincipal = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME, + Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL + ); + $this->assertEquals( + TESTS_ZEND_LDAP_ALT_USERNAME . '@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, + $canonPrincipal + ); + + $options['accountCanonicalForm'] = Ldap\Ldap::ACCTNAME_FORM_USERNAME; + $ldap->setOptions($options); + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME); + $this->assertEquals(TESTS_ZEND_LDAP_ALT_USERNAME, $canon); + + $options['accountCanonicalForm'] = Ldap\Ldap::ACCTNAME_FORM_BACKSLASH; + $ldap->setOptions($options); + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME); + $this->assertEquals( + TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\' . TESTS_ZEND_LDAP_ALT_USERNAME, $canon + ); + + $options['accountCanonicalForm'] = Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL; + $ldap->setOptions($options); + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME); + $this->assertEquals( + TESTS_ZEND_LDAP_ALT_USERNAME . '@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, $canon + ); + + unset($options['accountCanonicalForm']); + + unset($options['accountDomainName']); + $ldap->setOptions($options); + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME); + $this->assertEquals( + TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\' . TESTS_ZEND_LDAP_ALT_USERNAME, $canon + ); + + unset($options['accountDomainNameShort']); + $ldap->setOptions($options); + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME); + $this->assertEquals(TESTS_ZEND_LDAP_ALT_USERNAME, $canon); + + $options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME; + $ldap->setOptions($options); + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME); + $this->assertEquals( + TESTS_ZEND_LDAP_ALT_USERNAME . '@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, $canon + ); + } + + public function testDefaultAccountFilterFormat() + { + $options = $this->options; + + unset($options['accountFilterFormat']); + $options['bindRequiresDn'] = true; + $ldap = new Ldap\Ldap($options); + try { + $canon = $ldap->getCanonicalAccountName('invalid', Ldap\Ldap::ACCTNAME_FORM_DN); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('(&(objectClass=posixAccount)(uid=invalid))', $zle->getMessage()); + } + + $options['bindRequiresDn'] = false; + $ldap = new Ldap\Ldap($options); + try { + $canon = $ldap->getCanonicalAccountName('invalid', Ldap\Ldap::ACCTNAME_FORM_DN); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('(&(objectClass=user)(sAMAccountName=invalid))', $zle->getMessage()); + } + } + + public function testPossibleAuthority() + { + $options = $this->options; + $ldap = new Ldap\Ldap($options); + try { + $canon = $ldap->getCanonicalAccountName('invalid\invalid', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Binding domain is not an authority for user: invalid\invalid', + $zle->getMessage() + ); + } + try { + $canon = $ldap->getCanonicalAccountName('invalid@invalid.tld', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Binding domain is not an authority for user: invalid@invalid.tld', + $zle->getMessage() + ); + } + + unset($options['accountDomainName']); + $ldap = new Ldap\Ldap($options); + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\invalid', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->assertEquals('invalid', $canon); + try { + $canon = $ldap->getCanonicalAccountName('invalid@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Binding domain is not an authority for user: invalid@' . + TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, + $zle->getMessage() + ); + } + + unset($options['accountDomainNameShort']); + $options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME; + $ldap = new Ldap\Ldap($options); + try { + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\invalid', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Binding domain is not an authority for user: ' . + TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\invalid', + $zle->getMessage() + ); + } + + $canon = $ldap->getCanonicalAccountName('invalid@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->assertEquals('invalid', $canon); + + unset($options['accountDomainName']); + $ldap = new Ldap\Ldap($options); + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\invalid', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->assertEquals('invalid', $canon); + $canon = $ldap->getCanonicalAccountName('invalid@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->assertEquals('invalid', $canon); + } + + public function testInvalidAccountName() + { + $options = $this->options; + $ldap = new Ldap\Ldap($options); + + try { + $canon = $ldap->getCanonicalAccountName('0@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid account name syntax: 0@' . + TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, + $zle->getMessage() + ); + } + + try { + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\0', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid account name syntax: ' . + TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\0', + $zle->getMessage() + ); + } + } + + public function testGetUnknownCanonicalForm() + { + $options = $this->options; + $ldap = new Ldap\Ldap($options); + + try { + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME, 99); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Unknown canonical name form: 99', + $zle->getMessage() + ); + } + } + + public function testGetUnavailableCanoncialForm() + { + $options = $this->options; + unset($options['accountDomainName']); + $ldap = new Ldap\Ldap($options); + try { + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME, + Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL + ); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Option required: accountDomainName', + $zle->getMessage() + ); + } + + unset($options['accountDomainNameShort']); + $ldap = new Ldap\Ldap($options); + try { + $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME, + Ldap\Ldap::ACCTNAME_FORM_BACKSLASH + ); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Option required: accountDomainNameShort', + $zle->getMessage() + ); + } + } + + public function testSplittingOption() + { + $options = $this->options; + unset($options['accountDomainName']); + unset($options['accountDomainNameShort']); + $options['tryUsernameSplit'] = true; + $ldap = new Ldap\Ldap($options); + $this->assertEquals('username', $ldap->getCanonicalAccountName('username@example.com', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ) + ); + $this->assertEquals('username', $ldap->getCanonicalAccountName('EXAMPLE\username', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ) + ); + $this->assertEquals('username', $ldap->getCanonicalAccountName('username', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ) + ); + + $options['tryUsernameSplit'] = false; + $ldap = new Ldap\Ldap($options); + $this->assertEquals('username@example.com', + $ldap->getCanonicalAccountName('username@example.com', Ldap\Ldap::ACCTNAME_FORM_USERNAME) + ); + $this->assertEquals('example\username', $ldap->getCanonicalAccountName('EXAMPLE\username', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ) + ); + $this->assertEquals('username', $ldap->getCanonicalAccountName('username', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ) + ); + + } + + /** + * ZF-4495 + */ + public function testSpecialCharacterInUsername() + { + $options = $this->options; + $options['accountDomainName'] = 'example.com'; + $options['accountDomainNameShort'] = 'EXAMPLE'; + $ldap = new Ldap\Ldap($options); + + $this->assertEquals('schäfer', $ldap->getCanonicalAccountName('SCHÄFER@example.com', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ) + ); + $this->assertEquals('schäfer', $ldap->getCanonicalAccountName('EXAMPLE\SCHÄFER', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ) + ); + $this->assertEquals('schäfer', $ldap->getCanonicalAccountName('SCHÄFER', + Ldap\Ldap::ACCTNAME_FORM_USERNAME + ) + ); + + $this->assertEquals('schäfer@example.com', $ldap->getCanonicalAccountName('SCHÄFER@example.com', + Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL + ) + ); + $this->assertEquals('schäfer@example.com', $ldap->getCanonicalAccountName('EXAMPLE\SCHÄFER', + Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL + ) + ); + $this->assertEquals('schäfer@example.com', $ldap->getCanonicalAccountName('SCHÄFER', + Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL + ) + ); + + $this->assertEquals('EXAMPLE\schäfer', $ldap->getCanonicalAccountName('SCHÄFER@example.com', + Ldap\Ldap::ACCTNAME_FORM_BACKSLASH + ) + ); + $this->assertEquals('EXAMPLE\schäfer', $ldap->getCanonicalAccountName('EXAMPLE\SCHÄFER', + Ldap\Ldap::ACCTNAME_FORM_BACKSLASH + ) + ); + $this->assertEquals('EXAMPLE\schäfer', $ldap->getCanonicalAccountName('SCHÄFER', + Ldap\Ldap::ACCTNAME_FORM_BACKSLASH + ) + ); + } +} diff --git a/test/ChangePasswordTest.php b/test/ChangePasswordTest.php new file mode 100644 index 000000000..8320bb9ed --- /dev/null +++ b/test/ChangePasswordTest.php @@ -0,0 +1,213 @@ +getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_OPENLDAP + ) { + $this->markTestSkipped('Test can only be run on an OpenLDAP server'); + } + + $dn = $this->createDn('uid=newuser,'); + $data = array(); + $password = 'pa$$w0rd'; + Ldap\Attribute::setAttribute($data, 'uid', 'newuser', false); + Ldap\Attribute::setAttribute($data, 'objectClass', 'account', true); + Ldap\Attribute::setAttribute($data, 'objectClass', 'simpleSecurityObject', true); + Ldap\Attribute::setPassword($data, $password, + Ldap\Attribute::PASSWORD_HASH_SSHA, 'userPassword' + ); + + try { + $this->getLDAP()->add($dn, $data); + + $this->assertInstanceOf('Zend\Ldap\Ldap', $this->getLDAP()->bind($dn, $password)); + + $this->getLDAP()->bind(); + $this->getLDAP()->delete($dn); + } catch (Exception\LdapException $e) { + $this->getLDAP()->bind(); + if ($this->getLDAP()->exists($dn)) { + $this->getLDAP()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + public function testChangePasswordWithUserAccountOpenLDAP() + { + if ($this->getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_OPENLDAP + ) { + $this->markTestSkipped('Test can only be run on an OpenLDAP server'); + } + + $dn = $this->createDn('uid=newuser,'); + $data = array(); + $password = 'pa$$w0rd'; + Ldap\Attribute::setAttribute($data, 'uid', 'newuser', false); + Ldap\Attribute::setAttribute($data, 'objectClass', 'account', true); + Ldap\Attribute::setAttribute($data, 'objectClass', 'simpleSecurityObject', true); + Ldap\Attribute::setPassword($data, $password, + Ldap\Attribute::PASSWORD_HASH_SSHA, 'userPassword' + ); + + try { + $this->getLDAP()->add($dn, $data); + + $this->getLDAP()->bind($dn, $password); + + $newPasswd = 'newpasswd'; + $newData = array(); + Ldap\Attribute::setPassword($newData, $newPasswd, + Ldap\Attribute::PASSWORD_HASH_SHA, 'userPassword' + ); + $this->getLDAP()->update($dn, $newData); + + try { + $this->getLDAP()->bind($dn, $password); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $message = $zle->getMessage(); + $this->assertTrue(strstr($message, 'Invalid credentials') + || strstr($message, 'Server is unwilling to perform') + ); + } + + $this->assertInstanceOf('Zend\Ldap\Ldap', $this->getLDAP()->bind($dn, $newPasswd)); + + $this->getLDAP()->bind(); + $this->getLDAP()->delete($dn); + } catch (Exception\LdapException $e) { + $this->getLDAP()->bind(); + if ($this->getLDAP()->exists($dn)) { + $this->getLDAP()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + public function testAddNewUserWithPasswordActiveDirectory() + { + if ($this->getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_ACTIVEDIRECTORY + ) { + $this->markTestSkipped('Test can only be run on an ActiveDirectory server'); + } + $options = $this->getLDAP()->getOptions(); + if ($options['useSsl'] !== true && $options['useStartTls'] !== true) { + $this->markTestSkipped('Test can only be run on an SSL or TLS secured connection'); + } + + $dn = $this->createDn('cn=New User,'); + $data = array(); + $password = 'pa$$w0rd'; + Ldap\Attribute::setAttribute($data, 'cn', 'New User', false); + Ldap\Attribute::setAttribute($data, 'displayName', 'New User', false); + Ldap\Attribute::setAttribute($data, 'sAMAccountName', 'newuser', false); + Ldap\Attribute::setAttribute($data, 'userAccountControl', 512, false); + Ldap\Attribute::setAttribute($data, 'objectClass', 'person', true); + Ldap\Attribute::setAttribute($data, 'objectClass', 'organizationalPerson', true); + Ldap\Attribute::setAttribute($data, 'objectClass', 'user', true); + Ldap\Attribute::setPassword($data, $password, + Ldap\Attribute::PASSWORD_UNICODEPWD, 'unicodePwd' + ); + + try { + $this->getLDAP()->add($dn, $data); + + $this->assertInstanceOf('Zend\Ldap', $this->getLDAP()->bind($dn, $password)); + + $this->getLDAP()->bind(); + $this->getLDAP()->delete($dn); + } catch (Exception\LdapException $e) { + $this->getLDAP()->bind(); + if ($this->getLDAP()->exists($dn)) { + $this->getLDAP()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + public function testChangePasswordWithUserAccountActiveDirectory() + { + if ($this->getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_ACTIVEDIRECTORY + ) { + $this->markTestSkipped('Test can only be run on an ActiveDirectory server'); + } + $options = $this->getLDAP()->getOptions(); + if ($options['useSsl'] !== true && $options['useStartTls'] !== true) { + $this->markTestSkipped('Test can only be run on an SSL or TLS secured connection'); + } + + $dn = $this->createDn('cn=New User,'); + $data = array(); + $password = 'pa$$w0rd'; + Ldap\Attribute::setAttribute($data, 'cn', 'New User', false); + Ldap\Attribute::setAttribute($data, 'displayName', 'New User', false); + Ldap\Attribute::setAttribute($data, 'sAMAccountName', 'newuser', false); + Ldap\Attribute::setAttribute($data, 'userAccountControl', 512, false); + Ldap\Attribute::setAttribute($data, 'objectClass', 'person', true); + Ldap\Attribute::setAttribute($data, 'objectClass', 'organizationalPerson', true); + Ldap\Attribute::setAttribute($data, 'objectClass', 'user', true); + Ldap\Attribute::setPassword($data, $password, + Ldap\Attribute::PASSWORD_UNICODEPWD, 'unicodePwd' + ); + + try { + $this->getLDAP()->add($dn, $data); + + $this->getLDAP()->bind($dn, $password); + + $newPasswd = 'newpasswd'; + $newData = array(); + Ldap\Attribute::setPassword($newData, $newPasswd, Ldap\Attribute::PASSWORD_UNICODEPWD); + $this->getLDAP()->update($dn, $newData); + + try { + $this->getLDAP()->bind($dn, $password); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $message = $zle->getMessage(); + $this->assertTrue(strstr($message, 'Invalid credentials') + || strstr($message, 'Server is unwilling to perform') + ); + } + + $this->assertInstanceOf('\Zend\Ldap\Ldap', $this->getLDAP()->bind($dn, $newPasswd)); + + $this->getLDAP()->bind(); + $this->getLDAP()->delete($dn); + } catch (Exception\LdapException $e) { + $this->getLDAP()->bind(); + if ($this->getLDAP()->exists($dn)) { + $this->getLDAP()->delete($dn); + } + $this->fail($e->getMessage()); + } + } +} diff --git a/test/ConnectTest.php b/test/ConnectTest.php new file mode 100644 index 000000000..873d99a01 --- /dev/null +++ b/test/ConnectTest.php @@ -0,0 +1,257 @@ +markTestSkipped("Zend_Ldap online tests are not enabled"); + } + + $this->options = array('host' => TESTS_ZEND_LDAP_HOST); + if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389) { + $this->options['port'] = TESTS_ZEND_LDAP_PORT; + } + if (defined('TESTS_ZEND_LDAP_USE_SSL')) { + $this->options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL; + } + } + + public function testEmptyOptionsConnect() + { + $ldap = new Ldap\Ldap(array()); + try { + $ldap->connect(); + $this->fail('Expected exception for empty options'); + } catch (Exception\LdapException $zle) { + $this->assertContains('host parameter is required', $zle->getMessage()); + } + } + + public function testUnknownHostConnect() + { + $ldap = new Ldap\Ldap(array('host' => 'bogus.example.com')); + try { + // connect doesn't actually try to connect until bind is called + $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored'); + $this->fail('Expected exception for unknown host'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Can\'t contact LDAP server', $zle->getMessage()); + } + } + + public function testPlainConnect() + { + $ldap = new Ldap\Ldap($this->options); + try { + // Connect doesn't actually try to connect until bind is called + // but if we get 'Invalid credentials' then we know the connect + // succeeded. + $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored'); + $this->fail('Expected exception for invalid username'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid credentials', $zle->getMessage()); + } + } + + public function testNetworkTimeoutConnect() + { + $networkTimeout = 1; + $ldap = new Ldap\Ldap(array_merge($this->options, array('networkTimeout' => $networkTimeout))); + + $ldap->connect(); + ldap_get_option($ldap->getResource(), LDAP_OPT_NETWORK_TIMEOUT, $actual); + $this->assertEquals($networkTimeout, $actual); + } + + public function testExplicitParamsConnect() + { + $host = TESTS_ZEND_LDAP_HOST; + $port = 0; + if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389) { + $port = TESTS_ZEND_LDAP_PORT; + } + $useSsl = false; + if (defined('TESTS_ZEND_LDAP_USE_SSL')) { + $useSsl = TESTS_ZEND_LDAP_USE_SSL; + } + + $ldap = new Ldap\Ldap(); + try { + $ldap->connect($host, $port, $useSsl) + ->bind('CN=ignored,DC=example,DC=com', 'ignored'); + $this->fail('Expected exception for invalid username'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid credentials', $zle->getMessage()); + } + } + + public function testExplicitPortConnect() + { + $port = 389; + if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT) { + $port = TESTS_ZEND_LDAP_PORT; + } + if (defined('TESTS_ZEND_LDAP_USE_SSL') && TESTS_ZEND_LDAP_USE_SSL) { + $port = 636; + } + + $ldap = new Ldap\Ldap($this->options); + try { + $ldap->connect(null, $port) + ->bind('CN=ignored,DC=example,DC=com', 'ignored'); + $this->fail('Expected exception for invalid username'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid credentials', $zle->getMessage()); + } + } + + public function testExplicitNetworkTimeoutConnect() + { + $networkTimeout = 1; + $host = TESTS_ZEND_LDAP_HOST; + $port = 0; + if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389) { + $port = TESTS_ZEND_LDAP_PORT; + } + $useSsl = false; + if (defined('TESTS_ZEND_LDAP_USE_SSL')) { + $useSsl = TESTS_ZEND_LDAP_USE_SSL; + } + + $ldap = new Ldap\Ldap(); + $ldap->connect($host, $port, $useSsl, null, $networkTimeout); + ldap_get_option($ldap->getResource(), LDAP_OPT_NETWORK_TIMEOUT, $actual); + $this->assertEquals($networkTimeout, $actual); + } + + public function testBadPortConnect() + { + $options = $this->options; + $options['port'] = 10; + + $ldap = new Ldap\Ldap($options); + try { + $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored'); + $this->fail('Expected exception for unknown username'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Can\'t contact LDAP server', $zle->getMessage()); + } + } + + public function testSetOptionsConnect() + { + $ldap = new Ldap\Ldap(); + $ldap->setOptions($this->options); + try { + $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored'); + $this->fail('Expected exception for invalid username'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid credentials', $zle->getMessage()); + } + } + + public function testMultiConnect() + { + $ldap = new Ldap\Ldap($this->options); + for ($i = 0; $i < 3; $i++) { + try { + $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored'); + $this->fail('Expected exception for unknown username'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid credentials', $zle->getMessage()); + } + } + } + + public function testDisconnect() + { + $ldap = new Ldap\Ldap($this->options); + for ($i = 0; $i < 3; $i++) { + $ldap->disconnect(); + try { + $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored'); + $this->fail('Expected exception for unknown username'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid credentials', $zle->getMessage()); + } + } + } + + public function testGetErrorCode() + { + $ldap = new Ldap\Ldap($this->options); + try { + // Connect doesn't actually try to connect until bind is called + // but if we get 'Invalid credentials' then we know the connect + // succeeded. + $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored'); + $this->fail('Expected exception for invalid username'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid credentials', $zle->getMessage()); + + $this->assertEquals(0x31, $zle->getCode()); + $this->assertEquals(0x0, $ldap->getLastErrorCode()); + } + } + + /** + * @group ZF-8274 + */ + public function testConnectWithUri() + { + $host = TESTS_ZEND_LDAP_HOST; + $port = 0; + if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389) { + $port = TESTS_ZEND_LDAP_PORT; + } + $useSsl = false; + if (defined('TESTS_ZEND_LDAP_USE_SSL')) { + $useSsl = TESTS_ZEND_LDAP_USE_SSL; + } + if ($useSsl) { + $host = 'ldaps://' . $host; + } else { + $host = 'ldap://' . $host; + } + if ($port) { + $host = $host . ':' . $port; + } + + $ldap = new Ldap\Ldap(); + try { + $ldap->connect($host) + ->bind('CN=ignored,DC=example,DC=com', 'ignored'); + $this->fail('Expected exception for invalid username'); + } catch (Exception\LdapException $zle) { + $this->assertContains('Invalid credentials', $zle->getMessage()); + } + } +} diff --git a/test/Converter/ConverterTest.php b/test/Converter/ConverterTest.php new file mode 100644 index 000000000..cfd502108 --- /dev/null +++ b/test/Converter/ConverterTest.php @@ -0,0 +1,255 @@ +?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`' . + 'abcdefghijklmnopqrstuvwxyz{|}~'; + $str = ''; + for ($i = 0; $i < 127; $i++) { + $str .= chr($i); + } + $this->assertEquals($expected, Converter::ascToHex32($str)); + } + + public function testHex2asc() + { + $expected = ''; + for ($i = 0; $i < 127; $i++) { + $expected .= chr($i); + } + + $str = '\00\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b' . + '\1c\1d\1e\1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefg' . + 'hijklmnopqrstuvwxyz{|}~'; + $this->assertEquals($expected, Converter::hex32ToAsc($str)); + } + + /** + * @dataProvider toLdapDateTimeProvider + */ + public function testToLdapDateTime($convert, $expect) + { + $result = Converter::toLdapDatetime($convert['date'], $convert['utc']); + $this->assertEquals($expect, $result); + } + + public function toLdapDateTimeProvider() + { + $tz = new DateTimeZone('UTC'); + return array( + array(array('date'=> 0, + 'utc' => true), '19700101000000Z'), + array(array('date'=> new DateTime('2010-05-12 13:14:45+0300', $tz), + 'utc' => false), '20100512131445+0300'), + array(array('date'=> new DateTime('2010-05-12 13:14:45+0300', $tz), + 'utc' => true), '20100512101445Z'), + array(array('date'=> '2010-05-12 13:14:45+0300', + 'utc' => false), '20100512131445+0300'), + array(array('date'=> '2010-05-12 13:14:45+0300', + 'utc' => true), '20100512101445Z'), + array(array('date'=> DateTime::createFromFormat(DateTime::ISO8601, '2010-05-12T13:14:45+0300'), + 'utc' => true), '20100512101445Z'), + array(array('date'=> DateTime::createFromFormat(DateTime::ISO8601, '2010-05-12T13:14:45+0300'), + 'utc' => false), '20100512131445+0300'), + array(array('date'=> date_timestamp_set(new DateTime(), 0), + 'utc' => true), '19700101000000Z'), + ); + } + + /** + * @dataProvider toLdapBooleanProvider + */ + public function testToLdapBoolean($expect, $convert) + { + $this->assertEquals($expect, Converter::toldapBoolean($convert)); + } + + public function toLdapBooleanProvider() + { + return array( + array('TRUE', true), + array('TRUE', 1), + array('TRUE', 'true'), + array('FALSE', 'false'), + array('FALSE', false), + array('FALSE', array('true')), + array('FALSE', array('false')), + ); + } + + /** + * @dataProvider toLdapSerializeProvider + */ + public function testToLdapSerialize($expect, $convert) + { + $this->assertEquals($expect, Converter::toLdapSerialize($convert)); + } + + public function toLdapSerializeProvider() + { + return array( + array('N;', null), + array('i:1;', 1), + array('O:8:"DateTime":3:{s:4:"date";s:19:"1970-01-01 00:00:00";s:13:"timezone_type";i:1;s:8:"timezone";s:6:"+00:00";}', + new DateTime('@0')), + array('a:3:{i:0;s:4:"test";i:1;i:1;s:3:"foo";s:3:"bar";}', array('test', 1, + 'foo'=> 'bar')), + ); + } + + /** + * @dataProvider toLdapProvider + */ + public function testToLdap($expect, $convert) + { + $this->assertEquals($expect, Converter::toLdap($convert['value'], $convert['type'])); + } + + public function toLdapProvider() + { + return array( + array(null, array('value' => null, + 'type' => 0)), + array('19700101000000Z', array('value'=> 0, + 'type' => 2)), + array('0', array('value'=> 0, + 'type' => 0)), + array('FALSE', array('value'=> 0, + 'type' => 1)), + array('19700101000000Z', array('value'=> DateTime::createFromFormat(DateTime::ISO8601, '1970-01-01T00:00:00+0000'), + 'type' => 0)), + + ); + } + + /** + * @dataProvider fromLdapUnserializeProvider + */ + public function testFromLdapUnserialize($expect, $convert) + { + $this->assertEquals($expect, Converter::fromLdapUnserialize($convert)); + } + + public function testFromLdapUnserializeThrowsException() + { + $this->setExpectedException('UnexpectedValueException'); + Converter::fromLdapUnserialize('--'); + } + + public function fromLdapUnserializeProvider() + { + return array( + array(null, 'N;'), + array(1, 'i:1;'), + array(false, 'b:0;'), + ); + } + + public function testFromLdapBoolean() + { + $this->assertTrue(Converter::fromLdapBoolean('TRUE')); + $this->assertFalse(Converter::fromLdapBoolean('FALSE')); + $this->setExpectedException('InvalidArgumentException'); + Converter::fromLdapBoolean('test'); + } + + /** + * @dataProvider fromLdapDateTimeProvider + * + * @param DateTime $expected + * @param string $convert + * @param boolean $utc + * @return void + */ + public function testFromLdapDateTime($expected, $convert, $utc) + { + if (true === $utc) { + $expected->setTimezone(new DateTimeZone('UTC')); + } + $this->assertEquals($expected, Converter::fromLdapDatetime($convert, $utc)); + } + + public function fromLdapDateTimeProvider() + { + return array( + array(new DateTime('2010-12-24 08:00:23+0300'), '20101224080023+0300', false), + array(new DateTime('2010-12-24 08:00:23+0300'), '20101224080023+03\'00\'', false), + array(new DateTime('2010-12-24 08:00:23+0000'), '20101224080023', false), + array(new DateTime('2010-12-24 08:00:00+0000'), '201012240800', false), + array(new DateTime('2010-12-24 08:00:00+0000'), '2010122408', false), + array(new DateTime('2010-12-24 00:00:00+0000'), '20101224', false), + array(new DateTime('2010-12-01 00:00:00+0000'), '201012', false), + array(new DateTime('2010-01-01 00:00:00+0000'), '2010', false), + array(new DateTime('2010-04-03 12:23:34+0000'), '20100403122334', true), + ); + } + + /** + * @expectedException InvalidArgumentException + * @dataProvider fromLdapDateTimeException + */ + public function testFromLdapDateTimeThrowsException($value) + { + Converter::fromLdapDatetime($value); + } + + public static function fromLdapDateTimeException() + { + return array( + array('foobar'), + array('201'), + array('201013'), + array('20101232'), + array('2010123124'), + array('201012312360'), + array('20101231235960'), + array('20101231235959+13'), + array('20101231235959+1160'), + ); + } + + /** + * @dataProvider fromLdapProvider + */ + public function testFromLdap($expect, $value, $type, $dateTimeAsUtc) + { + $this->assertSame($expect, Converter::fromLdap($value, $type, $dateTimeAsUtc)); + } + + public function fromLdapProvider() + { + return array( + array('1', '1', 0, true), + array('0', '0', 0, true), + array(true, 'TRUE', 0, true), + array(false, 'FALSE', 0, true), + array('123456789', '123456789', 0, true), + // ZF-11639 + array('+123456789', '+123456789', 0, true), + ); + } +} + diff --git a/test/CopyRenameTest.php b/test/CopyRenameTest.php new file mode 100644 index 000000000..98db1fde7 --- /dev/null +++ b/test/CopyRenameTest.php @@ -0,0 +1,364 @@ +prepareLDAPServer(); + + $this->orgDn = $this->createDn('ou=OrgTest,'); + $this->newDn = $this->createDn('ou=NewTest,'); + $this->orgSubTreeDn = $this->createDn('ou=OrgSubtree,'); + $this->newSubTreeDn = $this->createDn('ou=NewSubtree,'); + $this->targetSubTreeDn = $this->createDn('ou=Target,'); + + $this->nodes = array( + $this->orgDn => array("objectClass" => "organizationalUnit", + "ou" => "OrgTest"), + $this->orgSubTreeDn => array("objectClass" => "organizationalUnit", + "ou" => "OrgSubtree"), + 'ou=Subtree1,' . $this->orgSubTreeDn => + array("objectClass" => "organizationalUnit", + "ou" => "Subtree1"), + 'ou=Subtree11,ou=Subtree1,' . $this->orgSubTreeDn => + array("objectClass" => "organizationalUnit", + "ou" => "Subtree11"), + 'ou=Subtree12,ou=Subtree1,' . $this->orgSubTreeDn => + array("objectClass" => "organizationalUnit", + "ou" => "Subtree12"), + 'ou=Subtree13,ou=Subtree1,' . $this->orgSubTreeDn => + array("objectClass" => "organizationalUnit", + "ou" => "Subtree13"), + 'ou=Subtree2,' . $this->orgSubTreeDn => + array("objectClass" => "organizationalUnit", + "ou" => "Subtree2"), + 'ou=Subtree3,' . $this->orgSubTreeDn => + array("objectClass" => "organizationalUnit", + "ou" => "Subtree3"), + $this->targetSubTreeDn => array("objectClass" => "organizationalUnit", + "ou" => "Target") + ); + + $ldap = $this->getLDAP()->getResource(); + foreach ($this->nodes as $dn => $entry) { + ldap_add($ldap, $dn, $entry); + } + } + + protected function tearDown() + { + if (!constant('TESTS_ZEND_LDAP_ONLINE_ENABLED')) { + return; + } + if ($this->getLDAP()->exists($this->newDn)) { + $this->getLDAP()->delete($this->newDn, false); + } + if ($this->getLDAP()->exists($this->orgDn)) { + $this->getLDAP()->delete($this->orgDn, false); + } + if ($this->getLDAP()->exists($this->orgSubTreeDn)) { + $this->getLDAP()->delete($this->orgSubTreeDn, true); + } + if ($this->getLDAP()->exists($this->newSubTreeDn)) { + $this->getLDAP()->delete($this->newSubTreeDn, true); + } + if ($this->getLDAP()->exists($this->targetSubTreeDn)) { + $this->getLDAP()->delete($this->targetSubTreeDn, true); + } + + + $this->cleanupLDAPServer(); + parent::tearDown(); + } + + public function testSimpleLeafRename() + { + $org = $this->getLDAP()->getEntry($this->orgDn, array(), true); + $this->getLDAP()->rename($this->orgDn, $this->newDn, false); + $this->assertFalse($this->getLDAP()->exists($this->orgDn)); + $this->assertTrue($this->getLDAP()->exists($this->newDn)); + $new = $this->getLDAP()->getEntry($this->newDn); + $this->assertEquals($org['objectclass'], $new['objectclass']); + $this->assertEquals(array('NewTest'), $new['ou']); + } + + public function testSimpleLeafMoveAlias() + { + $this->getLDAP()->move($this->orgDn, $this->newDn, false); + $this->assertFalse($this->getLDAP()->exists($this->orgDn)); + $this->assertTrue($this->getLDAP()->exists($this->newDn)); + } + + public function testSimpleLeafMoveToSubtree() + { + $this->getLDAP()->moveToSubtree($this->orgDn, $this->orgSubTreeDn, false); + $this->assertFalse($this->getLDAP()->exists($this->orgDn)); + $this->assertTrue($this->getLDAP()->exists('ou=OrgTest,' . $this->orgSubTreeDn)); + } + + /** + * @expectedException Zend\Ldap\Exception\LdapException + */ + public function testRenameSourceNotExists() + { + $this->getLDAP()->rename($this->createDn('ou=DoesNotExist,'), $this->newDn, false); + } + + /** + * @expectedException Zend\Ldap\Exception\LdapException + */ + public function testRenameTargetExists() + { + $this->getLDAP()->rename($this->orgDn, $this->createDn('ou=Test1,'), false); + } + + /** + * @expectedException Zend\Ldap\Exception\LdapException + */ + public function testRenameTargetParentNotExists() + { + $this->getLDAP()->rename($this->orgDn, $this->createDn('ou=Test1,ou=ParentDoesNotExist,'), false); + } + + /** + * @expectedException Zend\Ldap\Exception\LdapException + */ + public function testRenameEmulationSourceNotExists() + { + $this->getLDAP()->rename($this->createDn('ou=DoesNotExist,'), $this->newDn, false, true); + } + + /** + * @expectedException Zend\Ldap\Exception\LdapException + */ + public function testRenameEmulationTargetExists() + { + $this->getLDAP()->rename($this->orgDn, $this->createDn('ou=Test1,'), false, true); + } + + /** + * @expectedException Zend\Ldap\Exception\LdapException + */ + public function testRenameEmulationTargetParentNotExists() + { + $this->getLDAP()->rename($this->orgDn, $this->createDn('ou=Test1,ou=ParentDoesNotExist,'), + false, true + ); + } + + public function testSimpleLeafRenameEmulation() + { + $this->getLDAP()->rename($this->orgDn, $this->newDn, false, true); + $this->assertFalse($this->getLDAP()->exists($this->orgDn)); + $this->assertTrue($this->getLDAP()->exists($this->newDn)); + } + + public function testSimpleLeafCopyToSubtree() + { + $this->getLDAP()->copyToSubtree($this->orgDn, $this->orgSubTreeDn, false); + $this->assertTrue($this->getLDAP()->exists($this->orgDn)); + $this->assertTrue($this->getLDAP()->exists('ou=OrgTest,' . $this->orgSubTreeDn)); + } + + public function testSimpleLeafCopy() + { + $this->getLDAP()->copy($this->orgDn, $this->newDn, false); + $this->assertTrue($this->getLDAP()->exists($this->orgDn)); + $this->assertTrue($this->getLDAP()->exists($this->newDn)); + } + + public function testRecursiveRename() + { + $this->getLDAP()->rename($this->orgSubTreeDn, $this->newSubTreeDn, true); + $this->assertFalse($this->getLDAP()->exists($this->orgSubTreeDn)); + $this->assertTrue($this->getLDAP()->exists($this->newSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren($this->newSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=Subtree1,' . $this->newSubTreeDn)); + } + + public function testRecursiveMoveToSubtree() + { + $this->getLDAP()->moveToSubtree($this->orgSubTreeDn, $this->targetSubTreeDn, true); + $this->assertFalse($this->getLDAP()->exists($this->orgSubTreeDn)); + $this->assertTrue($this->getLDAP()->exists('ou=OrgSubtree,' . $this->targetSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=OrgSubtree,' . $this->targetSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=Subtree1,ou=OrgSubtree,' . $this->targetSubTreeDn)); + } + + public function testRecursiveCopyToSubtree() + { + $this->getLDAP()->copyToSubtree($this->orgSubTreeDn, $this->targetSubTreeDn, true); + $this->assertTrue($this->getLDAP()->exists($this->orgSubTreeDn)); + $this->assertTrue($this->getLDAP()->exists('ou=OrgSubtree,' . $this->targetSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren($this->orgSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=Subtree1,' . $this->orgSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=OrgSubtree,' . $this->targetSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=Subtree1,ou=OrgSubtree,' . $this->targetSubTreeDn)); + } + + public function testRecursiveCopy() + { + $this->getLDAP()->copy($this->orgSubTreeDn, $this->newSubTreeDn, true); + $this->assertTrue($this->getLDAP()->exists($this->orgSubTreeDn)); + $this->assertTrue($this->getLDAP()->exists($this->newSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren($this->orgSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=Subtree1,' . $this->orgSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren($this->newSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=Subtree1,' . $this->newSubTreeDn)); + } + + public function testSimpleLeafRenameWithDnObjects() + { + $orgDn = Ldap\Dn::fromString($this->orgDn); + $newDn = Ldap\Dn::fromString($this->newDn); + + $this->getLDAP()->rename($orgDn, $newDn, false); + $this->assertFalse($this->getLDAP()->exists($orgDn)); + $this->assertTrue($this->getLDAP()->exists($newDn)); + + $this->getLDAP()->move($newDn, $orgDn, false); + $this->assertTrue($this->getLDAP()->exists($orgDn)); + $this->assertFalse($this->getLDAP()->exists($newDn)); + } + + public function testSimpleLeafMoveToSubtreeWithDnObjects() + { + $orgDn = Ldap\Dn::fromString($this->orgDn); + $orgSubTreeDn = Ldap\Dn::fromString($this->orgSubTreeDn); + + $this->getLDAP()->moveToSubtree($orgDn, $orgSubTreeDn, false); + $this->assertFalse($this->getLDAP()->exists($orgDn)); + $this->assertTrue($this->getLDAP()->exists('ou=OrgTest,' . $orgSubTreeDn->toString())); + } + + public function testSimpleLeafRenameEmulationWithDnObjects() + { + $orgDn = Ldap\Dn::fromString($this->orgDn); + $newDn = Ldap\Dn::fromString($this->newDn); + + $this->getLDAP()->rename($orgDn, $newDn, false, true); + $this->assertFalse($this->getLDAP()->exists($orgDn)); + $this->assertTrue($this->getLDAP()->exists($newDn)); + } + + public function testSimpleLeafCopyToSubtreeWithDnObjects() + { + $orgDn = Ldap\Dn::fromString($this->orgDn); + $orgSubTreeDn = Ldap\Dn::fromString($this->orgSubTreeDn); + + $this->getLDAP()->copyToSubtree($orgDn, $orgSubTreeDn, false); + $this->assertTrue($this->getLDAP()->exists($orgDn)); + $this->assertTrue($this->getLDAP()->exists('ou=OrgTest,' . $orgSubTreeDn->toString())); + } + + public function testSimpleLeafCopyWithDnObjects() + { + $orgDn = Ldap\Dn::fromString($this->orgDn); + $newDn = Ldap\Dn::fromString($this->newDn); + + $this->getLDAP()->copy($orgDn, $newDn, false); + $this->assertTrue($this->getLDAP()->exists($orgDn)); + $this->assertTrue($this->getLDAP()->exists($newDn)); + } + + public function testRecursiveRenameWithDnObjects() + { + $orgSubTreeDn = Ldap\Dn::fromString($this->orgSubTreeDn); + $newSubTreeDn = Ldap\Dn::fromString($this->newSubTreeDn); + + $this->getLDAP()->rename($orgSubTreeDn, $newSubTreeDn, true); + $this->assertFalse($this->getLDAP()->exists($orgSubTreeDn)); + $this->assertTrue($this->getLDAP()->exists($newSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren($newSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=Subtree1,' . $newSubTreeDn->toString())); + } + + public function testRecursiveMoveToSubtreeWithDnObjects() + { + $orgSubTreeDn = Ldap\Dn::fromString($this->orgSubTreeDn); + $targetSubTreeDn = Ldap\Dn::fromString($this->targetSubTreeDn); + + $this->getLDAP()->moveToSubtree($orgSubTreeDn, $targetSubTreeDn, true); + $this->assertFalse($this->getLDAP()->exists($orgSubTreeDn)); + $this->assertTrue($this->getLDAP()->exists('ou=OrgSubtree,' . $targetSubTreeDn->toString())); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=OrgSubtree,' . $targetSubTreeDn->toString())); + $this->assertEquals(3, + $this->getLDAP()->countChildren('ou=Subtree1,ou=OrgSubtree,' . $targetSubTreeDn->toString()) + ); + } + + public function testRecursiveCopyToSubtreeWithDnObjects() + { + $orgSubTreeDn = Ldap\Dn::fromString($this->orgSubTreeDn); + $targetSubTreeDn = Ldap\Dn::fromString($this->targetSubTreeDn); + + $this->getLDAP()->copyToSubtree($orgSubTreeDn, $targetSubTreeDn, true); + $this->assertTrue($this->getLDAP()->exists($orgSubTreeDn)); + $this->assertTrue($this->getLDAP()->exists('ou=OrgSubtree,' . $targetSubTreeDn->toString())); + $this->assertEquals(3, $this->getLDAP()->countChildren($orgSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=Subtree1,' . $orgSubTreeDn->toString())); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=OrgSubtree,' . $targetSubTreeDn->toString())); + $this->assertEquals(3, + $this->getLDAP()->countChildren('ou=Subtree1,ou=OrgSubtree,' . $targetSubTreeDn->toString()) + ); + } + + public function testRecursiveCopyWithDnObjects() + { + $orgSubTreeDn = Ldap\Dn::fromString($this->orgSubTreeDn); + $newSubTreeDn = Ldap\Dn::fromString($this->newSubTreeDn); + + $this->getLDAP()->copy($orgSubTreeDn, $newSubTreeDn, true); + $this->assertTrue($this->getLDAP()->exists($orgSubTreeDn)); + $this->assertTrue($this->getLDAP()->exists($newSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren($orgSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=Subtree1,' . $orgSubTreeDn->toString())); + $this->assertEquals(3, $this->getLDAP()->countChildren($newSubTreeDn)); + $this->assertEquals(3, $this->getLDAP()->countChildren('ou=Subtree1,' . $newSubTreeDn->toString())); + } +} diff --git a/test/CrudTest.php b/test/CrudTest.php new file mode 100644 index 000000000..1d0e29fc4 --- /dev/null +++ b/test/CrudTest.php @@ -0,0 +1,498 @@ +createDn('ou=TestCreated,'); + $data = array( + 'ou' => 'TestCreated', + 'objectClass' => 'organizationalUnit' + ); + try { + $this->getLDAP()->add($dn, $data); + $this->assertEquals(1, $this->getLDAP()->count('ou=TestCreated')); + $this->getLDAP()->delete($dn); + $this->assertEquals(0, $this->getLDAP()->count('ou=TestCreated')); + } catch (Exception\LdapException $e) { + if ($this->getLDAP()->exists($dn)) { + $this->getLDAP()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + public function testUpdate() + { + $dn = $this->createDn('ou=TestCreated,'); + $data = array( + 'ou' => 'TestCreated', + 'l' => 'mylocation1', + 'objectClass' => 'organizationalUnit' + ); + try { + $this->getLDAP()->add($dn, $data); + $entry = $this->getLDAP()->getEntry($dn); + $this->assertEquals('mylocation1', $entry['l'][0]); + $entry['l'] = 'mylocation2'; + $this->getLDAP()->update($dn, $entry); + $entry = $this->getLDAP()->getEntry($dn); + $this->getLDAP()->delete($dn); + $this->assertEquals('mylocation2', $entry['l'][0]); + } catch (Exception\LdapException $e) { + if ($this->getLDAP()->exists($dn)) { + $this->getLDAP()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + /** + * @expectedException Zend\Ldap\Exception\LdapException + */ + public function testIllegalAdd() + { + $dn = $this->createDn('ou=TestCreated,ou=Node2,'); + $data = array( + 'ou' => 'TestCreated', + 'objectClass' => 'organizationalUnit' + ); + $this->getLDAP()->add($dn, $data); + $this->getLDAP()->delete($dn); + } + + public function testIllegalUpdate() + { + $dn = $this->createDn('ou=TestCreated,'); + $data = array( + 'ou' => 'TestCreated', + 'objectclass' => 'organizationalUnit' + ); + try { + $this->getLDAP()->add($dn, $data); + $entry = $this->getLDAP()->getEntry($dn); + $entry['objectclass'][] = 'inetOrgPerson'; + + $exThrown = false; + try { + $this->getLDAP()->update($dn, $entry); + } catch (Exception\LdapException $e) { + $exThrown = true; + } + $this->getLDAP()->delete($dn); + if (!$exThrown) { + $this->fail('no exception thrown while illegaly updating entry'); + } + } catch (Exception\LdapException $e) { + $this->fail($e->getMessage()); + } + } + + /** + * @expectedException Zend\Ldap\Exception\LdapException + */ + public function testIllegalDelete() + { + $dn = $this->createDn('ou=TestCreated,'); + $this->getLDAP()->delete($dn); + } + + public function testDeleteRecursively() + { + $topDn = $this->createDn('ou=RecursiveTest,'); + $dn = $topDn; + $data = array('ou' => 'RecursiveTest', + 'objectclass' => 'organizationalUnit' + ); + $this->getLDAP()->add($dn, $data); + for ($level = 1; $level <= 5; $level++) { + $name = 'Level' . $level; + $dn = 'ou=' . $name . ',' . $dn; + $data = array('ou' => $name, + 'objectclass' => 'organizationalUnit'); + $this->getLDAP()->add($dn, $data); + for ($item = 1; $item <= 5; $item++) { + $uid = 'Item' . $item; + $idn = 'ou=' . $uid . ',' . $dn; + $idata = array('ou' => $uid, + 'objectclass' => 'organizationalUnit'); + $this->getLDAP()->add($idn, $idata); + } + } + + $exCaught = false; + try { + $this->getLDAP()->delete($topDn, false); + } catch (Exception\LdapException $e) { + $exCaught = true; + } + $this->assertTrue($exCaught, + 'Execption not raised when deleting item with children without specifiying recursive delete' + ); + $this->getLDAP()->delete($topDn, true); + $this->assertFalse($this->getLDAP()->exists($topDn)); + } + + public function testSave() + { + $dn = $this->createDn('ou=TestCreated,'); + $data = array('ou' => 'TestCreated', + 'objectclass' => 'organizationalUnit'); + try { + $this->getLDAP()->save($dn, $data); + $this->assertTrue($this->getLDAP()->exists($dn)); + $data['l'] = 'mylocation1'; + $this->getLDAP()->save($dn, $data); + $this->assertTrue($this->getLDAP()->exists($dn)); + $entry = $this->getLDAP()->getEntry($dn); + $this->getLDAP()->delete($dn); + $this->assertEquals('mylocation1', $entry['l'][0]); + } catch (Exception\LdapException $e) { + if ($this->getLDAP()->exists($dn)) { + $this->getLDAP()->delete($dn); + } + $this->fail($e->getMessage()); + } + + } + + public function testPrepareLDAPEntryArray() + { + $data = array( + 'a1' => 'TestCreated', + 'a2' => 'account', + 'a3' => null, + 'a4' => '', + 'a5' => array('TestCreated'), + 'a6' => array('account'), + 'a7' => array(null), + 'a8' => array(''), + 'a9' => array('', null, 'account', '', null, 'TestCreated', '', null)); + Ldap\Ldap::prepareLDAPEntryArray($data); + $expected = array( + 'a1' => array('TestCreated'), + 'a2' => array('account'), + 'a3' => array(), + 'a4' => array(), + 'a5' => array('TestCreated'), + 'a6' => array('account'), + 'a7' => array(), + 'a8' => array(), + 'a9' => array('account', 'TestCreated')); + $this->assertEquals($expected, $data); + } + + /** + * @group ZF-7888 + */ + public function testZeroValueMakesItThroughSanitationProcess() + { + $data = array( + 'string' => '0', + 'integer' => 0, + 'stringArray' => array('0'), + 'integerArray' => array(0), + 'null' => null, + 'empty' => '', + 'nullArray' => array(null), + 'emptyArray' => array(''), + ); + Ldap\Ldap::prepareLDAPEntryArray($data); + $expected = array( + 'string' => array('0'), + 'integer' => array('0'), + 'stringarray' => array('0'), + 'integerarray' => array('0'), + 'null' => array(), + 'empty' => array(), + 'nullarray' => array(), + 'emptyarray' => array() + ); + $this->assertEquals($expected, $data); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testPrepareLDAPEntryArrayArrayData() + { + $data = array( + 'a1' => array(array('account'))); + Ldap\Ldap::prepareLDAPEntryArray($data); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testPrepareLDAPEntryArrayObjectData() + { + $class = new \stdClass(); + $class->a = 'b'; + $data = array( + 'a1' => array($class)); + Ldap\Ldap::prepareLDAPEntryArray($data); + } + + public function testAddWithDnObject() + { + $dn = Ldap\Dn::fromString($this->createDn('ou=TestCreated,')); + $data = array( + 'ou' => 'TestCreated', + 'objectclass' => 'organizationalUnit' + ); + try { + $this->getLDAP()->add($dn, $data); + $this->assertEquals(1, $this->getLDAP()->count('ou=TestCreated')); + $this->getLDAP()->delete($dn); + } catch (Exception\LdapException $e) { + $this->fail($e->getMessage()); + } + } + + public function testUpdateWithDnObject() + { + $dn = Ldap\Dn::fromString($this->createDn('ou=TestCreated,')); + $data = array( + 'ou' => 'TestCreated', + 'l' => 'mylocation1', + 'objectclass' => 'organizationalUnit' + ); + try { + $this->getLDAP()->add($dn, $data); + $entry = $this->getLDAP()->getEntry($dn); + $this->assertEquals('mylocation1', $entry['l'][0]); + $entry['l'] = 'mylocation2'; + $this->getLDAP()->update($dn, $entry); + $entry = $this->getLDAP()->getEntry($dn); + $this->getLDAP()->delete($dn); + $this->assertEquals('mylocation2', $entry['l'][0]); + } catch (Exception\LdapException $e) { + $this->fail($e->getMessage()); + } + } + + public function testSaveWithDnObject() + { + $dn = Ldap\Dn::fromString($this->createDn('ou=TestCreated,')); + $data = array('ou' => 'TestCreated', + 'objectclass' => 'organizationalUnit'); + try { + $this->getLDAP()->save($dn, $data); + $this->assertTrue($this->getLDAP()->exists($dn)); + $data['l'] = 'mylocation1'; + $this->getLDAP()->save($dn, $data); + $this->assertTrue($this->getLDAP()->exists($dn)); + $entry = $this->getLDAP()->getEntry($dn); + $this->getLDAP()->delete($dn); + $this->assertEquals('mylocation1', $entry['l'][0]); + } catch (Exception\LdapException $e) { + if ($this->getLDAP()->exists($dn)) { + $this->getLDAP()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + public function testAddObjectClass() + { + $dn = $this->createDn('ou=TestCreated,'); + $data = array( + 'ou' => 'TestCreated', + 'l' => 'mylocation1', + 'objectClass' => 'organizationalUnit' + ); + try { + $this->getLDAP()->add($dn, $data); + $entry = $this->getLDAP()->getEntry($dn); + $entry['objectclass'][] = 'domainRelatedObject'; + $entry['associatedDomain'][] = 'domain'; + $this->getLDAP()->update($dn, $entry); + $entry = $this->getLDAP()->getEntry($dn); + $this->getLDAP()->delete($dn); + + $this->assertEquals('domain', $entry['associateddomain'][0]); + $this->assertContains('organizationalUnit', $entry['objectclass']); + $this->assertContains('domainRelatedObject', $entry['objectclass']); + } catch (Exception\LdapException $e) { + if ($this->getLDAP()->exists($dn)) { + $this->getLDAP()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + public function testRemoveObjectClass() + { + $dn = $this->createDn('ou=TestCreated,'); + $data = array( + 'associatedDomain' => 'domain', + 'ou' => 'TestCreated', + 'l' => 'mylocation1', + 'objectClass' => array('organizationalUnit', 'domainRelatedObject') + ); + try { + $this->getLDAP()->add($dn, $data); + $entry = $this->getLDAP()->getEntry($dn); + $entry['objectclass'] = 'organizationalUnit'; + $entry['associatedDomain'] = null; + $this->getLDAP()->update($dn, $entry); + $entry = $this->getLDAP()->getEntry($dn); + $this->getLDAP()->delete($dn); + + $this->assertArrayNotHasKey('associateddomain', $entry); + $this->assertContains('organizationalUnit', $entry['objectclass']); + $this->assertNotContains('domainRelatedObject', $entry['objectclass']); + } catch (Exception\LdapException $e) { + if ($this->getLDAP()->exists($dn)) { + $this->getLDAP()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + /** + * @group ZF-9564 + */ + public function testAddingEntryWithMissingRdnAttribute() + { + $dn = $this->createDn('ou=TestCreated,'); + $data = array( + 'objectClass' => array('organizationalUnit') + ); + try { + $this->getLdap()->add($dn, $data); + $entry = $this->getLdap()->getEntry($dn); + $this->getLdap()->delete($dn); + $this->assertEquals(array('TestCreated'), $entry['ou']); + + } catch (Exception\LdapException $e) { + if ($this->getLdap()->exists($dn)) { + $this->getLdap()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + /** + * @group ZF-9564 + */ + public function testAddingEntryWithMissingRdnAttributeValue() + { + $dn = $this->createDn('ou=TestCreated,'); + $data = array( + 'ou' => array('SecondOu'), + 'objectClass' => array('organizationalUnit') + ); + try { + $this->getLdap()->add($dn, $data); + $entry = $this->getLdap()->getEntry($dn); + $this->getLdap()->delete($dn); + $this->assertEquals(array('TestCreated', 'SecondOu'), $entry['ou']); + + } catch (Exception\LdapException $e) { + if ($this->getLdap()->exists($dn)) { + $this->getLdap()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + /** + * @group ZF-9564 + */ + public function testAddingEntryThatHasMultipleValuesOnRdnAttribute() + { + $dn = $this->createDn('ou=TestCreated,'); + $data = array( + 'ou' => array('TestCreated', 'SecondOu'), + 'objectClass' => array('organizationalUnit') + ); + try { + $this->getLdap()->add($dn, $data); + $entry = $this->getLdap()->getEntry($dn); + $this->getLdap()->delete($dn); + $this->assertEquals(array('TestCreated', 'SecondOu'), $entry['ou']); + + } catch (Exception\LdapException $e) { + if ($this->getLdap()->exists($dn)) { + $this->getLdap()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + /** + * @group ZF-9564 + */ + public function testUpdatingEntryWithAttributeThatIsAnRdnAttribute() + { + $dn = $this->createDn('ou=TestCreated,'); + $data = array( + 'ou' => array('TestCreated'), + 'objectClass' => array('organizationalUnit') + ); + try { + $this->getLdap()->add($dn, $data); + $entry = $this->getLdap()->getEntry($dn); + + $data = array('ou' => array_merge($entry['ou'], array('SecondOu'))); + $this->getLdap()->update($dn, $data); + $entry = $this->getLdap()->getEntry($dn); + $this->getLdap()->delete($dn); + $this->assertEquals(array('TestCreated', 'SecondOu'), $entry['ou']); + + } catch (Exception\LdapException $e) { + if ($this->getLdap()->exists($dn)) { + $this->getLdap()->delete($dn); + } + $this->fail($e->getMessage()); + } + } + + /** + * @group ZF-9564 + */ + public function testUpdatingEntryWithRdnAttributeValueMissingInData() + { + $dn = $this->createDn('ou=TestCreated,'); + $data = array( + 'ou' => array('TestCreated'), + 'objectClass' => array('organizationalUnit') + ); + try { + $this->getLdap()->add($dn, $data); + $entry = $this->getLdap()->getEntry($dn); + + $data = array('ou' => 'SecondOu'); + $this->getLdap()->update($dn, $data); + $entry = $this->getLdap()->getEntry($dn); + $this->getLdap()->delete($dn); + $this->assertEquals(array('TestCreated', 'SecondOu'), $entry['ou']); + + } catch (Exception\LdapException $e) { + if ($this->getLdap()->exists($dn)) { + $this->getLdap()->delete($dn); + } + $this->fail($e->getMessage()); + } + } +} diff --git a/test/Dn/CreationTest.php b/test/Dn/CreationTest.php new file mode 100644 index 000000000..4d6862e4c --- /dev/null +++ b/test/Dn/CreationTest.php @@ -0,0 +1,201 @@ + 'Baker, Alice'), + array('CN' => 'Users', + 'OU' => 'Lab'), + array('DC' => 'example'), + array('DC' => 'com')); + + $dnString2 = 'cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example,dc=com'; + $dnArray2 = array( + array('cn' => 'Baker, Alice'), + array('cn' => 'Users', + 'ou' => 'Lab'), + array('dc' => 'example'), + array('dc' => 'com')); + + $dnString3 = 'Cn=Baker\\, Alice,Cn=Users+Ou=Lab,Dc=example,Dc=com'; + $dnArray3 = array( + array('Cn' => 'Baker, Alice'), + array('Cn' => 'Users', + 'Ou' => 'Lab'), + array('Dc' => 'example'), + array('Dc' => 'com')); + + $dn11 = Ldap\Dn::fromString($dnString1); + $dn12 = Ldap\Dn::fromArray($dnArray1); + $dn13 = Ldap\Dn::factory($dnString1); + $dn14 = Ldap\Dn::factory($dnArray1); + + $this->assertEquals($dn11, $dn12); + $this->assertEquals($dn11, $dn13); + $this->assertEquals($dn11, $dn14); + + $this->assertEquals($dnString1, $dn11->toString()); + $this->assertEquals($dnString1, $dn11->toString(Ldap\Dn::ATTR_CASEFOLD_UPPER)); + $this->assertEquals($dnString2, $dn11->toString(Ldap\Dn::ATTR_CASEFOLD_LOWER)); + $this->assertEquals($dnArray1, $dn11->toArray()); + $this->assertEquals($dnArray1, $dn11->toArray(Ldap\Dn::ATTR_CASEFOLD_UPPER)); + $this->assertEquals($dnArray2, $dn11->toArray(Ldap\Dn::ATTR_CASEFOLD_LOWER)); + + $dn21 = Ldap\Dn::fromString($dnString2); + $dn22 = Ldap\Dn::fromArray($dnArray2); + $dn23 = Ldap\Dn::factory($dnString2); + $dn24 = Ldap\Dn::factory($dnArray2); + + $this->assertEquals($dn21, $dn22); + $this->assertEquals($dn21, $dn23); + $this->assertEquals($dn21, $dn24); + + $this->assertEquals($dnString2, $dn21->toString()); + $this->assertEquals($dnString1, $dn21->toString(Ldap\Dn::ATTR_CASEFOLD_UPPER)); + $this->assertEquals($dnString2, $dn21->toString(Ldap\Dn::ATTR_CASEFOLD_LOWER)); + $this->assertEquals($dnArray2, $dn21->toArray()); + $this->assertEquals($dnArray1, $dn21->toArray(Ldap\Dn::ATTR_CASEFOLD_UPPER)); + $this->assertEquals($dnArray2, $dn21->toArray(Ldap\Dn::ATTR_CASEFOLD_LOWER)); + $this->assertEquals($dnArray2, $dn22->toArray()); + + $dn31 = Ldap\Dn::fromString($dnString3); + $dn32 = Ldap\Dn::fromArray($dnArray3); + $dn33 = Ldap\Dn::factory($dnString3); + $dn34 = Ldap\Dn::factory($dnArray3); + + $this->assertEquals($dn31, $dn32); + $this->assertEquals($dn31, $dn33); + $this->assertEquals($dn31, $dn34); + + $this->assertEquals($dnString3, $dn31->toString()); + $this->assertEquals($dnString1, $dn31->toString(Ldap\Dn::ATTR_CASEFOLD_UPPER)); + $this->assertEquals($dnString2, $dn31->toString(Ldap\Dn::ATTR_CASEFOLD_LOWER)); + $this->assertEquals($dnArray3, $dn31->toArray()); + $this->assertEquals($dnArray1, $dn31->toArray(Ldap\Dn::ATTR_CASEFOLD_UPPER)); + $this->assertEquals($dnArray2, $dn31->toArray(Ldap\Dn::ATTR_CASEFOLD_LOWER)); + + try { + Ldap\Dn::factory(1); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('Invalid argument type for $dn', $e->getMessage()); + } + } + + public function testDnCreationWithDifferentCaseFoldings() + { + Ldap\Dn::setDefaultCaseFold(Ldap\Dn::ATTR_CASEFOLD_NONE); + + $dnString1 = 'Cn=Baker\\, Alice,Cn=Users+Ou=Lab,Dc=example,Dc=com'; + $dnString2 = 'CN=Baker\\, Alice,CN=Users+OU=Lab,DC=example,DC=com'; + $dnString3 = 'cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example,dc=com'; + + $dn = Ldap\Dn::fromString($dnString1, null); + $this->assertEquals($dnString1, (string)$dn); + $dn->setCaseFold(Ldap\Dn::ATTR_CASEFOLD_UPPER); + $this->assertEquals($dnString2, (string)$dn); + $dn->setCaseFold(Ldap\Dn::ATTR_CASEFOLD_LOWER); + $this->assertEquals($dnString3, (string)$dn); + + $dn = Ldap\Dn::fromString($dnString1, Ldap\Dn::ATTR_CASEFOLD_UPPER); + $this->assertEquals($dnString2, (string)$dn); + $dn->setCaseFold(null); + $this->assertEquals($dnString1, (string)$dn); + $dn->setCaseFold(Ldap\Dn::ATTR_CASEFOLD_LOWER); + $this->assertEquals($dnString3, (string)$dn); + + $dn = Ldap\Dn::fromString($dnString1, Ldap\Dn::ATTR_CASEFOLD_LOWER); + $this->assertEquals($dnString3, (string)$dn); + $dn->setCaseFold(Ldap\Dn::ATTR_CASEFOLD_UPPER); + $this->assertEquals($dnString2, (string)$dn); + $dn->setCaseFold(Ldap\Dn::ATTR_CASEFOLD_LOWER); + $this->assertEquals($dnString3, (string)$dn); + $dn->setCaseFold(Ldap\Dn::ATTR_CASEFOLD_UPPER); + $this->assertEquals($dnString2, (string)$dn); + + Ldap\Dn::setDefaultCaseFold(Ldap\Dn::ATTR_CASEFOLD_UPPER); + $dn = Ldap\Dn::fromString($dnString1, null); + $this->assertEquals($dnString2, (string)$dn); + + Ldap\Dn::setDefaultCaseFold(null); + $dn = Ldap\Dn::fromString($dnString1, null); + $this->assertEquals($dnString1, (string)$dn); + + Ldap\Dn::setDefaultCaseFold(Ldap\Dn::ATTR_CASEFOLD_NONE); + } + + public function testGetRdn() + { + Ldap\Dn::setDefaultCaseFold(Ldap\Dn::ATTR_CASEFOLD_NONE); + + $dnString = 'cn=Baker\\, Alice,cn=Users,dc=example,dc=com'; + $dn = Ldap\Dn::fromString($dnString); + + $this->assertEquals(array('cn' => 'Baker, Alice'), $dn->getRdn()); + $this->assertEquals('cn=Baker\\, Alice', $dn->getRdnString()); + + $dnString = 'Cn=Users+Ou=Lab,dc=example,dc=com'; + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals(array('Cn' => 'Users', + 'Ou' => 'Lab'), $dn->getRdn() + ); + $this->assertEquals('Cn=Users+Ou=Lab', $dn->getRdnString()); + } + + public function testGetParentDn() + { + $dnString = 'cn=Baker\\, Alice,cn=Users,dc=example,dc=com'; + $dn = Ldap\Dn::fromString($dnString); + + $this->assertEquals('cn=Users,dc=example,dc=com', $dn->getParentDn()->toString()); + $this->assertEquals('cn=Users,dc=example,dc=com', $dn->getParentDn(1)->toString()); + $this->assertEquals('dc=example,dc=com', $dn->getParentDn(2)->toString()); + $this->assertEquals('dc=com', $dn->getParentDn(3)->toString()); + + try { + $dn->getParentDn(0)->toString(); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('Cannot retrieve parent DN with given $levelUp', $e->getMessage()); + } + try { + $dn->getParentDn(4)->toString(); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('Cannot retrieve parent DN with given $levelUp', $e->getMessage()); + } + } + + public function testEmptyStringDn() + { + $dnString = ''; + $dn = Ldap\Dn::fromString($dnString); + + $this->assertEquals($dnString, $dn->toString()); + } +} diff --git a/test/Dn/EscapingTest.php b/test/Dn/EscapingTest.php new file mode 100644 index 000000000..02dc0f1e2 --- /dev/null +++ b/test/Dn/EscapingTest.php @@ -0,0 +1,45 @@ +l;u#e=! '; + $expected = '\20\20\16 t\,e\+s\"t\,\\\\v\l\;u\#e\=!\20\20\20\20'; + $this->assertEquals($expected, Ldap\Dn::escapeValue($dnval)); + $this->assertEquals($expected, Ldap\Dn::escapeValue(array($dnval))); + $this->assertEquals(array($expected, $expected, $expected), + Ldap\Dn::escapeValue(array($dnval, $dnval, $dnval)) + ); + } + + public function testUnescapeValues() + { + $dnval = '\\20\\20\\16\\20t\\,e\\+s \\"t\\,\\\\v\\l\\;u\\#e\\=!\\20\\20\\20\\20'; + $expected = ' ' . chr(22) . ' t,e+s "t,\\vl;u#e=! '; + $this->assertEquals($expected, Ldap\Dn::unescapeValue($dnval)); + $this->assertEquals($expected, Ldap\Dn::unescapeValue(array($dnval))); + $this->assertEquals(array($expected, $expected, $expected), + Ldap\Dn::unescapeValue(array($dnval, $dnval, $dnval)) + ); + } +} diff --git a/test/Dn/ExplodingTest.php b/test/Dn/ExplodingTest.php new file mode 100644 index 000000000..6746c742a --- /dev/null +++ b/test/Dn/ExplodingTest.php @@ -0,0 +1,252 @@ +assertTrue($ret === $expected); + } + + public function testExplodeDnCaseFold() + { + $dn = 'CN=Alice Baker,cn=Users,DC=example,dc=com'; + $k = array(); + $v = null; + $this->assertTrue(Ldap\Dn::checkDn($dn, $k, $v, Ldap\Dn::ATTR_CASEFOLD_NONE)); + $this->assertEquals(array('CN', 'cn', 'DC', 'dc'), $k); + + $this->assertTrue(Ldap\Dn::checkDn($dn, $k, $v, Ldap\Dn::ATTR_CASEFOLD_LOWER)); + $this->assertEquals(array('cn', 'cn', 'dc', 'dc'), $k); + + $this->assertTrue(Ldap\Dn::checkDn($dn, $k, $v, Ldap\Dn::ATTR_CASEFOLD_UPPER)); + $this->assertEquals(array('CN', 'CN', 'DC', 'DC'), $k); + } + + public function testExplodeDn() + { + $dn = 'cn=name1,cn=name2,dc=example,dc=org'; + $k = array(); + $v = array(); + $dnArray = Ldap\Dn::explodeDn($dn, $k, $v); + $expected = array( + array("cn" => "name1"), + array("cn" => "name2"), + array("dc" => "example"), + array("dc" => "org") + ); + $ke = array('cn', 'cn', 'dc', 'dc'); + $ve = array('name1', 'name2', 'example', 'org'); + $this->assertEquals($expected, $dnArray); + $this->assertEquals($ke, $k); + $this->assertEquals($ve, $v); + } + + public function testExplodeDnWithUtf8Characters() + { + $dn = 'uid=rogasawara,ou=営業部,o=Airius'; + $k = array(); + $v = array(); + $dnArray = Ldap\Dn::explodeDn($dn, $k, $v); + $expected = array( + array("uid" => "rogasawara"), + array("ou" => "営業部"), + array("o" => "Airius"), + ); + $ke = array('uid', 'ou', 'o'); + $ve = array('rogasawara', '営業部', 'Airius'); + $this->assertEquals($expected, $dnArray); + $this->assertEquals($ke, $k); + $this->assertEquals($ve, $v); + } + + public function testExplodeDnWithSpaces() + { + $dn = 'cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com'; + $k = array(); + $v = array(); + $dnArray = Ldap\Dn::explodeDn($dn, $k, $v); + $expected = array( + array("cn" => "Barbara Jensen"), + array("ou" => "Product Development"), + array("dc" => "airius"), + array("dc" => "com"), + ); + $ke = array('cn', 'ou', 'dc', 'dc'); + $ve = array('Barbara Jensen', 'Product Development', 'airius', 'com'); + $this->assertEquals($expected, $dnArray); + $this->assertEquals($ke, $k); + $this->assertEquals($ve, $v); + } + + public function testCoreExplodeDnWithMultiValuedRdn() + { + $dn = 'cn=name1+uid=user,cn=name2,dc=example,dc=org'; + $k = array(); + $v = array(); + $this->assertTrue(Ldap\Dn::checkDn($dn, $k, $v)); + $ke = array(array('cn', 'uid'), 'cn', 'dc', 'dc'); + $ve = array(array('name1', 'user'), 'name2', 'example', 'org'); + $this->assertEquals($ke, $k); + $this->assertEquals($ve, $v); + + $dn = 'cn=name11+cn=name12,cn=name2,dc=example,dc=org'; + $this->assertFalse(Ldap\Dn::checkDn($dn)); + + $dn = 'CN=name11+Cn=name12,cn=name2,dc=example,dc=org'; + $this->assertFalse(Ldap\Dn::checkDn($dn)); + } + + public function testExplodeDnWithMultiValuedRdn() + { + $dn = 'cn=Surname\, Firstname+uid=userid,cn=name2,dc=example,dc=org'; + $k = array(); + $v = array(); + $dnArray = Ldap\Dn::explodeDn($dn, $k, $v); + $ke = array(array('cn', 'uid'), 'cn', 'dc', 'dc'); + $ve = array(array('Surname, Firstname', 'userid'), 'name2', 'example', 'org'); + $this->assertEquals($ke, $k); + $this->assertEquals($ve, $v); + $expected = array( + array("cn" => "Surname, Firstname", + "uid" => "userid"), + array("cn" => "name2"), + array("dc" => "example"), + array("dc" => "org") + ); + $this->assertEquals($expected, $dnArray); + } + + public function testExplodeDnWithMultiValuedRdn2() + { + $dn = 'cn=Surname\, Firstname+uid=userid+sn=Surname,cn=name2,dc=example,dc=org'; + $k = array(); + $v = array(); + $dnArray = Ldap\Dn::explodeDn($dn, $k, $v); + $ke = array(array('cn', 'uid', 'sn'), 'cn', 'dc', 'dc'); + $ve = array(array('Surname, Firstname', 'userid', 'Surname'), 'name2', 'example', 'org'); + $this->assertEquals($ke, $k); + $this->assertEquals($ve, $v); + $expected = array( + array("cn" => "Surname, Firstname", + "uid" => "userid", + "sn" => "Surname"), + array("cn" => "name2"), + array("dc" => "example"), + array("dc" => "org") + ); + $this->assertEquals($expected, $dnArray); + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testCreateDnArrayIllegalDn() + { + $dn = 'name1,cn=name2,dc=example,dc=org'; + $dnArray = Ldap\Dn::explodeDn($dn); + } + + public static function rfc2253DnProvider() + { + $testData = array( + array('CN=Steve Kille,O=Isode Limited,C=GB', + array( + array('CN' => 'Steve Kille'), + array('O' => 'Isode Limited'), + array('C' => 'GB') + )), + array('OU=Sales+CN=J. Smith,O=Widget Inc.,C=US', + array( + array('OU' => 'Sales', + 'CN' => 'J. Smith'), + array('O' => 'Widget Inc.'), + array('C' => 'US') + )), + array('CN=L. Eagle,O=Sue\, Grabbit and Runn,C=GB', + array( + array('CN' => 'L. Eagle'), + array('O' => 'Sue, Grabbit and Runn'), + array('C' => 'GB') + )), + array('CN=Before\0DAfter,O=Test,C=GB', + array( + array('CN' => "Before\rAfter"), + array('O' => 'Test'), + array('C' => 'GB') + )), + array('SN=Lu\C4\8Di\C4\87', + array( + array('SN' => 'Lučić') + )) + ); + return $testData; + } + + /** + * @dataProvider rfc2253DnProvider + */ + public function testExplodeDnsProvidedByRFC2253($input, $expected) + { + $dnArray = Ldap\Dn::explodeDn($input); + $this->assertEquals($expected, $dnArray); + } +} diff --git a/test/Dn/ImplodingTest.php b/test/Dn/ImplodingTest.php new file mode 100644 index 000000000..e3339f766 --- /dev/null +++ b/test/Dn/ImplodingTest.php @@ -0,0 +1,133 @@ +assertEquals($dn1, $dn2); + } + + public function testImplodeDn() + { + $expected = 'cn=name1,cn=name2,dc=example,dc=org'; + $dnArray = array( + array("cn" => "name1"), + array("cn" => "name2"), + array("dc" => "example"), + array("dc" => "org") + ); + $dn = Ldap\Dn::implodeDn($dnArray); + $this->assertEquals($expected, $dn); + + $dn = Ldap\Dn::implodeDn($dnArray, Ldap\Dn::ATTR_CASEFOLD_UPPER, ';'); + $this->assertEquals('CN=name1;CN=name2;DC=example;DC=org', $dn); + } + + public function testImplodeDnWithUtf8Characters() + { + $expected = 'uid=rogasawara,ou=営業部,o=Airius'; + $dnArray = array( + array("uid" => "rogasawara"), + array("ou" => "営業部"), + array("o" => "Airius"), + ); + $dn = Ldap\Dn::implodeDn($dnArray); + $this->assertEquals($expected, $dn); + } + + public function testImplodeRdn() + { + $a = array('cn' => 'value'); + $expected = 'cn=value'; + $this->assertEquals($expected, Ldap\Dn::implodeRdn($a)); + } + + public function testImplodeRdnMultiValuedRdn() + { + $a = array('cn' => 'value', + 'uid' => 'testUser'); + $expected = 'cn=value+uid=testUser'; + $this->assertEquals($expected, Ldap\Dn::implodeRdn($a)); + } + + public function testImplodeRdnMultiValuedRdn2() + { + $a = array('cn' => 'value', + 'uid' => 'testUser', + 'ou' => 'myDep'); + $expected = 'cn=value+ou=myDep+uid=testUser'; + $this->assertEquals($expected, Ldap\Dn::implodeRdn($a)); + } + + public function testImplodeRdnCaseFold() + { + $a = array('cn' => 'value'); + $expected = 'CN=value'; + $this->assertEquals($expected, + Ldap\Dn::implodeRdn($a, Ldap\Dn::ATTR_CASEFOLD_UPPER) + ); + $a = array('CN' => 'value'); + $expected = 'cn=value'; + $this->assertEquals($expected, + Ldap\Dn::implodeRdn($a, Ldap\Dn::ATTR_CASEFOLD_LOWER) + ); + } + + public function testImplodeRdnMultiValuedRdnCaseFold() + { + $a = array('cn' => 'value', + 'uid' => 'testUser', + 'ou' => 'myDep'); + $expected = 'CN=value+OU=myDep+UID=testUser'; + $this->assertEquals($expected, + Ldap\Dn::implodeRdn($a, Ldap\Dn::ATTR_CASEFOLD_UPPER) + ); + $a = array('CN' => 'value', + 'uID' => 'testUser', + 'ou' => 'myDep'); + $expected = 'cn=value+ou=myDep+uid=testUser'; + $this->assertEquals($expected, + Ldap\Dn::implodeRdn($a, Ldap\Dn::ATTR_CASEFOLD_LOWER) + ); + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testImplodeRdnInvalidOne() + { + $a = array('cn'); + Ldap\Dn::implodeRdn($a); + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testImplodeRdnInvalidThree() + { + $a = array('cn' => 'value', 'ou'); + Ldap\Dn::implodeRdn($a); + } +} diff --git a/test/Dn/MiscTest.php b/test/Dn/MiscTest.php new file mode 100644 index 000000000..0d6ca8b67 --- /dev/null +++ b/test/Dn/MiscTest.php @@ -0,0 +1,72 @@ +assertFalse(Ldap\Dn::isChildOf($dn1, $dn2)); + } + + public function testIsChildOfIllegalDn2() + { + $dn1 = 'cn=name1,cn=name2,dc=example,dc=org'; + $dn2 = 'example,dc=org'; + $this->assertFalse(Ldap\Dn::isChildOf($dn1, $dn2)); + } + + public function testIsChildOfIllegalBothDn() + { + $dn1 = 'name1,cn=name2,dc=example,dc=org'; + $dn2 = 'example,dc=org'; + $this->assertFalse(Ldap\Dn::isChildOf($dn1, $dn2)); + } + + public function testIsChildOf() + { + $dn1 = 'cb=name1,cn=name2,dc=example,dc=org'; + $dn2 = 'dc=example,dc=org'; + $this->assertTrue(Ldap\Dn::isChildOf($dn1, $dn2)); + } + + public function testIsChildOfWithDnObjects() + { + $dn1 = Ldap\Dn::fromString('cb=name1,cn=name2,dc=example,dc=org'); + $dn2 = Ldap\Dn::fromString('dc=example,dc=org'); + $this->assertTrue(Ldap\Dn::isChildOf($dn1, $dn2)); + } + + public function testIsChildOfOtherSubtree() + { + $dn1 = 'cb=name1,cn=name2,dc=example,dc=org'; + $dn2 = 'dc=example,dc=de'; + $this->assertFalse(Ldap\Dn::isChildOf($dn1, $dn2)); + } + + public function testIsChildOfParentDnLonger() + { + $dn1 = 'dc=example,dc=de'; + $dn2 = 'cb=name1,cn=name2,dc=example,dc=org'; + $this->assertFalse(Ldap\Dn::isChildOf($dn1, $dn2)); + } +} diff --git a/test/Dn/ModificationTest.php b/test/Dn/ModificationTest.php new file mode 100644 index 000000000..7e18a398a --- /dev/null +++ b/test/Dn/ModificationTest.php @@ -0,0 +1,316 @@ +assertEquals(array('cn' => 'Baker, Alice'), $dn->get(0)); + $this->assertEquals(array('cn' => 'Users', + 'ou' => 'Lab'), $dn->get(1) + ); + $this->assertEquals(array('dc' => 'example'), $dn->get(2)); + $this->assertEquals(array('dc' => 'com'), $dn->get(3)); + try { + $this->assertEquals(array('dc' => 'com'), $dn->get('string')); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('Parameter $index must be an integer', $e->getMessage()); + } + try { + $this->assertEquals(array('cn' => 'Baker, Alice'), $dn->get(-1)); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('Parameter $index out of bounds', $e->getMessage()); + } + try { + $this->assertEquals(array('dc' => 'com'), $dn->get(4)); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('Parameter $index out of bounds', $e->getMessage()); + } + + $this->assertEquals(array( + array('cn' => 'Baker, Alice'), + array('cn' => 'Users', + 'ou' => 'Lab') + ), $dn->get(0, 2) + ); + $this->assertEquals(array( + array('cn' => 'Baker, Alice'), + array('cn' => 'Users', + 'ou' => 'Lab'), + array('dc' => 'example') + ), $dn->get(0, 3) + ); + $this->assertEquals(array( + array('cn' => 'Baker, Alice'), + array('cn' => 'Users', + 'ou' => 'Lab'), + array('dc' => 'example'), + array('dc' => 'com') + ), $dn->get(0, 4) + ); + $this->assertEquals(array( + array('cn' => 'Baker, Alice'), + array('cn' => 'Users', + 'ou' => 'Lab'), + array('dc' => 'example'), + array('dc' => 'com') + ), $dn->get(0, 5) + ); + + $this->assertEquals(array( + array('cn' => 'Users', + 'ou' => 'Lab'), + array('dc' => 'example') + ), $dn->get(1, 2) + ); + $this->assertEquals(array( + array('cn' => 'Users', + 'ou' => 'Lab'), + array('dc' => 'example'), + array('dc' => 'com') + ), $dn->get(1, 3) + ); + $this->assertEquals(array( + array('cn' => 'Users', + 'ou' => 'Lab'), + array('dc' => 'example'), + array('dc' => 'com') + ), $dn->get(1, 4) + ); + + $this->assertEquals(array( + array('dc' => 'example'), + array('dc' => 'com') + ), $dn->get(2, 2) + ); + $this->assertEquals(array( + array('dc' => 'example'), + array('dc' => 'com') + ), $dn->get(2, 3) + ); + + $this->assertEquals(array( + array('dc' => 'com') + ), $dn->get(3, 2) + ); + } + + public function testDnManipulationSet() + { + $dnString = 'cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example,dc=com'; + $dn = Ldap\Dn::fromString($dnString); + + $this->assertEquals('uid=abaker,cn=Users+ou=Lab,dc=example,dc=com', + $dn->set(0, array('uid' => 'abaker'))->toString() + ); + $this->assertEquals('uid=abaker,ou=Lab,dc=example,dc=com', + $dn->set(1, array('ou' => 'Lab'))->toString() + ); + $this->assertEquals('uid=abaker,ou=Lab,dc=example+ou=Test,dc=com', + $dn->set(2, array('dc' => 'example', + 'ou' => 'Test') + )->toString() + ); + $this->assertEquals('uid=abaker,ou=Lab,dc=example+ou=Test,dc=de\+fr', + $dn->set(3, array('dc' => 'de+fr'))->toString() + ); + + try { + $dn->set(4, array('dc' => 'de')); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('Parameter $index out of bounds', $e->getMessage()); + } + try { + $dn->set(3, array('dc' => 'de', 'ou')); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('RDN Array is malformed: it must use string keys', $e->getMessage()); + } + } + + public function testDnManipulationRemove() + { + $dnString = 'cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example,dc=com'; + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Users+ou=Lab,dc=example,dc=com', $dn->remove(0)->toString()); + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Baker\\, Alice,dc=example,dc=com', $dn->remove(1)->toString()); + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Baker\\, Alice,cn=Users+ou=Lab,dc=com', $dn->remove(2)->toString()); + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example', + $dn->remove(3)->toString() + ); + + try { + $dn = Ldap\Dn::fromString($dnString); + $dn->remove(4); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('Parameter $index out of bounds', $e->getMessage()); + } + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Baker\\, Alice,dc=com', + $dn->remove(1, 2)->toString() + ); + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Baker\\, Alice', + $dn->remove(1, 3)->toString() + ); + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Baker\\, Alice', + $dn->remove(1, 4)->toString() + ); + } + + public function testDnManipulationAppendAndPrepend() + { + $dnString = 'OU=Sales,DC=example'; + $dn = Ldap\Dn::fromString($dnString); + + $this->assertEquals('OU=Sales,DC=example,DC=com', + $dn->append(array('DC' => 'com'))->toString() + ); + + $this->assertEquals('OU=New York,OU=Sales,DC=example,DC=com', + $dn->prepend(array('OU' => 'New York'))->toString() + ); + + try { + $dn->append(array('dc' => 'de', 'ou')); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('RDN Array is malformed: it must use string keys', $e->getMessage()); + } + try { + $dn->prepend(array('dc' => 'de', 'ou')); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('RDN Array is malformed: it must use string keys', $e->getMessage()); + } + } + + public function testDnManipulationInsert() + { + $dnString = 'cn=Baker\\, Alice,cn=Users,dc=example,dc=com'; + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Baker\\, Alice,dc=test,cn=Users,dc=example,dc=com', + $dn->insert(0, array('dc' => 'test'))->toString() + ); + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=test,dc=example,dc=com', + $dn->insert(1, array('dc' => 'test'))->toString() + ); + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=example,dc=test,dc=com', + $dn->insert(2, array('dc' => 'test'))->toString() + ); + + $dn = Ldap\Dn::fromString($dnString); + $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=example,dc=com,dc=test', + $dn->insert(3, array('dc' => 'test'))->toString() + ); + + try { + $dn = Ldap\Dn::fromString($dnString); + $dn->insert(4, array('dc' => 'de')); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('Parameter $index out of bounds', $e->getMessage()); + } + try { + $dn = Ldap\Dn::fromString($dnString); + $dn->insert(3, array('dc' => 'de', 'ou')); + $this->fail('Expected Zend\Ldap\Exception not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals('RDN Array is malformed: it must use string keys', $e->getMessage()); + } + } + + public function testArrayAccessImplementation() + { + $dnString = 'cn=Baker\\, Alice,cn=Users,dc=example,dc=com'; + $dn = Ldap\Dn::fromString($dnString); + + $this->assertEquals(array('cn' => 'Baker, Alice'), $dn[0]); + $this->assertEquals(array('cn' => 'Users'), $dn[1]); + $this->assertEquals(array('dc' => 'example'), $dn[2]); + $this->assertEquals(array('dc' => 'com'), $dn[3]); + + $this->assertTrue(isset($dn[0])); + $this->assertTrue(isset($dn[1])); + $this->assertTrue(isset($dn[2])); + $this->assertTrue(isset($dn[3])); + $this->assertFalse(isset($dn[-1])); + $this->assertFalse(isset($dn[4])); + + $dn = Ldap\Dn::fromString($dnString); + unset($dn[0]); + $this->assertEquals('cn=Users,dc=example,dc=com', $dn->toString()); + + $dn = Ldap\Dn::fromString($dnString); + unset($dn[1]); + $this->assertEquals('cn=Baker\\, Alice,dc=example,dc=com', $dn->toString()); + + $dn = Ldap\Dn::fromString($dnString); + unset($dn[2]); + $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=com', $dn->toString()); + + $dn = Ldap\Dn::fromString($dnString); + unset($dn[3]); + $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=example', $dn->toString()); + + $dn = Ldap\Dn::fromString($dnString); + $dn[0] = array('uid' => 'abaker'); + $this->assertEquals('uid=abaker,cn=Users,dc=example,dc=com', $dn->toString()); + + $dn = Ldap\Dn::fromString($dnString); + $dn[1] = array('ou' => 'Lab'); + $this->assertEquals('cn=Baker\\, Alice,ou=Lab,dc=example,dc=com', $dn->toString()); + + $dn = Ldap\Dn::fromString($dnString); + $dn[2] = array('dc' => 'example', + 'ou' => 'Test'); + $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=example+ou=Test,dc=com', $dn->toString()); + + $dn = Ldap\Dn::fromString($dnString); + $dn[3] = array('dc' => 'de+fr'); + $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=example,dc=de\+fr', $dn->toString()); + } +} diff --git a/test/FilterTest.php b/test/FilterTest.php new file mode 100644 index 000000000..ce2220ab6 --- /dev/null +++ b/test/FilterTest.php @@ -0,0 +1,200 @@ +assertEquals($expected, Ldap\Filter::escapeValue($input)); + } + + public function testEscapeValues() + { + $expected = 't\28e,s\29t\2av\5cal\1eue'; + $filterval = 't(e,s)t*v\\al' . chr(30) . 'ue'; + $this->assertEquals($expected, Ldap\Filter::escapeValue($filterval)); + $this->assertEquals($expected, Ldap\Filter::escapeValue(array($filterval))); + $this->assertEquals( + array($expected, $expected, $expected), + Ldap\Filter::escapeValue(array($filterval, $filterval, $filterval)) + ); + } + + public function testUnescapeValues() + { + $expected = 't(e,s)t*v\\al' . chr(30) . 'ue'; + $filterval = 't\28e,s\29t\2av\5cal\1eue'; + $this->assertEquals($expected, Ldap\Filter::unescapeValue($filterval)); + $this->assertEquals($expected, Ldap\Filter::unescapeValue(array($filterval))); + $this->assertEquals( + array($expected, $expected, $expected), + Ldap\Filter::unescapeValue(array($filterval, $filterval, $filterval)) + ); + } + + public function testFilterValueUtf8() + { + $filter = 'ÄÖÜäöü߀'; + $escaped = Ldap\Filter::escapeValue($filter); + $unescaped = Ldap\Filter::unescapeValue($escaped); + $this->assertEquals($filter, $unescaped); + } + + public function testFilterCreation() + { + $f1 = Ldap\Filter::equals('name', 'value'); + $this->assertEquals('(name=value)', $f1->toString()); + $f2 = Ldap\Filter::begins('name', 'value'); + $this->assertEquals('(name=value*)', $f2->toString()); + $f3 = Ldap\Filter::ends('name', 'value'); + $this->assertEquals('(name=*value)', $f3->toString()); + $f4 = Ldap\Filter::contains('name', 'value'); + $this->assertEquals('(name=*value*)', $f4->toString()); + $f5 = Ldap\Filter::greater('name', 'value'); + $this->assertEquals('(name>value)', $f5->toString()); + $f6 = Ldap\Filter::greaterOrEqual('name', 'value'); + $this->assertEquals('(name>=value)', $f6->toString()); + $f7 = Ldap\Filter::less('name', 'value'); + $this->assertEquals('(nametoString()); + $f8 = Ldap\Filter::lessOrEqual('name', 'value'); + $this->assertEquals('(name<=value)', $f8->toString()); + $f9 = Ldap\Filter::approx('name', 'value'); + $this->assertEquals('(name~=value)', $f9->toString()); + $f10 = Ldap\Filter::any('name'); + $this->assertEquals('(name=*)', $f10->toString()); + $f11 = Ldap\Filter::string('name=*value*value*'); + $this->assertEquals('(name=*value*value*)', $f11->toString()); + $f12 = Ldap\Filter::mask('(&(objectClass=account)(uid=%s))', 'a*b(b)d\e/f'); + $this->assertEquals('(&(objectClass=account)(uid=a\2ab\28b\29d\5ce/f))', $f12->toString()); + } + + public function testToStringImplementation() + { + $f1 = Ldap\Filter::ends('name', 'value'); + $this->assertEquals($f1->toString(), (string)$f1); + } + + public function testNegate() + { + $f1 = Ldap\Filter::ends('name', 'value'); + $this->assertEquals('(name=*value)', $f1->toString()); + $f1 = $f1->negate(); + $this->assertEquals('(!(name=*value))', $f1->toString()); + $f1 = $f1->negate(); + $this->assertEquals('(name=*value)', $f1->toString()); + } + + /** + * @expectedException Zend\Ldap\Filter\Exception\FilterException + */ + public function testIllegalGroupingFilter() + { + $data = array('a', 'b', 5); + $f = new Filter\AndFilter($data); + } + + public function testGroupingFilter() + { + $f1 = Ldap\Filter::equals('name', 'value'); + $f2 = Ldap\Filter::begins('name', 'value'); + $f3 = Ldap\Filter::ends('name', 'value'); + + $f4 = Ldap\Filter::andFilter($f1, $f2, $f3); + $f5 = Ldap\Filter::orFilter($f1, $f2, $f3); + + $this->assertEquals('(&(name=value)(name=value*)(name=*value))', $f4->toString()); + $this->assertEquals('(|(name=value)(name=value*)(name=*value))', $f5->toString()); + + $f4 = $f4->addFilter($f1); + $this->assertEquals('(&(name=value)(name=value*)(name=*value)(name=value))', $f4->toString()); + } + + public function testComplexFilter() + { + $f1 = Ldap\Filter::equals('name1', 'value1'); + $f2 = Ldap\Filter::equals('name1', 'value2'); + + $f3 = Ldap\Filter::equals('name2', 'value1'); + $f4 = Ldap\Filter::equals('name2', 'value2'); + + $f5 = Ldap\Filter::orFilter($f1, $f2); + $f6 = Ldap\Filter::orFilter($f3, $f4); + + $f7 = Ldap\Filter::andFilter($f5, $f6); + + $this->assertEquals( + '(&(|(name1=value1)(name1=value2))(|(name2=value1)(name2=value2)))', + $f7->toString() + ); + } + + public function testChaining() + { + $f = Ldap\Filter::equals('a1', 'v1') + ->addAnd(Ldap\Filter::approx('a2', 'v2')); + $this->assertEquals('(&(a1=v1)(a2~=v2))', $f->toString()); + $f = Ldap\Filter::equals('a1', 'v1') + ->addOr(Ldap\Filter::approx('a2', 'v2')); + $this->assertEquals('(|(a1=v1)(a2~=v2))', $f->toString()); + $f = Ldap\Filter::equals('a1', 'v1') + ->negate() + ->addOr(Ldap\Filter::approx('a2', 'v2')); + $this->assertEquals('(|(!(a1=v1))(a2~=v2))', $f->toString()); + $f = Ldap\Filter::equals('a1', 'v1') + ->addAnd(Ldap\Filter::approx('a2', 'v2')->negate()); + $this->assertEquals('(&(a1=v1)(!(a2~=v2)))', $f->toString()); + $f = Ldap\Filter::equals('a1', 'v1') + ->negate() + ->addAnd(Ldap\Filter::approx('a2', 'v2')->negate()); + $this->assertEquals('(&(!(a1=v1))(!(a2~=v2)))', $f->toString()); + $f = Ldap\Filter::equals('a1', 'v1') + ->negate() + ->addAnd(Ldap\Filter::approx('a2', 'v2')->negate()); + $this->assertEquals('(&(!(a1=v1))(!(a2~=v2)))', $f->toString()); + $f = Ldap\Filter::equals('a1', 'v1') + ->negate() + ->addAnd(Ldap\Filter::approx('a2', 'v2')->negate()) + ->negate(); + $this->assertEquals('(!(&(!(a1=v1))(!(a2~=v2))))', $f->toString()); + } + + public function testRealFilterString() + { + $f1 = Ldap\Filter::orFilter( + Ldap\Filter::equals('sn', 'Gehrig'), + Ldap\Filter::equals('sn', 'Goerke') + ); + $f2 = Ldap\Filter::orFilter( + Ldap\Filter::equals('givenName', 'Stefan'), + Ldap\Filter::equals('givenName', 'Ingo') + ); + + $f = Ldap\Filter::andFilter($f1, $f2); + + $this->assertEquals( + '(&(|(sn=Gehrig)(sn=Goerke))(|(givenName=Stefan)(givenName=Ingo)))', + $f->toString() + ); + } +} + diff --git a/test/Ldif/SimpleDecoderTest.php b/test/Ldif/SimpleDecoderTest.php new file mode 100644 index 000000000..9773b2697 --- /dev/null +++ b/test/Ldif/SimpleDecoderTest.php @@ -0,0 +1,373 @@ + 'cn=test3,ou=example,dc=cno', + 'objectclass' => array('oc1'), + 'attr3' => array('foo')); + $actual = Ldif\Encoder::decode($data); + $this->assertEquals($expected, $actual); + } + + public function testDecodeSingleItemWithFoldedAttribute() + { + $data = +"dn: cn=test blabla,ou=example,dc=cno +objectclass: oc2 +attr1: 12345 +attr2: 1234 +attr2: baz +attr3: foo +attr3: bar +cn: test blabla +verylong: fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8 + h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb + 5789thvngwr789cghm738"; + $expected = array( + 'dn' => 'cn=test blabla,ou=example,dc=cno', + 'objectclass' => array('oc2'), + 'attr1' => array('12345'), + 'attr2' => array('1234', 'baz'), + 'attr3' => array('foo', 'bar'), + 'cn' => array('test blabla'), + 'verylong' => array('fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8' + . 'h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb' + . '5789thvngwr789cghm738'), + ); + $actual = Ldif\Encoder::decode($data); + $this->assertEquals($expected, $actual); + } + + public function testDecodeSingleItemWithBase64Attributes() + { + $data = +"dn:: Y249dGVzdCBibGFibGEsb3U9ZXhhbXBsZSxkYz1jbm8= +objectclass: oc3 +attr1: 12345 +attr2: 1234 +attr2: baz +attr3: foo +attr3: bar +attr4:: w7bDpMO8 +attr5:: ZW5kc3BhY2Ug +attr6:: OmJhZGluaXRjaGFy +attr6:: PGJhZGluaXRjaGFy +cn:: dGVzdCDDtsOkw7w="; + $expected = array( + 'dn' => 'cn=test blabla,ou=example,dc=cno', + 'objectclass' => array('oc3'), + 'attr1' => array('12345'), + 'attr2' => array('1234', 'baz'), + 'attr3' => array('foo', 'bar'), + 'attr4' => array('öäü'), + 'attr5' => array('endspace '), + 'attr6' => array(':badinitchar', ' array('test öäü'), + ); + $actual = Ldif\Encoder::decode($data); + $this->assertEquals($expected, $actual); + } + + public function testDecodeSingleItemWithFoldedBase64Attribute() + { + $data = +"dn:: Y249dGVzdCBibGFibGEsb + 3U9ZXhhbXBsZSxkYz1jbm8= +objectclass: oc3 +attr1: 12345 +attr2: 1234 +attr2: baz +attr3: foo +attr3: bar"; + $expected = array( + 'dn' => 'cn=test blabla,ou=example,dc=cno', + 'objectclass' => array('oc3'), + 'attr1' => array('12345'), + 'attr2' => array('1234', 'baz'), + 'attr3' => array('foo', 'bar'), + ); + $actual = Ldif\Encoder::decode($data); + $this->assertEquals($expected, $actual); + } + + public function testDecodeTwoItems() + { + $data = +"version: 1 +dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Barbara Jensen +cn: Barbara J Jensen +cn: Babs Jensen +sn: Jensen +uid: bjensen +telephonenumber: +1 408 555 1212 +description: A big sailing fan. + +dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Bjorn Jensen +sn: Jensen +telephonenumber: +1 408 555 1212"; + $expected = array( + array( + 'dn' => 'cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com', + 'objectclass' => array('top', 'person', 'organizationalPerson'), + 'cn' => array('Barbara Jensen', 'Barbara J Jensen', 'Babs Jensen'), + 'sn' => array('Jensen'), + 'uid' => array('bjensen'), + 'telephonenumber' => array('+1 408 555 1212'), + 'description' => array('A big sailing fan.'), + ), + array( + 'dn' => 'cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com', + 'objectclass' => array('top', 'person', 'organizationalPerson'), + 'cn' => array('Bjorn Jensen'), + 'sn' => array('Jensen'), + 'telephonenumber' => array('+1 408 555 1212'), + ), + ); + $actual = Ldif\Encoder::decode($data); + $this->assertEquals($expected, $actual); + } + + public function testDecodeStringContainingEntryWithFoldedAttributeValue() + { + $data = +"version: 1 +dn:cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com +objectclass:top +objectclass:person +objectclass:organizationalPerson +cn:Barbara Jensen +cn:Barbara J Jensen +cn:Babs Jensen +sn:Jensen +uid:bjensen +telephonenumber:+1 408 555 1212 +description:Babs is a big sailing fan, and travels extensively in sea + rch of perfect sailing conditions. +title:Product Manager, Rod and Reel Division"; + $expected = array( + 'dn' => 'cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com', + 'objectclass' => array('top', 'person', 'organizationalPerson'), + 'cn' => array('Barbara Jensen', 'Barbara J Jensen', 'Babs Jensen'), + 'sn' => array('Jensen'), + 'uid' => array('bjensen'), + 'telephonenumber' => array('+1 408 555 1212'), + 'description' => array('Babs is a big sailing fan, and travels extensively' + . ' in search of perfect sailing conditions.'), + 'title' => array('Product Manager, Rod and Reel Division'), + ); + $actual = Ldif\Encoder::decode($data); + $this->assertEquals($expected, $actual); + } + + public function testDecodeStringContainingBase64EncodedValue() + { + $data = +"version: 1 +dn: cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Gern Jensen +cn: Gern O Jensen +sn: Jensen +uid: gernj +telephonenumber: +1 408 555 1212 +description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl + IGlzIGJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG + VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg + b3V0IG1vcmUu"; + $expected = array( + 'dn' => 'cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com', + 'objectclass' => array('top', 'person', 'organizationalPerson'), + 'cn' => array('Gern Jensen', 'Gern O Jensen'), + 'sn' => array('Jensen'), + 'uid' => array('gernj'), + 'telephonenumber' => array('+1 408 555 1212'), + 'description' => array('What a careful reader you are!' + . ' This value is base-64-encoded because it has a ' + . 'control character in it (a CR).' . "\r" + . ' By the way, you should really get out more.'), + ); + $actual = Ldif\Encoder::decode($data); + $this->assertEquals($expected, $actual); + } + + public function testDecodeStringContainingEntriesWithUtf8EncodedAttributeValues() + { + $data = +"version: 1 +dn:: b3U95Za25qWt6YOoLG89QWlyaXVz +# dn:: ou=営業部,o=Airius +objectclass: top +objectclass: organizationalUnit +ou:: 5Za25qWt6YOo +# ou:: 営業部 +ou;lang-ja:: 5Za25qWt6YOo +# ou;lang-ja:: 営業部 +ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2 +# ou;lang-ja:: えいぎょうぶ + +ou;lang-en: Sales +description: Japanese office + +dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz +# dn:: uid=rogasawara,ou=営業部,o=Airius +userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM= +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +uid: rogasawara +mail: rogasawara@airius.co.jp +givenname;lang-ja:: 44Ot44OJ44OL44O8 +# givenname;lang-ja:: ロドニー +sn;lang-ja:: 5bCP56yg5Y6f +# sn;lang-ja:: 小笠原 +cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA== +# cn;lang-ja:: 小笠原 ロドニー +title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw== +# title;lang-ja:: 営業部 部長 +preferredlanguage: ja +givenname:: 44Ot44OJ44OL44O8 +# givenname:: ロドニー +sn:: 5bCP56yg5Y6f +# sn:: 小笠原 +cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA== +# cn:: 小笠原 ロドニー +title:: 5Za25qWt6YOoIOmDqOmVtw== +# title:: 営業部 部長 +givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8 +# givenname;lang-ja;phonetic:: ろどにー +sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ +# sn;lang-ja;phonetic:: おがさわら +cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA== +# cn;lang-ja;phonetic:: おがさわら ろどにー +title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg== +# title;lang-ja;phonetic:: えいぎょうぶ ぶちょう +givenname;lang-en: Rodney +sn;lang-en: Ogasawara +cn;lang-en: Rodney Ogasawara +title;lang-en: Sales, Director"; + + $actual = Ldif\Encoder::decode($data); + + $this->assertEquals('ou=営業部,o=Airius', $actual[0]['dn']); + $this->assertEquals(array('top', 'organizationalUnit'), $actual[0]['objectclass']); + $this->assertEquals('営業部', $actual[0]['ou'][0]); + $this->assertEquals('営業部', $actual[0]['ou;lang-ja'][0]); + $this->assertEquals('えいぎょうぶ', $actual[0]['ou;lang-ja;phonetic'][0]); + $this->assertEquals('Sales', $actual[0]['ou;lang-en'][0]); + $this->assertEquals('Japanese office', $actual[0]['description'][0]); + + $this->assertEquals('uid=rogasawara,ou=営業部,o=Airius', $actual[1]['dn']); + $this->assertEquals('{SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM=', $actual[1]['userpassword'][0]); + $this->assertEquals(array('top', 'person', 'organizationalPerson', 'inetOrgPerson'), + $actual[1]['objectclass'] + ); + $this->assertEquals('rogasawara', $actual[1]['uid'][0]); + $this->assertEquals('rogasawara@airius.co.jp', $actual[1]['mail'][0]); + $this->assertEquals('ロドニー', $actual[1]['givenname;lang-ja'][0]); + $this->assertEquals('小笠原', $actual[1]['sn;lang-ja'][0]); + $this->assertEquals('小笠原 ロドニー', $actual[1]['cn;lang-ja'][0]); + $this->assertEquals('営業部 部長', $actual[1]['title;lang-ja'][0]); + $this->assertEquals('ja', $actual[1]['preferredlanguage'][0]); + $this->assertEquals('ロドニー', $actual[1]['givenname'][0]); + $this->assertEquals('小笠原', $actual[1]['sn'][0]); + $this->assertEquals('小笠原 ロドニー', $actual[1]['cn'][0]); + $this->assertEquals('営業部 部長', $actual[1]['title'][0]); + $this->assertEquals('ろどにー', $actual[1]['givenname;lang-ja;phonetic'][0]); + $this->assertEquals('おがさわら', $actual[1]['sn;lang-ja;phonetic'][0]); + $this->assertEquals('おがさわら ろどにー', $actual[1]['cn;lang-ja;phonetic'][0]); + $this->assertEquals('えいぎょうぶ ぶちょう', $actual[1]['title;lang-ja;phonetic'][0]); + $this->assertEquals('Rodney', $actual[1]['givenname;lang-en'][0]); + $this->assertEquals('Ogasawara', $actual[1]['sn;lang-en'][0]); + $this->assertEquals('Rodney Ogasawara', $actual[1]['cn;lang-en'][0]); + $this->assertEquals('Sales, Director', $actual[1]['title;lang-en'][0]); + } + + public function testDecodeSingleItemWithFoldedAttributesAndEmptyLinesBetween() + { + $data = +"dn: cn=test blabla,ou=example,dc=cno + +objectclass: top + + +objectclass: person + +objectclass: organizationalPerson + +description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl + + IGlzIGJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG + + VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg + + b3V0IG1vcmUu + + +verylong: fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8 + + h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb + + 5789thvngwr789cghm738"; + $expected = array( + 'dn' => 'cn=test blabla,ou=example,dc=cno', + 'objectclass' => array('top', 'person', 'organizationalPerson'), + 'description' => array('What a careful reader you are!' + . ' This value is base-64-encoded because it has a ' + . 'control character in it (a CR).' . "\r" + . ' By the way, you should really get out more.'), + 'verylong' => array('fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8' + . 'h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb' + . '5789thvngwr789cghm738'), + ); + $actual = Ldif\Encoder::decode($data); + $this->assertEquals($expected, $actual); + } + + public function testRoundtripEncoding() + { + $node = $this->createTestNode(); + $ldif = $node->toLdif(); + $data = Ldif\Encoder::decode($ldif); + $expected = array_merge(array('dn' => $node->getDnString()), $node->getData(false)); + $this->assertEquals($expected, $data); + } +} diff --git a/test/Ldif/SimpleEncoderTest.php b/test/Ldif/SimpleEncoderTest.php new file mode 100644 index 000000000..b9bcf061c --- /dev/null +++ b/test/Ldif/SimpleEncoderTest.php @@ -0,0 +1,255 @@ +assertEquals($expected, Ldif\Encoder::encode($string)); + } + + public static function attributeEncodingProvider() + { + $testData = array( + array(array('dn' => 'cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com'), + 'dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com'), + array(array('dn' => 'cn=Jürgen Österreicher, ou=Äpfel, dc=airius, dc=com'), + 'dn:: ' . base64_encode('cn=Jürgen Österreicher, ou=Äpfel, dc=airius, dc=com')), + array(array('description' => 'Babs is a big sailing fan, and travels extensively in search of perfect sailing conditions.'), + 'description: Babs is a big sailing fan, and travels extensively in search of p' + . PHP_EOL . ' erfect sailing conditions.'), + array(array('description' => "CHR(127) \x7f in string"), + 'description:: ' . base64_encode("CHR(127) \x7f in string")), + array(array('description' => '1234567890123456789012345678901234567890123456789012345678901234 567890'), + 'description: 1234567890123456789012345678901234567890123456789012345678901234 ' . PHP_EOL + . ' 567890'), + ); + return $testData; + } + + /** + * @dataProvider attributeEncodingProvider + */ + public function testAttributeEncoding($array, $expected) + { + $actual = Ldif\Encoder::encode($array); + $this->assertEquals($expected, $actual); + } + + public function testChangedWrapCount() + { + $input = '56789012345678901234567890'; + $expected = 'dn: 567890' . PHP_EOL . ' 1234567890' . PHP_EOL . ' 1234567890'; + $output = Ldif\Encoder::encode(array('dn' => $input), array('wrap' => 10)); + $this->assertEquals($expected, $output); + } + + public function testEncodeMultipleAttributes() + { + $data = array( + 'a' => array('a', 'b'), + 'b' => 'c', + 'c' => '', + 'd' => array(), + 'e' => array('')); + $expected = 'a: a' . PHP_EOL . + 'a: b' . PHP_EOL . + 'b: c' . PHP_EOL . + 'c: ' . PHP_EOL . + 'd: ' . PHP_EOL . + 'e: '; + $actual = Ldif\Encoder::encode($data); + $this->assertEquals($expected, $actual); + } + + public function testEncodeUnsupportedType() + { + $this->assertNull(Ldif\Encoder::encode(new \stdClass())); + } + + public function testSorting() + { + $data = array( + 'cn' => array('name'), + 'dn' => 'cn=name,dc=example,dc=org', + 'host' => array('a', 'b', 'c'), + 'empty' => array(), + 'boolean' => array('TRUE', 'FALSE'), + 'objectclass' => array('account', 'top'), + ); + $expected = 'version: 1' . PHP_EOL . + 'dn: cn=name,dc=example,dc=org' . PHP_EOL . + 'objectclass: account' . PHP_EOL . + 'objectclass: top' . PHP_EOL . + 'boolean: TRUE' . PHP_EOL . + 'boolean: FALSE' . PHP_EOL . + 'cn: name' . PHP_EOL . + 'empty: ' . PHP_EOL . + 'host: a' . PHP_EOL . + 'host: b' . PHP_EOL . + 'host: c'; + $actual = Ldif\Encoder::encode($data); + $this->assertEquals($expected, $actual); + + $expected = 'version: 1' . PHP_EOL . + 'cn: name' . PHP_EOL . + 'dn: cn=name,dc=example,dc=org' . PHP_EOL . + 'host: a' . PHP_EOL . + 'host: b' . PHP_EOL . + 'host: c' . PHP_EOL . + 'empty: ' . PHP_EOL . + 'boolean: TRUE' . PHP_EOL . + 'boolean: FALSE' . PHP_EOL . + 'objectclass: account' . PHP_EOL . + 'objectclass: top'; + $actual = Ldif\Encoder::encode($data, array('sort' => false)); + $this->assertEquals($expected, $actual); + } + + public function testNodeEncoding() + { + $node = $this->createTestNode(); + $expected = 'version: 1' . PHP_EOL . + 'dn: cn=name,dc=example,dc=org' . PHP_EOL . + 'objectclass: account' . PHP_EOL . + 'objectclass: top' . PHP_EOL . + 'boolean: TRUE' . PHP_EOL . + 'boolean: FALSE' . PHP_EOL . + 'cn: name' . PHP_EOL . + 'empty: ' . PHP_EOL . + 'host: a' . PHP_EOL . + 'host: b' . PHP_EOL . + 'host: c'; + $actual = $node->toLdif(); + $this->assertEquals($expected, $actual); + + $actual = Ldif\Encoder::encode($node); + $this->assertEquals($expected, $actual); + } + + public function testSupressVersionHeader() + { + $data = array( + 'cn' => array('name'), + 'dn' => 'cn=name,dc=example,dc=org', + 'host' => array('a', 'b', 'c'), + 'empty' => array(), + 'boolean' => array('TRUE', 'FALSE'), + 'objectclass' => array('account', 'top'), + ); + $expected = 'dn: cn=name,dc=example,dc=org' . PHP_EOL . + 'objectclass: account' . PHP_EOL . + 'objectclass: top' . PHP_EOL . + 'boolean: TRUE' . PHP_EOL . + 'boolean: FALSE' . PHP_EOL . + 'cn: name' . PHP_EOL . + 'empty: ' . PHP_EOL . + 'host: a' . PHP_EOL . + 'host: b' . PHP_EOL . + 'host: c'; + $actual = Ldif\Encoder::encode($data, array('version' => null)); + $this->assertEquals($expected, $actual); + } + + public function testEncodingWithJapaneseCharacters() + { + $data = array( + 'dn' => 'uid=rogasawara,ou=営業部,o=Airius', + 'objectclass' => array('top', 'person', 'organizationalPerson', 'inetOrgPerson'), + 'uid' => array('rogasawara'), + 'mail' => array('rogasawara@airius.co.jp'), + 'givenname;lang-ja' => array('ロドニー'), + 'sn;lang-ja' => array('小笠原'), + 'cn;lang-ja' => array('小笠原 ロドニー'), + 'title;lang-ja' => array('営業部 部長'), + 'preferredlanguage' => array('ja'), + 'givenname' => array('ロドニー'), + 'sn' => array('小笠原'), + 'cn' => array('小笠原 ロドニー'), + 'title' => array('営業部 部長'), + 'givenname;lang-ja;phonetic' => array('ろどにー'), + 'sn;lang-ja;phonetic' => array('おがさわら'), + 'cn;lang-ja;phonetic' => array('おがさわら ろどにー'), + 'title;lang-ja;phonetic' => array('えいぎょうぶ ぶちょう'), + 'givenname;lang-en' => array('Rodney'), + 'sn;lang-en' => array('Ogasawara'), + 'cn;lang-en' => array('Rodney Ogasawara'), + 'title;lang-en' => array('Sales, Director'), + ); + $expected = 'dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz' . PHP_EOL . + 'objectclass: top' . PHP_EOL . + 'objectclass: person' . PHP_EOL . + 'objectclass: organizationalPerson' . PHP_EOL . + 'objectclass: inetOrgPerson' . PHP_EOL . + 'uid: rogasawara' . PHP_EOL . + 'mail: rogasawara@airius.co.jp' . PHP_EOL . + 'givenname;lang-ja:: 44Ot44OJ44OL44O8' . PHP_EOL . + 'sn;lang-ja:: 5bCP56yg5Y6f' . PHP_EOL . + 'cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==' . PHP_EOL . + 'title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw==' . PHP_EOL . + 'preferredlanguage: ja' . PHP_EOL . + 'givenname:: 44Ot44OJ44OL44O8' . PHP_EOL . + 'sn:: 5bCP56yg5Y6f' . PHP_EOL . + 'cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA==' . PHP_EOL . + 'title:: 5Za25qWt6YOoIOmDqOmVtw==' . PHP_EOL . + 'givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8' . PHP_EOL . + 'sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ' . PHP_EOL . + 'cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA==' . PHP_EOL . + 'title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg==' . PHP_EOL . + 'givenname;lang-en: Rodney' . PHP_EOL . + 'sn;lang-en: Ogasawara' . PHP_EOL . + 'cn;lang-en: Rodney Ogasawara' . PHP_EOL . + 'title;lang-en: Sales, Director'; + $actual = Ldif\Encoder::encode($data, array('sort' => false, + 'version' => null) + ); + $this->assertEquals($expected, $actual); + } +} diff --git a/test/Node/AttributeIterationTest.php b/test/Node/AttributeIterationTest.php new file mode 100644 index 000000000..f929d74f7 --- /dev/null +++ b/test/Node/AttributeIterationTest.php @@ -0,0 +1,46 @@ +createTestNode(); + $i = 0; + $data = array(); + foreach ($node->getAttributes() as $k => $v) { + $this->assertNotNull($k); + $this->assertNotNull($v); + $this->assertEquals($node->$k, $v); + $data[$k] = $v; + $i++; + } + $this->assertEquals(5, $i); + $this->assertEquals($i, count($node)); + $this->assertEquals(array( + 'boolean' => array(true, false), + 'cn' => array('name'), + 'empty' => array(), + 'host' => array('a', 'b', 'c'), + 'objectclass' => array('account', 'top')), $data + ); + } +} diff --git a/test/Node/ChildrenIterationTest.php b/test/Node/ChildrenIterationTest.php new file mode 100644 index 000000000..2d8c2be56 --- /dev/null +++ b/test/Node/ChildrenIterationTest.php @@ -0,0 +1,104 @@ +prepareLDAPServer(); + } + + protected function tearDown() + { + $this->cleanupLDAPServer(); + parent::tearDown(); + } + + public function testSimpleIteration() + { + $node = $this->getLDAP()->getBaseNode(); + $children = $node->getChildren(); + + $i = 1; + foreach ($children as $rdn => $n) { + $dn = $n->getDn()->toString(Ldap\Dn::ATTR_CASEFOLD_LOWER); + $rdn = Ldap\Dn::implodeRdn($n->getRdnArray(), Ldap\Dn::ATTR_CASEFOLD_LOWER); + if ($i == 1) { + $this->assertEquals('ou=Node', $rdn); + $this->assertEquals($this->createDn('ou=Node,'), $dn); + } else { + $this->assertEquals('ou=Test' . ($i - 1), $rdn); + $this->assertEquals($this->createDn('ou=Test' . ($i - 1) . ','), $dn); + } + $i++; + } + $this->assertEquals(6, $i - 1); + } + + public function testSimpleRecursiveIteration() + { + $node = $this->getLDAP()->getBaseNode(); + $ri = new \RecursiveIteratorIterator($node, \RecursiveIteratorIterator::SELF_FIRST); + $i = 0; + foreach ($ri as $rdn => $n) { + $dn = $n->getDn()->toString(Ldap\Dn::ATTR_CASEFOLD_LOWER); + $rdn = Ldap\Dn::implodeRdn($n->getRdnArray(), Ldap\Dn::ATTR_CASEFOLD_LOWER); + if ($i == 0) { + $this->assertEquals(Ldap\Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE) + ->toString(Ldap\Dn::ATTR_CASEFOLD_LOWER), $dn + ); + } elseif ($i == 1) { + $this->assertEquals('ou=Node', $rdn); + $this->assertEquals($this->createDn('ou=Node,'), $dn); + } else { + if ($i < 4) { + $j = $i - 1; + $base = $this->createDn('ou=Node,'); + } else { + $j = $i - 3; + $base = Ldap\Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE) + ->toString(Ldap\Dn::ATTR_CASEFOLD_LOWER); + } + $this->assertEquals('ou=Test' . $j, $rdn); + $this->assertEquals('ou=Test' . $j . ',' . $base, $dn); + } + $i++; + } + $this->assertEquals(9, $i); + } + + /** + * Test issue reported by Lance Hendrix on + * http://framework.zend.com/wiki/display/ZFPROP/Zend_Ldap+-+Extended+support+-+Stefan+Gehrig? + * focusedCommentId=13107431#comment-13107431 + */ + public function testCallingNextAfterIterationShouldNotThrowException() + { + $node = $this->getLDAP()->getBaseNode(); + $nodes = $node->searchChildren('(objectClass=*)'); + foreach ($nodes as $rdn => $n) { + // do nothing - just iterate + } + $nodes->next(); + } +} diff --git a/test/Node/ChildrenTest.php b/test/Node/ChildrenTest.php new file mode 100644 index 000000000..e5005804d --- /dev/null +++ b/test/Node/ChildrenTest.php @@ -0,0 +1,188 @@ +prepareLDAPServer(); + } + + protected function tearDown() + { + $this->cleanupLDAPServer(); + parent::tearDown(); + } + + public function testGetChildrenOnAttachedNode() + { + $node = $this->getLDAP()->getBaseNode(); + $children = $node->getChildren(); + $this->assertInstanceOf('Zend\Ldap\Node\ChildrenIterator', $children); + $this->assertEquals(6, count($children)); + $this->assertInstanceOf('Zend\Ldap\Node', $children['ou=Node']); + } + + public function testGetChildrenOnDetachedNode() + { + $node = $this->getLDAP()->getBaseNode(); + $node->detachLDAP(); + $children = $node->getChildren(); + $this->assertInstanceOf('Zend\Ldap\Node\ChildrenIterator', $children); + $this->assertEquals(0, count($children)); + + $node->attachLDAP($this->getLDAP()); + $node->reload(); + $children = $node->getChildren(); + + $this->assertInstanceOf('Zend\Ldap\Node\ChildrenIterator', $children); + $this->assertEquals(6, count($children)); + $this->assertInstanceOf('Zend\Ldap\Node', $children['ou=Node']); + } + + public function testHasChildrenOnAttachedNode() + { + $node = $this->getLDAP()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $this->assertTrue($node->hasChildren()); + $this->assertTrue($node->hasChildren()); + + $node = $this->getLDAP()->getNode($this->createDn('ou=Node,')); + $this->assertTrue($node->hasChildren()); + $this->assertTrue($node->hasChildren()); + + $node = $this->getLDAP()->getNode($this->createDn('ou=Test1,')); + $this->assertFalse($node->hasChildren()); + $this->assertFalse($node->hasChildren()); + + $node = $this->getLDAP()->getNode($this->createDn('ou=Test1,ou=Node,')); + $this->assertFalse($node->hasChildren()); + $this->assertFalse($node->hasChildren()); + } + + public function testHasChildrenOnDetachedNodeWithoutPriorGetChildren() + { + $node = $this->getLDAP()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $node->detachLDAP(); + $this->assertFalse($node->hasChildren()); + + $node = $this->getLDAP()->getNode($this->createDn('ou=Node,')); + $node->detachLDAP(); + $this->assertFalse($node->hasChildren()); + + $node = $this->getLDAP()->getNode($this->createDn('ou=Test1,')); + $node->detachLDAP(); + $this->assertFalse($node->hasChildren()); + + $node = $this->getLDAP()->getNode($this->createDn('ou=Test1,ou=Node,')); + $node->detachLDAP(); + $this->assertFalse($node->hasChildren()); + } + + public function testHasChildrenOnDetachedNodeWithPriorGetChildren() + { + $node = $this->getLDAP()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $node->getChildren(); + $node->detachLDAP(); + $this->assertTrue($node->hasChildren()); + + $node = $this->getLDAP()->getNode($this->createDn('ou=Node,')); + $node->getChildren(); + $node->detachLDAP(); + $this->assertTrue($node->hasChildren()); + + $node = $this->getLDAP()->getNode($this->createDn('ou=Test1,')); + $node->getChildren(); + $node->detachLDAP(); + $this->assertFalse($node->hasChildren()); + + $node = $this->getLDAP()->getNode($this->createDn('ou=Test1,ou=Node,')); + $node->getChildren(); + $node->detachLDAP(); + $this->assertFalse($node->hasChildren()); + } + + public function testChildrenCollectionSerialization() + { + $node = $this->getLDAP()->getNode($this->createDn('ou=Node,')); + + $children = $node->getChildren(); + $this->assertTrue($node->hasChildren()); + $this->assertEquals(2, count($children)); + + $string = serialize($node); + $node2 = unserialize($string); + + $children2 = $node2->getChildren(); + $this->assertTrue($node2->hasChildren()); + $this->assertEquals(2, count($children2)); + + $node2->attachLDAP($this->getLDAP()); + + $children2 = $node2->getChildren(); + $this->assertTrue($node2->hasChildren()); + $this->assertEquals(2, count($children2)); + + $node = $this->getLDAP()->getNode($this->createDn('ou=Node,')); + $this->assertTrue($node->hasChildren()); + $string = serialize($node); + $node2 = unserialize($string); + $this->assertFalse($node2->hasChildren()); + $node2->attachLDAP($this->getLDAP()); + $this->assertTrue($node2->hasChildren()); + } + + public function testCascadingAttachAndDetach() + { + $node = $this->getLDAP()->getBaseNode(); + $baseChildren = $node->getChildren(); + $nodeChildren = $baseChildren['ou=Node']->getChildren(); + + $this->assertTrue($node->isAttached()); + foreach ($baseChildren as $bc) { + $this->assertTrue($bc->isAttached()); + } + foreach ($nodeChildren as $nc) { + $this->assertTrue($nc->isAttached()); + } + + $node->detachLDAP(); + $this->assertFalse($node->isAttached()); + foreach ($baseChildren as $bc) { + $this->assertFalse($bc->isAttached()); + } + foreach ($nodeChildren as $nc) { + $this->assertFalse($nc->isAttached()); + } + + $node->attachLDAP($this->getLDAP()); + $this->assertTrue($node->isAttached()); + $this->assertSame($this->getLDAP(), $node->getLDAP()); + foreach ($baseChildren as $bc) { + $this->assertTrue($bc->isAttached()); + $this->assertSame($this->getLDAP(), $bc->getLDAP()); + } + foreach ($nodeChildren as $nc) { + $this->assertTrue($nc->isAttached()); + $this->assertSame($this->getLDAP(), $nc->getLDAP()); + } + } +} diff --git a/test/Node/OfflineTest.php b/test/Node/OfflineTest.php new file mode 100644 index 000000000..2c5806fc2 --- /dev/null +++ b/test/Node/OfflineTest.php @@ -0,0 +1,664 @@ +assertEquals($tsValue, $value); + } + + protected function assertUtcDateTimeString($localTimestamp, $value) + { + $localOffset = date('Z', $localTimestamp); + $utcTimestamp = $localTimestamp - $localOffset; + $this->assertEquals(date('YmdHis', $utcTimestamp) . 'Z', $value); + } + + public function testCreateFromArrayStringDn() + { + $data = $this->createTestArrayData(); + $node = Ldap\Node::fromArray($data); + $this->assertInstanceOf('Zend\Ldap\Node', $node); + $this->assertFalse($node->isAttached()); + $this->assertFalse($node->willBeDeleted()); + $this->assertFalse($node->willBeMoved()); + $this->assertTrue($node->isNew()); + } + + public function testCreateFromArrayObjectDn() + { + $data = $this->createTestArrayData(); + $data['dn'] = Ldap\Dn::fromString($data['dn']); + $node = Ldap\Node::fromArray($data); + $this->assertInstanceOf('Zend\Ldap\Node', $node); + $this->assertFalse($node->isAttached()); + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testCreateFromArrayMissingDn() + { + $data = $this->createTestArrayData(); + unset($data['dn']); + $node = Ldap\Node::fromArray($data); + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testCreateFromArrayIllegalDn() + { + $data = $this->createTestArrayData(); + $data['dn'] = 5; + $node = Ldap\Node::fromArray($data); + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testCreateFromArrayMalformedDn() + { + $data = $this->createTestArrayData(); + $data['dn'] = 'name1,cn=name2,dc=example,dc=org'; + $node = Ldap\Node::fromArray($data); + } + + public function testCreateFromArrayAndEnsureRdnValues() + { + $data = $this->createTestArrayData(); + $data['dn'] = Ldap\Dn::fromString($data['dn']); + $node = Ldap\Node::fromArray($data); + $this->assertInstanceOf('Zend\Ldap\Node', $node); + $this->assertFalse($node->isAttached()); + unset($data['dn']); + $this->assertEquals($data, $node->getData()); + } + + public function testGetDnString() + { + $data = $this->createTestArrayData(); + $node = Ldap\Node::fromArray($data); + $this->assertEquals($data['dn'], $node->getDnString()); + } + + public function testGetDnArray() + { + $data = $this->createTestArrayData(); + $node = Ldap\Node::fromArray($data); + $exA = Ldap\Dn::explodeDn($data['dn']); + $this->assertEquals($exA, $node->getDnArray()); + } + + public function testGetDnObject() + { + $data = $this->createTestArrayData(); + $node = Ldap\Node::fromArray($data); + $compareDn = Ldap\Dn::fromString('cn=name,dc=example,dc=org'); + $this->assertEquals($compareDn, $node->getDn()); + $this->assertNotSame($node->getDn(), $node->getDn()); + } + + public function testGetRdnString() + { + $node = $this->createTestNode(); + $this->assertEquals('cn=name', $node->getRdnString()); + } + + public function testGetRdnArray() + { + $node = $this->createTestNode(); + $this->assertEquals(array('cn' => 'name'), $node->getRdnArray()); + } + + public function testSerialize() + { + $node = $this->createTestNode(); + $sdata = serialize($node); + $newObject = unserialize($sdata); + $this->assertEquals($node, $newObject); + } + + public function testToString() + { + $node = $this->createTestNode(); + $this->assertEquals('cn=name,dc=example,dc=org', $node->toString()); + $this->assertEquals('cn=name,dc=example,dc=org', (string)$node); + } + + public function testToArray() + { + $node = $this->createTestNode(); + $this->assertEquals(array( + 'dn' => 'cn=name,dc=example,dc=org', + 'cn' => array('name'), + 'host' => array('a', 'b', 'c'), + 'empty' => array(), + 'boolean' => array(true, false), + 'objectclass' => array('account', 'top'), + ), $node->toArray() + ); + } + + public function testToJson() + { + $node = $this->createTestNode(); + $this->assertEquals('{"dn":"cn=name,dc=example,dc=org",' . + '"boolean":[true,false],' . + '"cn":["name"],' . + '"empty":[],' . + '"host":["a","b","c"],' . + '"objectclass":["account","top"]}', $node->toJson() + ); + } + + public function testGetData() + { + $data = $this->createTestArrayData(); + $node = Ldap\Node::fromArray($data); + ksort($data, SORT_STRING); + unset($data['dn']); + $this->assertEquals($data, $node->getData()); + } + + public function testGetObjectClass() + { + $node = $this->createTestNode(); + $this->assertEquals(array('account', 'top'), $node->getObjectClass()); + } + + public function testModifyObjectClass() + { + $node = $this->createTestNode(); + $this->assertEquals(array('account', 'top'), $node->getObjectClass()); + + $node->setObjectClass('domain'); + $this->assertEquals(array('domain'), $node->getObjectClass()); + + $node->setObjectClass(array('account', 'top')); + $this->assertEquals(array('account', 'top'), $node->getObjectClass()); + + $node->appendObjectClass('domain'); + $this->assertEquals(array('account', 'top', 'domain'), $node->getObjectClass()); + + $node->setObjectClass('domain'); + $node->appendObjectClass(array('account', 'top')); + $this->assertEquals(array('domain', 'account', 'top'), $node->getObjectClass()); + } + + public function testGetAttributes() + { + $node = $this->createTestNode(); + $expected = array( + 'boolean' => array(true, false), + 'cn' => array('name'), + 'empty' => array(), + 'host' => array('a', 'b', 'c'), + 'objectclass' => array('account', 'top'), + ); + $this->assertEquals($expected, $node->getAttributes()); + $this->assertFalse($node->willBeDeleted()); + $this->assertFalse($node->willBeMoved()); + $this->assertFalse($node->isNew()); + + $node->delete(); + $this->assertTrue($node->willBeDeleted()); + } + + public function testAppendToAttributeFirstTime() + { + $node = $this->createTestNode(); + $node->appendToAttribute('host', 'newHost'); + $ts = mktime(12, 30, 30, 6, 25, 2008); + $node->appendToDateTimeAttribute('objectClass', $ts); + $this->assertEquals('newHost', $node->host[3]); + $this->assertEquals($ts, $node->getDateTimeAttribute('objectClass', 2)); + } + + public function testExistsAttribute() + { + $node = $this->createTestNode(); + $this->assertFalse($node->existsAttribute('nonExistant')); + $this->assertFalse($node->existsAttribute('empty', false)); + $this->assertTrue($node->existsAttribute('empty', true)); + + $node->newEmpty = null; + $this->assertFalse($node->existsAttribute('newEmpty', false)); + $this->assertTrue($node->existsAttribute('newEmpty', true)); + + $node->empty = 'string'; + $this->assertTrue($node->existsAttribute('empty', false)); + $this->assertTrue($node->existsAttribute('empty', true)); + + $node->deleteAttribute('empty'); + $this->assertFalse($node->existsAttribute('empty', false)); + $this->assertTrue($node->existsAttribute('empty', true)); + } + + public function testGetSetAndDeleteMethods() + { + $node = $this->createTestNode(); + + $node->setAttribute('key', 'value1'); + $this->assertEquals('value1', $node->getAttribute('key', 0)); + $node->appendToAttribute('key', 'value2'); + $this->assertEquals('value1', $node->getAttribute('key', 0)); + $this->assertEquals('value2', $node->getAttribute('key', 1)); + $this->assertTrue($node->existsAttribute('key', true)); + $this->assertTrue($node->existsAttribute('key', false)); + $node->deleteAttribute('key'); + $this->assertEquals(0, count($node->getAttribute('key'))); + $this->assertTrue($node->existsAttribute('key', true)); + $this->assertFalse($node->existsAttribute('key', false)); + + $ts = mktime(12, 30, 30, 6, 25, 2008); + $node->setDateTimeAttribute('key', $ts, false); + $this->assertLocalDateTimeString($ts, $node->getAttribute('key', 0)); + $this->assertEquals($ts, $node->getDateTimeAttribute('key', 0)); + $node->appendToDateTimeAttribute('key', $ts, true); + $this->assertLocalDateTimeString($ts, $node->getAttribute('key', 0)); + $this->assertEquals($ts, $node->getDateTimeAttribute('key', 0)); + $this->assertUtcDateTimeString($ts, $node->getAttribute('key', 1)); + $this->assertEquals($ts, $node->getDateTimeAttribute('key', 1)); + $this->assertTrue($node->existsAttribute('key', true)); + $this->assertTrue($node->existsAttribute('key', false)); + $node->deleteAttribute('key'); + $this->assertEquals(0, count($node->getAttribute('key'))); + $this->assertTrue($node->existsAttribute('key', true)); + $this->assertFalse($node->existsAttribute('key', false)); + + $node->setPasswordAttribute('pa$$w0rd', Ldap\Attribute::PASSWORD_HASH_MD5); + $this->assertEquals('{MD5}bJuLJ96h3bhF+WqiVnxnVA==', $node->getAttribute('userPassword', 0)); + $this->assertTrue($node->existsAttribute('userPassword', true)); + $this->assertTrue($node->existsAttribute('userPassword', false)); + $node->deleteAttribute('userPassword'); + $this->assertEquals(0, count($node->getAttribute('userPassword'))); + $this->assertTrue($node->existsAttribute('userPassword', true)); + $this->assertFalse($node->existsAttribute('userPassword', false)); + } + + public function testOverloading() + { + $node = $this->createTestNode(); + + $node->key = 'value1'; + $this->assertEquals('value1', $node->key[0]); + $this->assertTrue(isset($node->key)); + unset($node->key); + $this->assertEquals(0, count($node->key)); + $this->assertFalse(isset($node->key)); + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testIllegalAttributeAccessRdnAttributeSet() + { + $node = $this->createTestNode(); + $node->cn = 'test'; + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testIllegalAttributeAccessDnSet() + { + $node = $this->createTestNode(); + $node->dn = 'test'; + } + + public function testAttributeAccessDnGet() + { + $node = $this->createTestNode(); + $this->assertInternalType('string', $node->dn); + $this->assertEquals($node->getDn()->toString(), $node->dn); + } + + public function testArrayAccess() + { + $node = $this->createTestNode(); + + $node['key'] = 'value1'; + $this->assertEquals('value1', $node['key'][0]); + $this->assertTrue(isset($node['key'])); + unset($node['key']); + $this->assertEquals(0, count($node['key'])); + $this->assertFalse(isset($node['key'])); + } + + public function testCreateEmptyNode() + { + $dn = 'cn=name,dc=example,dc=org'; + $objectClass = array('account', 'test', 'inetOrgPerson'); + $node = Ldap\Node::create($dn, $objectClass); + $this->assertEquals($dn, $node->getDnString()); + $this->assertEquals('cn=name', $node->getRdnString()); + $this->assertEquals('name', $node->cn[0]); + $this->assertEquals($objectClass, $node->objectClass); + $this->assertFalse($node->willBeDeleted()); + $this->assertFalse($node->willBeMoved()); + $this->assertTrue($node->isNew()); + + $node->delete(); + $this->assertTrue($node->willBeDeleted()); + } + + public function testGetChangedData() + { + $node = $this->createTestNode(); + $node->host = array('d'); + $node->empty = 'not Empty'; + unset($node->objectClass); + $changedData = $node->getChangedData(); + $this->assertEquals(array('d'), $changedData['host']); + $this->assertEquals(array('not Empty'), $changedData['empty']); + $this->assertEquals(array(), $changedData['objectclass']); + } + + public function testDeleteUnusedAttribute() + { + $node = $this->createTestNode(); + $node->deleteAttribute('nonexistant'); + $changedData = $node->getChangedData(); + $this->assertArrayNotHasKey('nonexistant', $changedData); + } + + public function testRenameNodeString() + { + $data = $this->createTestArrayData(); + $node = Ldap\Node::fromArray($data); + + $newDnString = 'cn=test+ou=Lab+uid=tester,cn=name,dc=example,dc=org'; + $node->setDn($newDnString); + $this->assertEquals($data['dn'], $node->getCurrentDn()->toString()); + $this->assertEquals($newDnString, $node->getDn()->toString()); + $this->assertEquals(array('test'), $node->cn); + $this->assertEquals(array('tester'), $node->uid); + $this->assertEquals(array('Lab'), $node->ou); + + $this->assertFalse($node->willBeDeleted()); + $this->assertFalse($node->willBeMoved()); + $this->assertTrue($node->isNew()); + } + + public function testRenameNodeArray() + { + $data = $this->createTestArrayData(); + $node = Ldap\Node::fromArray($data); + + $newDnArray = array( + array('uid' => 'tester'), + array('dc' => 'example'), + array('dc' => 'org')); + + $node->setDn($newDnArray); + $this->assertEquals($data['dn'], $node->getCurrentDn()->toString()); + $this->assertEquals($newDnArray, $node->getDn()->toArray()); + $this->assertEquals(array('name'), $node->cn); + } + + public function testRenameNodeDnObject() + { + $data = $this->createTestArrayData(); + $node = Ldap\Node::fromArray($data); + + $newDn = Ldap\Dn::fromString('cn=test+ou=Lab+uid=tester,cn=name,dc=example,dc=org'); + $node->setDn($newDn); + $this->assertEquals($data['dn'], $node->getCurrentDn()->toString()); + $this->assertEquals($newDn, $node->getDn()); + $this->assertEquals(array('test'), $node->cn); + $this->assertEquals(array('tester'), $node->uid); + $this->assertEquals(array('Lab'), $node->ou); + } + + public function testRenameNodeFromDataSource() + { + $node = $this->createTestNode(); + $newDnString = 'cn=test+ou=Lab+uid=tester,cn=name,dc=example,dc=org'; + $node->rename($newDnString); + + $this->assertFalse($node->willBeDeleted()); + $this->assertTrue($node->willBeMoved()); + $this->assertFalse($node->isNew()); + } + + public function testDnObjectCloning() + { + $node1 = $this->createTestNode(); + $dn1 = Ldap\Dn::fromString('cn=name2,dc=example,dc=org'); + $node1->setDn($dn1); + $dn1->prepend(array('cn' => 'name')); + $this->assertNotEquals($dn1->toString(), $node1->getDn()->toString()); + + $dn2 = Ldap\Dn::fromString('cn=name2,dc=example,dc=org'); + $node2 = Ldap\Node::create($dn2); + $dn2->prepend(array('cn' => 'name')); + $this->assertNotEquals($dn2->toString(), $node2->getDn()->toString()); + + $dn3 = Ldap\Dn::fromString('cn=name2,dc=example,dc=org'); + $node3 = Ldap\Node::fromArray(array( + 'dn' => $dn3, + 'ou' => 'Test'), false + ); + $dn3->prepend(array('cn' => 'name')); + $this->assertNotEquals($dn3->toString(), $node3->getDn()->toString()); + } + + public function testGetChanges() + { + $node = $this->createTestNode(); + $node->host = array('d'); + $node->empty = 'not Empty'; + unset($node->boolean); + $changes = $node->getChanges(); + $this->assertEquals(array( + 'add' => array( + 'empty' => array('not Empty') + ), + 'delete' => array( + 'boolean' => array() + ), + 'replace' => array( + 'host' => array('d') + ) + ), $changes + ); + + $node = Ldap\Node::create('uid=test,dc=example,dc=org', array('account')); + $node->host = 'host'; + unset($node->cn); + unset($node['sn']); + $node['givenName'] = 'givenName'; + $node->appendToAttribute('objectClass', 'domain'); + $this->assertEquals(array( + 'uid' => array('test'), + 'objectclass' => array('account', 'domain'), + 'host' => array('host'), + 'givenname' => array('givenName') + ), $node->getChangedData() + ); + $this->assertEquals(array( + 'add' => array( + 'uid' => array('test'), + 'objectclass' => array('account', 'domain'), + 'host' => array('host'), + 'givenname' => array('givenName'), + ), + 'delete' => array(), + 'replace' => array() + ), $node->getChanges() + ); + } + + public function testHasValue() + { + $node = $this->createTestNode(); + + $this->assertTrue($node->attributeHasValue('cn', 'name')); + $this->assertFalse($node->attributeHasValue('cn', 'noname')); + $this->assertTrue($node->attributeHasValue('boolean', true)); + $this->assertTrue($node->attributeHasValue('boolean', false)); + + $this->assertTrue($node->attributeHasValue('host', array('a', 'b'))); + $this->assertTrue($node->attributeHasValue('host', array('a', 'b', 'c'))); + $this->assertFalse($node->attributeHasValue('host', array('a', 'b', 'c', 'd'))); + $this->assertTrue($node->attributeHasValue('boolean', array(true, false))); + } + + public function testRemoveDuplicates() + { + $node = $this->createTestNode(); + $node->strings1 = array('value1', 'value2', 'value2', 'value3'); + $node->strings2 = array('value1', 'value2', 'value3', 'value4'); + $node->boolean1 = array(true, true, true, true); + $node->boolean2 = array(true, false, true, false); + + $expected = array( + 'cn' => array('name'), + 'host' => array('a', 'b', 'c'), + 'empty' => array(), + 'boolean' => array('TRUE', 'FALSE'), + 'objectclass' => array('account', 'top'), + 'strings1' => array('value1', 'value2', 'value3'), + 'strings2' => array('value1', 'value2', 'value3', 'value4'), + 'boolean1' => array('TRUE'), + 'boolean2' => array('TRUE', 'FALSE'), + ); + + $node->removeDuplicatesFromAttribute('strings1'); + $node->removeDuplicatesFromAttribute('strings2'); + $node->removeDuplicatesFromAttribute('boolean1'); + $node->removeDuplicatesFromAttribute('boolean2'); + $this->assertEquals($expected, $node->getData(false)); + } + + public function testRemoveFromAttributeSimple() + { + $node = $this->createTestNode(); + $node->test = array('value1', 'value2', 'value3', 'value3'); + $node->removeFromAttribute('test', 'value2'); + $this->assertEquals(array('value1', 'value3', 'value3'), $node->test); + } + + public function testRemoveFromAttributeArray() + { + $node = $this->createTestNode(); + $node->test = array('value1', 'value2', 'value3', 'value3'); + $node->removeFromAttribute('test', array('value1', 'value2')); + $this->assertEquals(array('value3', 'value3'), $node->test); + } + + public function testRemoveFromAttributeMultipleSimple() + { + $node = $this->createTestNode(); + $node->test = array('value1', 'value2', 'value3', 'value3'); + $node->removeFromAttribute('test', 'value3'); + $this->assertEquals(array('value1', 'value2'), $node->test); + } + + public function testRemoveFromAttributeMultipleArray() + { + $node = $this->createTestNode(); + $node->test = array('value1', 'value2', 'value3', 'value3'); + $node->removeFromAttribute('test', array('value1', 'value3')); + $this->assertEquals(array('value2'), $node->test); + } + + /** + * ZF-11611 + */ + public function testRdnAttributesHandleMultiValuedAttribute() + { + $data = array( + 'dn' => 'cn=funkygroup,ou=Groupes,dc=domain,dc=local', + 'objectClass' => array( + 'groupOfNames', + 'top', + ), + 'cn' => array( + 'The Funkygroup', + 'funkygroup', + ), + 'member' => 'uid=john-doe,ou=Users,dc=domain,dc=local', + ); + + $node = Ldap\Node::fromArray($data, true); + $changedData = $node->getChangedData(); + $this->assertEmpty($changedData); + } + + /** + * ZF-11611 + */ + public function testRdnAttributesHandleMultiValuedAttribute2() + { + $data = array( + 'dn' => 'cn=funkygroup,ou=Groupes,dc=domain,dc=local', + 'objectClass' => array( + 'groupOfNames', + 'top', + ), + 'member' => 'uid=john-doe,ou=Users,dc=domain,dc=local', + ); + + $node = Ldap\Node::fromArray($data, true); + $cn = $node->getAttribute('cn'); + $this->assertEquals(array( + 0 => 'funkygroup' + ), $cn); + } + + /** + * ZF-11611 + */ + public function testRdnAttributesHandleMultiValuedAttribute3() + { + $data = array( + 'dn' => 'cn=funkygroup,ou=Groupes,dc=domain,dc=local', + 'objectClass' => array( + 'groupOfNames', + 'top', + ), + 'cn' => array( + 0 => 'The Funkygroup' + ), + 'member' => 'uid=john-doe,ou=Users,dc=domain,dc=local', + ); + + $node = Ldap\Node::fromArray($data, true); + $cn = $node->getAttribute('cn'); + $this->assertEquals(array( + 0 => 'The Funkygroup', + 1 => 'funkygroup', + ), $cn); + } +} diff --git a/test/Node/OnlineTest.php b/test/Node/OnlineTest.php new file mode 100644 index 000000000..cadc3cad4 --- /dev/null +++ b/test/Node/OnlineTest.php @@ -0,0 +1,277 @@ +prepareLDAPServer(); + } + + protected function tearDown() + { + $this->cleanupLDAPServer(); + parent::tearDown(); + } + + public function testLoadFromLDAP() + { + $dn = $this->createDn('ou=Test1,'); + $node = Ldap\Node::fromLDAP($dn, $this->getLDAP()); + $this->assertInstanceOf('Zend\Ldap\Node', $node); + $this->assertTrue($node->isAttached()); + } + + public function testChangeReadOnlySystemAttributes() + { + $node = $this->getLDAP()->getBaseNode(); + try { + $node->setAttribute('createTimestamp', false); + $this->fail('Expected exception for modification of read-only attribute createTimestamp'); + } catch (Exception\ExceptionInterface $e) { + $this->assertEquals('Cannot change attribute because it\'s read-only', $e->getMessage()); + } + try { + $node->createTimestamp = false; + $this->fail('Expected exception for modification of read-only attribute createTimestamp'); + } catch (Exception\ExceptionInterface $e) { + $this->assertEquals('Cannot change attribute because it\'s read-only', $e->getMessage()); + } + try { + $node['createTimestamp'] = false; + $this->fail('Expected exception for modification of read-only attribute createTimestamp'); + } catch (Exception\ExceptionInterface $e) { + $this->assertEquals('Cannot change attribute because it\'s read-only', $e->getMessage()); + } + try { + $node->appendToAttribute('createTimestamp', 'value'); + $this->fail('Expected exception for modification of read-only attribute createTimestamp'); + } catch (Exception\ExceptionInterface $e) { + $this->assertEquals('Cannot change attribute because it\'s read-only', $e->getMessage()); + } + try { + $rdn = $node->getRdnArray(Ldap\Dn::ATTR_CASEFOLD_LOWER); + $attr = key($rdn); + $node->deleteAttribute($attr); + $this->fail('Expected exception for modification of read-only attribute ' . $attr); + } catch (Exception\ExceptionInterface $e) { + $this->assertEquals('Cannot change attribute because it\'s part of the RDN', $e->getMessage()); + } + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testLoadFromLDAPIllegalEntry() + { + $dn = $this->createDn('ou=Test99,'); + $node = Ldap\Node::fromLDAP($dn, $this->getLDAP()); + } + + public function testDetachAndReattach() + { + $dn = $this->createDn('ou=Test1,'); + $node = Ldap\Node::fromLDAP($dn, $this->getLDAP()); + $this->assertInstanceOf('Zend\Ldap\Node', $node); + $this->assertTrue($node->isAttached()); + $node->detachLDAP(); + $this->assertFalse($node->isAttached()); + $node->attachLDAP($this->getLDAP()); + $this->assertTrue($node->isAttached()); + } + + public function testSerialize() + { + $dn = $this->createDn('ou=Test1,'); + $node = Ldap\Node::fromLDAP($dn, $this->getLDAP()); + $sdata = serialize($node); + $newObject = unserialize($sdata); + $this->assertFalse($newObject->isAttached()); + $this->assertTrue($node->isAttached()); + $this->assertEquals($sdata, serialize($newObject)); + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testAttachToInvalidLDAP() + { + $data = array( + 'dn' => 'ou=name,dc=example,dc=org', + 'ou' => array('name'), + 'l' => array('a', 'b', 'c'), + 'objectClass' => array('organizationalUnit', 'top'), + ); + $node = Ldap\Node::fromArray($data); + $this->assertFalse($node->isAttached()); + $node->attachLDAP($this->getLDAP()); + } + + public function testAttachToValidLDAP() + { + $data = array( + 'dn' => $this->createDn('ou=name,'), + 'ou' => array('name'), + 'l' => array('a', 'b', 'c'), + 'objectClass' => array('organizationalUnit', 'top'), + ); + $node = Ldap\Node::fromArray($data); + $this->assertFalse($node->isAttached()); + $node->attachLDAP($this->getLDAP()); + $this->assertTrue($node->isAttached()); + } + + public function testExistsDn() + { + $data = array( + 'dn' => $this->createDn('ou=name,'), + 'ou' => array('name'), + 'l' => array('a', 'b', 'c'), + 'objectClass' => array('organizationalUnit', 'top'), + ); + $node1 = Ldap\Node::fromArray($data); + $node1->attachLDAP($this->getLDAP()); + $this->assertFalse($node1->exists()); + $dn = $this->createDn('ou=Test1,'); + $node2 = Ldap\Node::fromLDAP($dn, $this->getLDAP()); + $this->assertTrue($node2->exists()); + } + + public function testReload() + { + $dn = $this->createDn('ou=Test1,'); + $node = Ldap\Node::fromLDAP($dn, $this->getLDAP()); + $node->reload(); + $this->assertEquals($dn, $node->getDn()->toString()); + $this->assertEquals('ou=Test1', $node->getRdnString()); + } + + public function testGetNode() + { + $dn = $this->createDn('ou=Test1,'); + $node = $this->getLDAP()->getNode($dn); + $this->assertEquals($dn, $node->getDn()->toString()); + $this->assertEquals("Test1", $node->getAttribute('ou', 0)); + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testGetIllegalNode() + { + $dn = $this->createDn('ou=Test99,'); + $node = $this->getLDAP()->getNode($dn); + } + + public function testGetBaseNode() + { + $node = $this->getLDAP()->getBaseNode(); + $this->assertEquals(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, $node->getDnString()); + + $dn = Ldap\Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, + Ldap\Dn::ATTR_CASEFOLD_LOWER + ); + $this->assertEquals($dn[0]['ou'], $node->getAttribute('ou', 0)); + } + + public function testSearchSubtree() + { + $node = $this->getLDAP()->getNode($this->createDn('ou=Node,')); + $items = $node->searchSubtree('(objectClass=organizationalUnit)', Ldap\Ldap::SEARCH_SCOPE_SUB, + array(), 'ou' + ); + $this->assertInstanceOf('Zend\Ldap\Node\Collection', $items); + $this->assertEquals(3, $items->count()); + + $i = 0; + $dns = array( + $this->createDn('ou=Node,'), + $this->createDn('ou=Test1,ou=Node,'), + $this->createDn('ou=Test2,ou=Node,')); + foreach ($items as $key => $node) { + $key = Ldap\Dn::fromString($key)->toString(Ldap\Dn::ATTR_CASEFOLD_LOWER); + $this->assertEquals($dns[$i], $key); + if ($i === 0) { + $this->assertEquals('Node', $node->ou[0]); + } else { + $this->assertEquals('Test' . $i, $node->ou[0]); + } + $this->assertEquals($key, $node->getDnString(Ldap\Dn::ATTR_CASEFOLD_LOWER)); + $i++; + } + $this->assertEquals(3, $i); + } + + public function testCountSubtree() + { + $node = $this->getLDAP()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $this->assertEquals(9, $node->countSubtree('(objectClass=organizationalUnit)', + Ldap\Ldap::SEARCH_SCOPE_SUB + ) + ); + } + + public function testCountChildren() + { + $node = $this->getLDAP()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $this->assertEquals(6, $node->countChildren()); + $node = $this->getLDAP()->getNode($this->createDn('ou=Node,')); + $this->assertEquals(2, $node->countChildren()); + } + + public function testSearchChildren() + { + $node = $this->getLDAP()->getNode($this->createDn('ou=Node,')); + $this->assertEquals(2, $node->searchChildren('(objectClass=*)', array(), 'ou')->count()); + $node = $this->getLDAP()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $this->assertEquals(6, $node->searchChildren('(objectClass=*)', array(), 'ou')->count()); + } + + public function testGetParent() + { + $node = $this->getLDAP()->getNode($this->createDn('ou=Node,')); + $pnode = $node->getParent(); + $this->assertEquals(Ldap\Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE) + ->toString(Ldap\Dn::ATTR_CASEFOLD_LOWER), + $pnode->getDnString(Ldap\Dn::ATTR_CASEFOLD_LOWER) + ); + } + + /** + * @expectedException Zend\Ldap\Exception\ExceptionInterface + */ + public function testGetNonexistantParent() + { + $node = $this->getLDAP()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $pnode = $node->getParent(); + } + + public function testLoadFromLDAPWithDnObject() + { + $dn = Ldap\Dn::fromString($this->createDn('ou=Test1,')); + $node = Ldap\Node::fromLDAP($dn, $this->getLDAP()); + $this->assertInstanceOf('Zend\Ldap\Node', $node); + $this->assertTrue($node->isAttached()); + } +} diff --git a/test/Node/RootDseTest.php b/test/Node/RootDseTest.php new file mode 100644 index 000000000..790712371 --- /dev/null +++ b/test/Node/RootDseTest.php @@ -0,0 +1,175 @@ +getLDAP()->getRootDse(); + $root2 = $this->getLDAP()->getRootDse(); + + $this->assertEquals($root1, $root2); + $this->assertSame($root1, $root2); + } + + public function testSupportCheckMethods() + { + $root = $this->getLDAP()->getRootDse(); + + $this->assertInternalType('boolean', $root->supportsSaslMechanism('GSSAPI')); + $this->assertInternalType('boolean', $root->supportsSaslMechanism(array('GSSAPI', 'DIGEST-MD5'))); + $this->assertInternalType('boolean', $root->supportsVersion('3')); + $this->assertInternalType('boolean', $root->supportsVersion(3)); + $this->assertInternalType('boolean', $root->supportsVersion(array('3', '2'))); + $this->assertInternalType('boolean', $root->supportsVersion(array(3, 2))); + + switch ($root->getServerType()) { + case Node\RootDse::SERVER_TYPE_ACTIVEDIRECTORY: + $this->assertInternalType('boolean', $root->supportsControl('1.2.840.113556.1.4.319')); + $this->assertInternalType('boolean', $root->supportsControl(array('1.2.840.113556.1.4.319', + '1.2.840.113556.1.4.473') + ) + ); + $this->assertInternalType('boolean', $root->supportsCapability('1.3.6.1.4.1.4203.1.9.1.1')); + $this->assertInternalType('boolean', $root->supportsCapability(array('1.3.6.1.4.1.4203.1.9.1.1', + '2.16.840.1.113730.3.4.18') + ) + ); + $this->assertInternalType('boolean', $root->supportsPolicy('unknown')); + $this->assertInternalType('boolean', $root->supportsPolicy(array('unknown', 'unknown'))); + break; + case Node\RootDse::SERVER_TYPE_EDIRECTORY: + $this->assertInternalType('boolean', $root->supportsExtension('1.3.6.1.4.1.1466.20037')); + $this->assertInternalType('boolean', $root->supportsExtension(array('1.3.6.1.4.1.1466.20037', + '1.3.6.1.4.1.4203.1.11.1') + ) + ); + break; + case Node\RootDse::SERVER_TYPE_OPENLDAP: + $this->assertInternalType('boolean', $root->supportsControl('1.3.6.1.4.1.4203.1.9.1.1')); + $this->assertInternalType('boolean', $root->supportsControl(array('1.3.6.1.4.1.4203.1.9.1.1', + '2.16.840.1.113730.3.4.18') + ) + ); + $this->assertInternalType('boolean', $root->supportsExtension('1.3.6.1.4.1.1466.20037')); + $this->assertInternalType('boolean', $root->supportsExtension(array('1.3.6.1.4.1.1466.20037', + '1.3.6.1.4.1.4203.1.11.1') + ) + ); + $this->assertInternalType('boolean', $root->supportsFeature('1.3.6.1.1.14')); + $this->assertInternalType('boolean', $root->supportsFeature(array('1.3.6.1.1.14', + '1.3.6.1.4.1.4203.1.5.1') + ) + ); + break; + } + } + + public function testGetters() + { + $root = $this->getLDAP()->getRootDse(); + + $this->assertInternalType('array', $root->getNamingContexts()); + $this->assertInternalType('string', $root->getSubschemaSubentry()); + + switch ($root->getServerType()) { + case Node\RootDse::SERVER_TYPE_ACTIVEDIRECTORY: + $this->assertInternalType('string', $root->getConfigurationNamingContext()); + $this->assertInternalType('string', $root->getCurrentTime()); + $this->assertInternalType('string', $root->getDefaultNamingContext()); + $this->assertInternalType('string', $root->getDnsHostName()); + $this->assertInternalType('string', $root->getDomainControllerFunctionality()); + $this->assertInternalType('string', $root->getDomainFunctionality()); + $this->assertInternalType('string', $root->getDsServiceName()); + $this->assertInternalType('string', $root->getForestFunctionality()); + $this->assertInternalType('string', $root->getHighestCommittedUSN()); + $this->assertInternalType('boolean', $root->getIsGlobalCatalogReady()); + $this->assertInternalType('boolean', $root->getIsSynchronized()); + $this->assertInternalType('string', $root->getLDAPServiceName()); + $this->assertInternalType('string', $root->getRootDomainNamingContext()); + $this->assertInternalType('string', $root->getSchemaNamingContext()); + $this->assertInternalType('string', $root->getServerName()); + break; + case Node\RootDse::SERVER_TYPE_EDIRECTORY: + $this->assertInternalType('string', $root->getVendorName()); + $this->assertInternalType('string', $root->getVendorVersion()); + $this->assertInternalType('string', $root->getDsaName()); + $this->assertInternalType('string', $root->getStatisticsErrors()); + $this->assertInternalType('string', $root->getStatisticsSecurityErrors()); + $this->assertInternalType('string', $root->getStatisticsChainings()); + $this->assertInternalType('string', $root->getStatisticsReferralsReturned()); + $this->assertInternalType('string', $root->getStatisticsExtendedOps()); + $this->assertInternalType('string', $root->getStatisticsAbandonOps()); + $this->assertInternalType('string', $root->getStatisticsWholeSubtreeSearchOps()); + break; + case Node\RootDse::SERVER_TYPE_OPENLDAP: + $this->assertNullOrString($root->getConfigContext()); + $this->assertNullOrString($root->getMonitorContext()); + break; + } + } + + protected function assertNullOrString($value) + { + if ($value === null) { + $this->assertNull($value); + } else { + $this->assertInternalType('string', $value); + } + } + + /** + * @expectedException BadMethodCallException + */ + public function testSetterWillThrowException() + { + $root = $this->getLDAP()->getRootDse(); + $root->objectClass = 'illegal'; + } + + /** + * @expectedException BadMethodCallException + */ + public function testOffsetSetWillThrowException() + { + $root = $this->getLDAP()->getRootDse(); + $root['objectClass'] = 'illegal'; + } + + /** + * @expectedException BadMethodCallException + */ + public function testUnsetterWillThrowException() + { + $root = $this->getLDAP()->getRootDse(); + unset($root->objectClass); + } + + /** + * @expectedException BadMethodCallException + */ + public function testOffsetUnsetWillThrowException() + { + $root = $this->getLDAP()->getRootDse(); + unset($root['objectClass']); + } +} diff --git a/test/Node/SchemaTest.php b/test/Node/SchemaTest.php new file mode 100644 index 000000000..b37e25f05 --- /dev/null +++ b/test/Node/SchemaTest.php @@ -0,0 +1,317 @@ +schema = $this->getLDAP()->getSchema(); + } + + public function testSchemaNode() + { + $schema = $this->getLDAP()->getSchema(); + + $this->assertEquals($this->schema, $schema); + $this->assertSame($this->schema, $schema); + + $serial = serialize($this->schema); + $schemaUn = unserialize($serial); + $this->assertEquals($this->schema, $schemaUn); + $this->assertNotSame($this->schema, $schemaUn); + } + + public function testGetters() + { + $this->assertInternalType('array', $this->schema->getAttributeTypes()); + $this->assertInternalType('array', $this->schema->getObjectClasses()); + + switch ($this->getLDAP()->getRootDse()->getServerType()) { + case Node\RootDse::SERVER_TYPE_ACTIVEDIRECTORY: + break; + case Node\RootDse::SERVER_TYPE_EDIRECTORY: + break; + case Node\RootDse::SERVER_TYPE_OPENLDAP: + $this->assertInternalType('array', $this->schema->getLDAPSyntaxes()); + $this->assertInternalType('array', $this->schema->getMatchingRules()); + $this->assertInternalType('array', $this->schema->getMatchingRuleUse()); + break; + } + } + + /** + * @expectedException BadMethodCallException + */ + public function testSetterWillThrowException() + { + $this->schema->objectClass = 'illegal'; + } + + /** + * @expectedException BadMethodCallException + */ + public function testOffsetSetWillThrowException() + { + $this->schema['objectClass'] = 'illegal'; + } + + /** + * @expectedException BadMethodCallException + */ + public function testUnsetterWillThrowException() + { + unset($this->schema->objectClass); + } + + /** + * @expectedException BadMethodCallException + */ + public function testOffsetUnsetWillThrowException() + { + unset($this->schema['objectClass']); + } + + public function testOpenLDAPSchema() + { + if ($this->getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_OPENLDAP + ) { + $this->markTestSkipped('Test can only be run on an OpenLDAP server'); + } + + $objectClasses = $this->schema->getObjectClasses(); + $attributeTypes = $this->schema->getAttributeTypes(); + + $this->assertArrayHasKey('organizationalUnit', $objectClasses); + $ou = $objectClasses['organizationalUnit']; + $this->assertInstanceOf('Zend\Ldap\Node\Schema\ObjectClass\OpenLdap', $ou); + $this->assertEquals('organizationalUnit', $ou->getName()); + $this->assertEquals('2.5.6.5', $ou->getOid()); + $this->assertEquals(array('objectClass', 'ou'), $ou->getMustContain()); + $this->assertEquals(array('businessCategory', 'description', 'destinationIndicator', + 'facsimileTelephoneNumber', 'internationaliSDNNumber', 'l', + 'physicalDeliveryOfficeName', 'postOfficeBox', 'postalAddress', 'postalCode', + 'preferredDeliveryMethod', 'registeredAddress', 'searchGuide', 'seeAlso', 'st', + 'street', 'telephoneNumber', 'teletexTerminalIdentifier', 'telexNumber', + 'userPassword', 'x121Address'), $ou->getMayContain() + ); + $this->assertEquals('RFC2256: an organizational unit', $ou->getDescription()); + $this->assertEquals(\Zend\Ldap\Node\Schema::OBJECTCLASS_TYPE_STRUCTURAL, $ou->getType()); + $this->assertEquals(array('top'), $ou->getParentClasses()); + + $this->assertEquals('2.5.6.5', $ou->oid); + $this->assertEquals('organizationalUnit', $ou->name); + $this->assertEquals('RFC2256: an organizational unit', $ou->desc); + $this->assertFalse($ou->obsolete); + $this->assertEquals(array('top'), $ou->sup); + $this->assertFalse($ou->abstract); + $this->assertTrue($ou->structural); + $this->assertFalse($ou->auxiliary); + $this->assertEquals(array('ou'), $ou->must); + $this->assertEquals(array('userPassword', 'searchGuide', 'seeAlso', 'businessCategory', + 'x121Address', 'registeredAddress', 'destinationIndicator', 'preferredDeliveryMethod', + 'telexNumber', 'teletexTerminalIdentifier', 'telephoneNumber', + 'internationaliSDNNumber', 'facsimileTelephoneNumber', 'street', 'postOfficeBox', + 'postalCode', 'postalAddress', 'physicalDeliveryOfficeName', 'st', 'l', + 'description'), $ou->may + ); + $this->assertEquals("( 2.5.6.5 NAME 'organizationalUnit' " . + "DESC 'RFC2256: an organizational unit' SUP top STRUCTURAL MUST ou " . + "MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ " . + "registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ " . + "teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ " . + "facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ " . + "physicalDeliveryOfficeName $ st $ l $ description ) )", $ou->_string + ); + + $this->assertEquals(array(), $ou->aliases); + $this->assertSame($objectClasses['top'], $ou->_parents[0]); + + $this->assertArrayHasKey('ou', $attributeTypes); + $ou = $attributeTypes['ou']; + $this->assertInstanceOf('Zend\Ldap\Node\Schema\AttributeType\OpenLdap', $ou); + $this->assertEquals('ou', $ou->getName()); + $this->assertEquals('2.5.4.11', $ou->getOid()); + $this->assertEquals('1.3.6.1.4.1.1466.115.121.1.15', $ou->getSyntax()); + $this->assertEquals(32768, $ou->getMaxLength()); + $this->assertFalse($ou->isSingleValued()); + $this->assertEquals('RFC2256: organizational unit this object belongs to', $ou->getDescription()); + + $this->assertEquals('2.5.4.11', $ou->oid); + $this->assertEquals('ou', $ou->name); + $this->assertEquals('RFC2256: organizational unit this object belongs to', $ou->desc); + $this->assertFalse($ou->obsolete); + $this->assertEquals(array('name'), $ou->sup); + $this->assertNull($ou->equality); + $this->assertNull($ou->ordering); + $this->assertNull($ou->substr); + $this->assertNull($ou->syntax); + $this->assertNull($ou->{'max-length'}); + $this->assertFalse($ou->{'single-value'}); + $this->assertFalse($ou->collective); + $this->assertFalse($ou->{'no-user-modification'}); + $this->assertEquals('userApplications', $ou->usage); + $this->assertEquals("( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) " . + "DESC 'RFC2256: organizational unit this object belongs to' SUP name )", $ou->_string + ); + $this->assertEquals(array('organizationalUnitName'), $ou->aliases); + $this->assertSame($attributeTypes['name'], $ou->_parents[0]); + } + + public function testActiveDirectorySchema() + { + if ($this->getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_ACTIVEDIRECTORY + ) { + $this->markTestSkipped('Test can only be run on an Active Directory server'); + } + + $objectClasses = $this->schema->getObjectClasses(); + $attributeTypes = $this->schema->getAttributeTypes(); + } + + public function testeDirectorySchema() + { + if ($this->getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_EDIRECTORY + ) { + $this->markTestSkipped('Test can only be run on an eDirectory server'); + } + $this->markTestIncomplete("Novell eDirectory schema parsing is incomplete"); + } + + public function testOpenLDAPSchemaAttributeTypeInheritance() + { + if ($this->getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_OPENLDAP + ) { + $this->markTestSkipped('Test can only be run on an OpenLDAP server'); + } + + $attributeTypes = $this->schema->getAttributeTypes(); + + $name = $attributeTypes['name']; + $cn = $attributeTypes['cn']; + + $this->assertEquals('2.5.4.41', $name->getOid()); + $this->assertEquals('2.5.4.3', $cn->getOid()); + $this->assertNull($name->sup); + $this->assertEquals(array('name'), $cn->sup); + + $this->assertEquals('caseIgnoreMatch', $name->equality); + $this->assertNull($name->ordering); + $this->assertEquals('caseIgnoreSubstringsMatch', $name->substr); + $this->assertEquals('1.3.6.1.4.1.1466.115.121.1.15', $name->syntax); + $this->assertEquals('1.3.6.1.4.1.1466.115.121.1.15', $name->getSyntax()); + $this->assertEquals(32768, $name->{'max-length'}); + $this->assertEquals(32768, $name->getMaxLength()); + + $this->assertNull($cn->equality); + $this->assertNull($cn->ordering); + $this->assertNull($cn->substr); + $this->assertNull($cn->syntax); + $this->assertEquals('1.3.6.1.4.1.1466.115.121.1.15', $cn->getSyntax()); + $this->assertNull($cn->{'max-length'}); + $this->assertEquals(32768, $cn->getMaxLength()); + } + + public function testOpenLDAPSchemaObjectClassInheritance() + { + if ($this->getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_OPENLDAP + ) { + $this->markTestSkipped('Test can only be run on an OpenLDAP server'); + } + + $objectClasses = $this->schema->getObjectClasses(); + + if (!array_key_exists('certificationAuthority', $objectClasses) + || !array_key_exists('certificationAuthority-V2', $objectClasses) + ) { + $this->markTestSkipped('This requires OpenLDAP core schema'); + } + + $ca = $objectClasses['certificationAuthority']; + $ca2 = $objectClasses['certificationAuthority-V2']; + + $this->assertEquals('2.5.6.16', $ca->getOid()); + $this->assertEquals('2.5.6.16.2', $ca2->getOid()); + $this->assertEquals(array('top'), $ca->sup); + $this->assertEquals(array('certificationAuthority'), $ca2->sup); + + $this->assertEquals(array('authorityRevocationList', 'certificateRevocationList', + 'cACertificate'), $ca->must + ); + $this->assertEquals(array('authorityRevocationList', 'cACertificate', + 'certificateRevocationList', 'objectClass'), $ca->getMustContain() + ); + $this->assertEquals(array('crossCertificatePair'), $ca->may); + $this->assertEquals(array('crossCertificatePair'), $ca->getMayContain()); + + $this->assertEquals(array(), $ca2->must); + $this->assertEquals(array('authorityRevocationList', 'cACertificate', + 'certificateRevocationList', 'objectClass'), $ca2->getMustContain() + ); + $this->assertEquals(array('deltaRevocationList'), $ca2->may); + $this->assertEquals(array('crossCertificatePair', 'deltaRevocationList'), + $ca2->getMayContain() + ); + } + + public function testOpenLDAPSchemaAttributeTypeAliases() + { + if ($this->getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_OPENLDAP + ) { + $this->markTestSkipped('Test can only be run on an OpenLDAP server'); + } + + $attributeTypes = $this->schema->getAttributeTypes(); + $this->assertArrayHasKey('cn', $attributeTypes); + $this->assertArrayHasKey('commonName', $attributeTypes); + $ob1 = $attributeTypes['cn']; + $ob2 = $attributeTypes['commonName']; + $this->assertSame($ob1, $ob2); + } + + public function testOpenLDAPSchemaObjectClassAliases() + { + if ($this->getLDAP()->getRootDse()->getServerType() !== + Node\RootDse::SERVER_TYPE_OPENLDAP + ) { + $this->markTestSkipped('Test can only be run on an OpenLDAP server'); + } + + $objectClasses = $this->schema->getObjectClasses(); + $this->assertArrayHasKey('OpenLDAProotDSE', $objectClasses); + $this->assertArrayHasKey('LDAProotDSE', $objectClasses); + $ob1 = $objectClasses['OpenLDAProotDSE']; + $ob2 = $objectClasses['LDAProotDSE']; + $this->assertSame($ob1, $ob2); + } +} diff --git a/test/Node/UpdateTest.php b/test/Node/UpdateTest.php new file mode 100644 index 000000000..df21eb160 --- /dev/null +++ b/test/Node/UpdateTest.php @@ -0,0 +1,205 @@ +prepareLDAPServer(); + } + + protected function tearDown() + { + if (!constant('TESTS_ZEND_LDAP_ONLINE_ENABLED')) { + return; + } + + foreach ($this->getLDAP()->getBaseNode()->searchChildren('objectClass=*') as $child) { + $this->getLDAP()->delete($child->getDn(), true); + } + + parent::tearDown(); + } + + protected function stripActiveDirectorySystemAttributes(&$entry) + { + $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory', + 'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated'); + foreach ($adAttributes as $attr) { + if (array_key_exists($attr, $entry)) { + unset($entry[$attr]); + } + } + + if (array_key_exists('objectclass', $entry) && count($entry['objectclass']) > 0) { + if ($entry['objectclass'][0] !== 'top') { + $entry['objectclass'] = array_merge(array('top'), $entry['objectclass']); + } + } + } + + public function testSimpleUpdateOneValue() + { + $dn = $this->createDn('ou=Test1,'); + $node1 = Ldap\Node::fromLDAP($dn, $this->getLDAP()); + $node1->l = 'f'; + $node1->update(); + + $this->assertTrue($this->getLDAP()->exists($dn)); + $node2 = $this->getLDAP()->getEntry($dn); + $this->stripActiveDirectorySystemAttributes($node2); + unset($node2['dn']); + $node1 = $node1->getData(false); + $this->stripActiveDirectorySystemAttributes($node1); + $this->assertEquals($node2, $node1); + } + + public function testAddNewNode() + { + $dn = $this->createDn('ou=Test,'); + $node1 = Ldap\Node::create($dn, array('organizationalUnit')); + $node1->l = 'a'; + $node1->update($this->getLDAP()); + + $this->assertTrue($this->getLDAP()->exists($dn)); + $node2 = $this->getLDAP()->getEntry($dn); + $this->stripActiveDirectorySystemAttributes($node2); + unset($node2['dn']); + $node1 = $node1->getData(false); + $this->stripActiveDirectorySystemAttributes($node1); + $this->assertEquals($node2, $node1); + } + + public function testMoveExistingNode() + { + $dnOld = $this->createDn('ou=Test1,'); + $dnNew = $this->createDn('ou=Test,'); + $node1 = Ldap\Node::fromLDAP($dnOld, $this->getLDAP()); + $node1->l = 'f'; + $node1->setDn($dnNew); + $node1->update(); + + $this->assertFalse($this->getLDAP()->exists($dnOld)); + $this->assertTrue($this->getLDAP()->exists($dnNew)); + $node2 = $this->getLDAP()->getEntry($dnNew); + $this->stripActiveDirectorySystemAttributes($node2); + unset($node2['dn']); + $node1 = $node1->getData(false); + $this->stripActiveDirectorySystemAttributes($node1); + $this->assertEquals($node2, $node1); + } + + public function testMoveNewNode() + { + $dnOld = $this->createDn('ou=Test,'); + $dnNew = $this->createDn('ou=TestNew,'); + $node1 = Ldap\Node::create($dnOld, array('organizationalUnit')); + $node1->l = 'a'; + $node1->setDn($dnNew); + $node1->update($this->getLDAP()); + + $this->assertFalse($this->getLDAP()->exists($dnOld)); + $this->assertTrue($this->getLDAP()->exists($dnNew)); + $node2 = $this->getLDAP()->getEntry($dnNew); + $this->stripActiveDirectorySystemAttributes($node2); + unset($node2['dn']); + $node1 = $node1->getData(false); + $this->stripActiveDirectorySystemAttributes($node1); + $this->assertEquals($node2, $node1); + } + + public function testModifyDeletedNode() + { + $dn = $this->createDn('ou=Test1,'); + $node1 = Ldap\Node::create($dn, array('organizationalUnit')); + $node1->delete(); + $node1->update($this->getLDAP()); + + $this->assertFalse($this->getLDAP()->exists($dn)); + + $node1->l = 'a'; + $node1->update(); + + $this->assertFalse($this->getLDAP()->exists($dn)); + } + + public function testAddDeletedNode() + { + $dn = $this->createDn('ou=Test,'); + $node1 = Ldap\Node::create($dn, array('organizationalUnit')); + $node1->delete(); + $node1->update($this->getLDAP()); + + $this->assertFalse($this->getLDAP()->exists($dn)); + } + + public function testMoveDeletedExistingNode() + { + $dnOld = $this->createDn('ou=Test1,'); + $dnNew = $this->createDn('ou=Test,'); + $node1 = Ldap\Node::fromLDAP($dnOld, $this->getLDAP()); + $node1->setDn($dnNew); + $node1->delete(); + $node1->update(); + + $this->assertFalse($this->getLDAP()->exists($dnOld)); + $this->assertFalse($this->getLDAP()->exists($dnNew)); + } + + public function testMoveDeletedNewNode() + { + $dnOld = $this->createDn('ou=Test,'); + $dnNew = $this->createDn('ou=TestNew,'); + $node1 = Ldap\Node::create($dnOld, array('organizationalUnit')); + $node1->setDn($dnNew); + $node1->delete(); + $node1->update($this->getLDAP()); + + $this->assertFalse($this->getLDAP()->exists($dnOld)); + $this->assertFalse($this->getLDAP()->exists($dnNew)); + } + + public function testMoveNode() + { + $dnOld = $this->createDn('ou=Test1,'); + $dnNew = $this->createDn('ou=Test,'); + + $node = Ldap\Node::fromLDAP($dnOld, $this->getLDAP()); + $node->setDn($dnNew); + $node->update(); + $this->assertFalse($this->getLDAP()->exists($dnOld)); + $this->assertTrue($this->getLDAP()->exists($dnNew)); + + $node = Ldap\Node::fromLDAP($dnNew, $this->getLDAP()); + $node->move($dnOld); + $node->update(); + $this->assertFalse($this->getLDAP()->exists($dnNew)); + $this->assertTrue($this->getLDAP()->exists($dnOld)); + + $node = Ldap\Node::fromLDAP($dnOld, $this->getLDAP()); + $node->rename($dnNew); + $node->update(); + $this->assertFalse($this->getLDAP()->exists($dnOld)); + $this->assertTrue($this->getLDAP()->exists($dnNew)); + } +} diff --git a/test/OfflineTest.php b/test/OfflineTest.php new file mode 100644 index 000000000..a947f0735 --- /dev/null +++ b/test/OfflineTest.php @@ -0,0 +1,131 @@ +markTestSkipped('LDAP is not enabled'); + } + $this->ldap = new Ldap\Ldap(); + } + + /** + * @return void + */ + public function testInvalidOptionResultsInException() + { + $optionName = 'invalid'; + try { + $this->ldap->setOptions(array($optionName => 'irrelevant')); + $this->fail('Expected Zend\Ldap\Exception\LdapException not thrown'); + } catch (Exception\LdapException $e) { + $this->assertEquals("Unknown Zend\Ldap\Ldap option: $optionName", $e->getMessage()); + } + } + + public function testException() + { + $e = new Exception\LdapException(null, '', 0); + $this->assertEquals('no exception message', $e->getMessage()); + $this->assertEquals(0, $e->getCode()); + + $e = new Exception\LdapException(null, '', 15); + $this->assertEquals('0xf: no exception message', $e->getMessage()); + $this->assertEquals(15, $e->getCode()); + } + + public function testOptionsGetter() + { + $options = array( + 'host' => TESTS_ZEND_LDAP_HOST, + 'username' => TESTS_ZEND_LDAP_USERNAME, + 'password' => TESTS_ZEND_LDAP_PASSWORD, + 'baseDn' => TESTS_ZEND_LDAP_BASE_DN, + ); + $ldap = new Ldap\Ldap($options); + $this->assertEquals(array( + 'host' => TESTS_ZEND_LDAP_HOST, + 'port' => 0, + 'useSsl' => false, + 'username' => TESTS_ZEND_LDAP_USERNAME, + 'password' => TESTS_ZEND_LDAP_PASSWORD, + 'bindRequiresDn' => false, + 'baseDn' => TESTS_ZEND_LDAP_BASE_DN, + 'accountCanonicalForm' => null, + 'accountDomainName' => null, + 'accountDomainNameShort' => null, + 'accountFilterFormat' => null, + 'allowEmptyPassword' => false, + 'useStartTls' => false, + 'optReferrals' => false, + 'tryUsernameSplit' => true, + 'networkTimeout' => null, + ), $ldap->getOptions() + ); + } + + public function testConfigObject() + { + $config = new Config\Config(array( + 'host' => TESTS_ZEND_LDAP_HOST, + 'username' => TESTS_ZEND_LDAP_USERNAME, + 'password' => TESTS_ZEND_LDAP_PASSWORD, + 'baseDn' => TESTS_ZEND_LDAP_BASE_DN, + )); + $ldap = new Ldap\Ldap($config); + $this->assertEquals(array( + 'host' => TESTS_ZEND_LDAP_HOST, + 'port' => 0, + 'useSsl' => false, + 'username' => TESTS_ZEND_LDAP_USERNAME, + 'password' => TESTS_ZEND_LDAP_PASSWORD, + 'bindRequiresDn' => false, + 'baseDn' => TESTS_ZEND_LDAP_BASE_DN, + 'accountCanonicalForm' => null, + 'accountDomainName' => null, + 'accountDomainNameShort' => null, + 'accountFilterFormat' => null, + 'allowEmptyPassword' => false, + 'useStartTls' => false, + 'optReferrals' => false, + 'tryUsernameSplit' => true, + 'networkTimeout' => null, + ), $ldap->getOptions() + ); + } +} diff --git a/test/SearchTest.php b/test/SearchTest.php new file mode 100644 index 000000000..1b3de040f --- /dev/null +++ b/test/SearchTest.php @@ -0,0 +1,621 @@ +prepareLDAPServer(); + } + + protected function tearDown() + { + $this->cleanupLDAPServer(); + parent::tearDown(); + } + + public function testGetSingleEntry() + { + $dn = $this->createDn('ou=Test1,'); + $entry = $this->getLDAP()->getEntry($dn); + $this->assertEquals($dn, $entry["dn"]); + $this->assertArrayHasKey('ou', $entry); + $this->assertContains('Test1', $entry['ou']); + $this->assertEquals(1, count($entry['ou'])); + } + + public function testGetSingleIllegalEntry() + { + $dn = $this->createDn('ou=Test99,'); + $entry = $this->getLDAP()->getEntry($dn); + $this->assertNull($entry); + } + + /** + * @expectedException Zend\Ldap\Exception\LdapException + */ + public function testGetSingleIllegalEntryWithException() + { + $dn = $this->createDn('ou=Test99,'); + $entry = $this->getLDAP()->getEntry($dn, array(), true); + } + + public function testCountBase() + { + $dn = $this->createDn('ou=Node,'); + $count = $this->getLDAP()->count('(objectClass=*)', $dn, Ldap\Ldap::SEARCH_SCOPE_BASE); + $this->assertEquals(1, $count); + } + + public function testCountOne() + { + $dn1 = $this->createDn('ou=Node,'); + $count1 = $this->getLDAP()->count('(objectClass=*)', $dn1, Ldap\Ldap::SEARCH_SCOPE_ONE); + $this->assertEquals(2, $count1); + $dn2 = TESTS_ZEND_LDAP_WRITEABLE_SUBTREE; + $count2 = $this->getLDAP()->count('(objectClass=*)', $dn2, Ldap\Ldap::SEARCH_SCOPE_ONE); + $this->assertEquals(6, $count2); + } + + public function testCountSub() + { + $dn1 = $this->createDn('ou=Node,'); + $count1 = $this->getLDAP()->count('(objectClass=*)', $dn1, Ldap\Ldap::SEARCH_SCOPE_SUB); + $this->assertEquals(3, $count1); + $dn2 = TESTS_ZEND_LDAP_WRITEABLE_SUBTREE; + $count2 = $this->getLDAP()->count('(objectClass=*)', $dn2, Ldap\Ldap::SEARCH_SCOPE_SUB); + $this->assertEquals(9, $count2); + } + + public function testResultIteration() + { + $items = $this->getLDAP()->search('(objectClass=organizationalUnit)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $this->assertEquals(9, $items->count()); + $this->assertEquals(9, count($items)); + + $i = 0; + foreach ($items as $key => $item) { + $this->assertEquals($i, $key); + $i++; + } + $this->assertEquals(9, $i); + $j = 0; + foreach ($items as $item) { + $j++; + } + $this->assertEquals($i, $j); + } + + public function testSearchNoResult() + { + $items = $this->getLDAP()->search('(objectClass=account)', TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, + Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $this->assertEquals(0, $items->count()); + } + + public function testSearchEntriesShortcut() + { + $entries = $this->getLDAP()->searchEntries('(objectClass=organizationalUnit)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $this->assertInternalType("array", $entries); + $this->assertEquals(9, count($entries)); + } + + /** + * @expectedException Zend\Ldap\Exception\LdapException + */ + public function testIllegalSearch() + { + $dn = $this->createDn('ou=Node2,'); + $items = $this->getLDAP()->search('(objectClass=account)', $dn, Ldap\Ldap::SEARCH_SCOPE_SUB); + } + + public function testSearchNothingGetFirst() + { + $entries = $this->getLDAP()->search('(objectClass=account)', TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, + Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $this->assertEquals(0, $entries->count()); + $this->assertNull($entries->getFirst()); + } + + public function testSorting() + { + $lSorted = array('a', 'b', 'c', 'd', 'e'); + $items = $this->getLDAP()->search('(l=*)', TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, + Ldap\Ldap::SEARCH_SCOPE_SUB, array(), 'l' + ); + $this->assertEquals(5, $items->count()); + foreach ($items as $key => $item) { + $this->assertEquals($lSorted[$key], $item['l'][0]); + } + } + + public function testCountChildren() + { + $dn1 = $this->createDn('ou=Node,'); + $count1 = $this->getLDAP()->countChildren($dn1); + $this->assertEquals(2, $count1); + $dn2 = TESTS_ZEND_LDAP_WRITEABLE_SUBTREE; + $count2 = $this->getLDAP()->countChildren($dn2); + $this->assertEquals(6, $count2); + } + + public function testExistsDn() + { + $dn1 = $this->createDn('ou=Test2,'); + $dn2 = $this->createDn('ou=Test99,'); + $this->assertTrue($this->getLDAP()->exists($dn1)); + $this->assertFalse($this->getLDAP()->exists($dn2)); + } + + public function testSearchWithDnObjectAndFilterObject() + { + $dn = Ldap\Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $filter = Ldap\Filter::equals('objectClass', 'organizationalUnit'); + + $items = $this->getLDAP()->search($filter, $dn, Ldap\Ldap::SEARCH_SCOPE_SUB); + $this->assertEquals(9, $items->count()); + } + + public function testCountSubWithDnObjectAndFilterObject() + { + $dn1 = Ldap\Dn::fromString($this->createDn('ou=Node,')); + $filter = Ldap\Filter::any('objectClass'); + + $count1 = $this->getLDAP()->count($filter, $dn1, Ldap\Ldap::SEARCH_SCOPE_SUB); + $this->assertEquals(3, $count1); + + $dn2 = Ldap\Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $count2 = $this->getLDAP()->count($filter, $dn2, Ldap\Ldap::SEARCH_SCOPE_SUB); + $this->assertEquals(9, $count2); + } + + public function testCountChildrenWithDnObject() + { + $dn1 = Ldap\Dn::fromString($this->createDn('ou=Node,')); + $count1 = $this->getLDAP()->countChildren($dn1); + $this->assertEquals(2, $count1); + + $dn2 = Ldap\Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $count2 = $this->getLDAP()->countChildren($dn2); + $this->assertEquals(6, $count2); + } + + public function testExistsDnWithDnObject() + { + $dn1 = Ldap\Dn::fromString($this->createDn('ou=Test2,')); + $dn2 = Ldap\Dn::fromString($this->createDn('ou=Test99,')); + + $this->assertTrue($this->getLDAP()->exists($dn1)); + $this->assertFalse($this->getLDAP()->exists($dn2)); + } + + public function testSearchEntriesShortcutWithDnObjectAndFilterObject() + { + $dn = Ldap\Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE); + $filter = Ldap\Filter::equals('objectClass', 'organizationalUnit'); + + $entries = $this->getLDAP()->searchEntries($filter, $dn, Ldap\Ldap::SEARCH_SCOPE_SUB); + $this->assertInternalType("array", $entries); + $this->assertEquals(9, count($entries)); + } + + public function testGetSingleEntryWithDnObject() + { + $dn = Ldap\Dn::fromString($this->createDn('ou=Test1,')); + $entry = $this->getLDAP()->getEntry($dn); + $this->assertEquals($dn->toString(), $entry["dn"]); + } + + public function testMultipleResultIteration() + { + $items = $this->getLDAP()->search('(objectClass=organizationalUnit)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $isCount = 9; + $this->assertEquals($isCount, $items->count()); + + $i = 0; + foreach ($items as $key => $item) { + $this->assertEquals($i, $key); + $i++; + } + $this->assertEquals($isCount, $i); + $i = 0; + foreach ($items as $key => $item) { + $this->assertEquals($i, $key); + $i++; + } + $this->assertEquals($isCount, $i); + + $items->close(); + $i = 0; + foreach ($items as $key => $item) { + $this->assertEquals($i, $key); + $i++; + } + $this->assertEquals($isCount, $i); + $i = 0; + foreach ($items as $key => $item) { + $this->assertEquals($i, $key); + $i++; + } + $this->assertEquals($isCount, $i); + } + + /** + * Test issue reported by Lance Hendrix on + * http://framework.zend.com/wiki/display/ZFPROP/Zend_Ldap+-+Extended+support+-+Stefan+Gehrig? + * focusedCommentId=13107431#comment-13107431 + */ + public function testCallingNextAfterIterationShouldNotThrowException() + { + $items = $this->getLDAP()->search('(objectClass=organizationalUnit)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB + ); + foreach ($items as $key => $item) { + // do nothing - just iterate + } + $items->next(); + } + + public function testUnknownCollectionClassThrowsException() + { + try { + $items = $this->getLDAP()->search('(objectClass=organizationalUnit)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB, array(), null, + 'This_Class_Does_Not_Exist' + ); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains("Class 'This_Class_Does_Not_Exist' can not be found", + $zle->getMessage() + ); + } + } + + public function testCollectionClassNotSubclassingZendLDAPCollectionThrowsException() + { + try { + $items = $this->getLDAP()->search( + '(objectClass=organizationalUnit)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, + Ldap\Ldap::SEARCH_SCOPE_SUB, + array(), + null, + 'ZendTest\Ldap\CollectionClassNotSubclassingZendLDAPCollection' + ); + $this->fail('Expected exception not thrown'); + } catch (Exception\LdapException $zle) { + $this->assertContains( + "Class 'ZendTest\\Ldap\\CollectionClassNotSubclassingZendLDAPCollection' must subclass 'Zend\\Ldap\\Collection'", + $zle->getMessage() + ); + } + } + + /** + * @group ZF-8233 + */ + public function testSearchWithOptionsArray() + { + $items = $this + ->getLDAP() + ->search(array( + 'filter' => '(objectClass=organizationalUnit)', + 'baseDn' => TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, + 'scope' => Ldap\Ldap::SEARCH_SCOPE_SUB + ) + ); + $this->assertEquals(9, $items->count()); + } + + /** + * @group ZF-8233 + */ + public function testSearchEntriesShortcutWithOptionsArray() + { + $items = $this + ->getLDAP() + ->searchEntries(array( + 'filter' => '(objectClass=organizationalUnit)', + 'baseDn' => TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, + 'scope' => Ldap\Ldap::SEARCH_SCOPE_SUB + ) + ); + $this->assertEquals(9, count($items)); + } + + /** + * @group ZF-8233 + */ + public function testReverseSortingWithSearchEntriesShortcut() + { + $lSorted = array('e', 'd', 'c', 'b', 'a'); + $items = $this->getLDAP()->searchEntries('(l=*)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB, + array(), 'l', true + ); + foreach ($items as $key => $item) { + $this->assertEquals($lSorted[$key], $item['l'][0]); + } + } + + /** + * @group ZF-8233 + */ + public function testReverseSortingWithSearchEntriesShortcutWithOptionsArray() + { + $lSorted = array('e', 'd', 'c', 'b', 'a'); + $items = $this + ->getLDAP() + ->searchEntries(array( + 'filter' => '(l=*)', + 'baseDn' => TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, + 'scope' => Ldap\Ldap::SEARCH_SCOPE_SUB, + 'sort' => 'l', + 'reverseSort' => true + ) + ); + foreach ($items as $key => $item) { + $this->assertEquals($lSorted[$key], $item['l'][0]); + } + } + + public function testSearchNothingIteration() + { + $entries = $this->getLDAP()->search('(objectClass=account)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB, + array(), 'uid' + ); + $this->assertEquals(0, $entries->count()); + $i = 0; + foreach ($entries as $key => $item) { + $i++; + } + $this->assertEquals(0, $i); + } + + public function testSearchNothingToArray() + { + $entries = $this->getLDAP()->search('(objectClass=account)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB, + array(), 'uid' + ); + $entries = $entries->toArray(); + $this->assertEquals(0, count($entries)); + $i = 0; + foreach ($entries as $key => $item) { + $i++; + } + $this->assertEquals(0, $i); + } + + /** + * @group ZF-8259 + */ + public function testUserIsAutomaticallyBoundOnOperationInDisconnectedState() + { + $ldap = $this->getLDAP(); + $ldap->disconnect(); + $dn = $this->createDn('ou=Test1,'); + $entry = $ldap->getEntry($dn); + $this->assertEquals($dn, $entry['dn']); + } + + /** + * @group ZF-8259 + */ + public function testUserIsAutomaticallyBoundOnOperationInUnboundState() + { + $ldap = $this->getLDAP(); + $ldap->disconnect(); + $ldap->connect(); + $dn = $this->createDn('ou=Test1,'); + $entry = $ldap->getEntry($dn); + $this->assertEquals($dn, $entry['dn']); + } + + public function testInnerIteratorIsOfRequiredType() + { + $items = $this->getLDAP()->search('(objectClass=organizationalUnit)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $this->assertInstanceOf('\Zend\Ldap\Collection\DefaultIterator', $items->getInnerIterator()); + } + + /** + * @group ZF-8262 + */ + public function testCallingCurrentOnIteratorReturnsFirstElement() + { + $items = $this->getLDAP()->search('(objectClass=organizationalUnit)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $this->assertEquals(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, $items->getInnerIterator()->key()); + $current = $items->getInnerIterator()->current(); + $this->assertInternalType('array', $current); + $this->assertEquals(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, $current['dn']); + } + + /** + * @group ZF-8262 + */ + public function testCallingCurrentOnCollectionReturnsFirstElement() + { + $items = $this->getLDAP()->search('(objectClass=organizationalUnit)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $this->assertEquals(0, $items->key()); + $this->assertEquals(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, $items->dn()); + $current = $items->current(); + $this->assertInternalType('array', $current); + $this->assertEquals(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, $current['dn']); + } + + /** + * @group ZF-8262 + */ + public function testCallingCurrentOnEmptyIteratorReturnsNull() + { + $items = $this->getLDAP()->search('(objectClass=account)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $this->assertNull($items->getInnerIterator()->key()); + $this->assertNull($items->getInnerIterator()->current()); + } + + /** + * @group ZF-8262 + */ + public function testCallingCurrentOnEmptyCollectionReturnsNull() + { + $items = $this->getLDAP()->search('(objectClass=account)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $this->assertNull($items->key()); + $this->assertNull($items->dn()); + $this->assertNull($items->current()); + } + + /** + * @group ZF-8262 + */ + public function testResultIterationAfterCallingCurrent() + { + $items = $this->getLDAP()->search('(objectClass=organizationalUnit)', + TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Ldap\Ldap::SEARCH_SCOPE_SUB + ); + $this->assertEquals(9, $items->count()); + $this->assertEquals(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, $items->getInnerIterator()->key()); + $current = $items->current(); + $this->assertInternalType('array', $current); + $this->assertEquals(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, $current['dn']); + + $i = 0; + foreach ($items as $key => $item) { + $this->assertEquals($i, $key); + $i++; + } + $this->assertEquals(9, $i); + $j = 0; + foreach ($items as $item) { + $j++; + } + $this->assertEquals($i, $j); + } + + /** + * @group ZF-8263 + */ + public function testAttributeNameTreatmentToLower() + { + $dn = $this->createDn('ou=Node,'); + $list = $this->getLDAP()->search('objectClass=*', $dn, Ldap\Ldap::SEARCH_SCOPE_BASE); + $list->getInnerIterator()->setAttributeNameTreatment(Collection\DefaultIterator::ATTRIBUTE_TO_LOWER); + $this->assertArrayHasKey('postalcode', $list->current()); + } + + /** + * @group ZF-8263 + */ + public function testAttributeNameTreatmentToUpper() + { + $dn = $this->createDn('ou=Node,'); + $list = $this->getLDAP()->search('objectClass=*', $dn, Ldap\Ldap::SEARCH_SCOPE_BASE); + $list->getInnerIterator()->setAttributeNameTreatment(Collection\DefaultIterator::ATTRIBUTE_TO_UPPER); + $this->assertArrayHasKey('POSTALCODE', $list->current()); + } + + /** + * @group ZF-8263 + */ + public function testAttributeNameTreatmentNative() + { + $dn = $this->createDn('ou=Node,'); + $list = $this->getLDAP()->search('objectClass=*', $dn, Ldap\Ldap::SEARCH_SCOPE_BASE); + $list->getInnerIterator()->setAttributeNameTreatment(Collection\DefaultIterator::ATTRIBUTE_NATIVE); + $this->assertArrayHasKey('postalCode', $list->current()); + } + + /** + * @group ZF-8263 + */ + public function testAttributeNameTreatmentCustomFunction() + { + $dn = $this->createDn('ou=Node,'); + $list = $this->getLDAP()->search('objectClass=*', $dn, Ldap\Ldap::SEARCH_SCOPE_BASE); + $list->getInnerIterator()->setAttributeNameTreatment('ZendTest\Ldap\customNaming'); + $this->assertArrayHasKey('EDOCLATSOP', $list->current()); + } + + /** + * @group ZF-8263 + */ + public function testAttributeNameTreatmentCustomStaticMethod() + { + $dn = $this->createDn('ou=Node,'); + $list = $this->getLDAP()->search('objectClass=*', $dn, Ldap\Ldap::SEARCH_SCOPE_BASE); + $list->getInnerIterator()->setAttributeNameTreatment(array(__NAMESPACE__ . '\CustomNaming', 'name1')); + $this->assertArrayHasKey('edoclatsop', $list->current()); + } + + /** + * @group ZF-8263 + */ + public function testAttributeNameTreatmentCustomInstanceMethod() + { + $dn = $this->createDn('ou=Node,'); + $list = $this->getLDAP()->search('objectClass=*', $dn, Ldap\Ldap::SEARCH_SCOPE_BASE); + $namer = new CustomNaming(); + $list->getInnerIterator()->setAttributeNameTreatment(array($namer, 'name2')); + $this->assertArrayHasKey('edoClatsop', $list->current()); + } +} + +function customNaming($attrib) +{ + return strtoupper(strrev($attrib)); +} + +class CustomNaming +{ + public static function name1($attrib) + { + return strtolower(strrev($attrib)); + } + + public function name2($attrib) + { + return strrev($attrib); + } +} + +class CollectionClassNotSubclassingZendLDAPCollection +{ +} diff --git a/test/_files/AttributeTest.input.txt b/test/_files/AttributeTest.input.txt new file mode 100644 index 000000000..83c1b193e --- /dev/null +++ b/test/_files/AttributeTest.input.txt @@ -0,0 +1 @@ +String from file \ No newline at end of file diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 000000000..565d7bb7c --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,34 @@ +