diff --git a/.travis.yml b/.travis.yml index 23533ea632..764897ff66 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,7 @@ before_install: - if [[ "$TRAVIS_PHP_VERSION" = 5.* ]]; then (pecl install -f memcached-2.1.0 && echo "extension = memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini) || echo "Let's continue without memcache extension"; fi; - if [[ "$TRAVIS_PHP_VERSION" = 5.* ]] && [ "$deps" = "no" ]; then (cd src/Symfony/Component/Debug/Resources/ext && phpize && ./configure && make && echo "extension = $(pwd)/modules/symfony_debug.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini); fi; - if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then php -i; fi; + - if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then echo "extension = ldap.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi; - ./phpunit install - export PHPUNIT="$(readlink -f ./phpunit)" diff --git a/appveyor.yml b/appveyor.yml index 2e4d1cce40..069bee46c5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -45,6 +45,7 @@ install: - IF %PHP_EXT%==1 echo extension=php_mbstring.dll >> php.ini - IF %PHP_EXT%==1 echo extension=php_fileinfo.dll >> php.ini - IF %PHP_EXT%==1 echo extension=php_pdo_sqlite.dll >> php.ini + - IF %PHP_EXT%==1 echo extension=php_ldap.dll >> php.ini - cd c:\projects\symfony - php phpunit install - IF %APPVEYOR_REPO_BRANCH%==master (SET COMPOSER_ROOT_VERSION=dev-master) ELSE (SET COMPOSER_ROOT_VERSION=%APPVEYOR_REPO_BRANCH%.x-dev) diff --git a/src/Symfony/Component/Ldap/.gitignore b/src/Symfony/Component/Ldap/.gitignore new file mode 100644 index 0000000000..c49a5d8df5 --- /dev/null +++ b/src/Symfony/Component/Ldap/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Ldap/Exception/ConnectionException.php b/src/Symfony/Component/Ldap/Exception/ConnectionException.php new file mode 100644 index 0000000000..80d9af51ea --- /dev/null +++ b/src/Symfony/Component/Ldap/Exception/ConnectionException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Exception; + +/** + * ConnectionException is throw if binding to ldap can not be established. + * + * @author Grégoire Pineau + */ +class ConnectionException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/Ldap/Exception/LdapException.php b/src/Symfony/Component/Ldap/Exception/LdapException.php new file mode 100644 index 0000000000..213625b021 --- /dev/null +++ b/src/Symfony/Component/Ldap/Exception/LdapException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Exception; + +/** + * LdapException is throw if php ldap module is not loaded. + * + * @author Grégoire Pineau + */ +class LdapException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/Ldap/LICENSE b/src/Symfony/Component/Ldap/LICENSE new file mode 100644 index 0000000000..43028bc600 --- /dev/null +++ b/src/Symfony/Component/Ldap/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2015 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Ldap/LdapClient.php b/src/Symfony/Component/Ldap/LdapClient.php new file mode 100644 index 0000000000..5887a623ad --- /dev/null +++ b/src/Symfony/Component/Ldap/LdapClient.php @@ -0,0 +1,220 @@ + + * @author Francis Besset + * @author Charles Sarrazin + */ +class LdapClient implements LdapClientInterface +{ + private $host; + private $port; + private $version; + private $useSsl; + private $useStartTls; + private $optReferrals; + private $connection; + private $charmaps; + + /** + * Constructor. + * + * @param string $host + * @param int $port + * @param int $version + * @param bool $useSsl + * @param bool $useStartTls + * @param bool $optReferrals + */ + public function __construct($host = null, $port = 389, $version = 3, $useSsl = false, $useStartTls = false, $optReferrals = false) + { + if (!extension_loaded('ldap')) { + throw new LdapException('The ldap module is needed.'); + } + + $this->host = $host; + $this->port = $port; + $this->version = $version; + $this->useSsl = (bool) $useSsl; + $this->useStartTls = (bool) $useStartTls; + $this->optReferrals = (bool) $optReferrals; + } + + public function __destruct() + { + $this->disconnect(); + } + + /** + * {@inheritdoc} + */ + public function bind($dn = null, $password = null) + { + if (!$this->connection) { + $this->connect(); + } + + if (false === @ldap_bind($this->connection, $dn, $password)) { + throw new ConnectionException(ldap_error($this->connection)); + } + } + + /** + * {@inheritdoc} + */ + public function find($dn, $query, $filter = '*') + { + if (!is_array($filter)) { + $filter = array($filter); + } + + $search = ldap_search($this->connection, $dn, $query, $filter); + $infos = ldap_get_entries($this->connection, $search); + + if (0 === $infos['count']) { + return; + } + + return $infos; + } + + /** + * {@inheritdoc} + */ + public function escape($subject, $ignore = '', $flags = 0) + { + if (function_exists('ldap_escape')) { + return ldap_escape($subject, $ignore, $flags); + } + + return $this->doEscape($subject, $ignore, $flags); + } + + private function connect() + { + if (!$this->connection) { + $host = $this->host; + + if ($this->useSsl) { + $host = 'ldaps://'.$host; + } + + ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $this->version); + ldap_set_option($this->connection, LDAP_OPT_REFERRALS, $this->optReferrals); + + $this->connection = ldap_connect($host, $this->port); + + if ($this->useStartTls) { + ldap_start_tls($this->connection); + } + } + } + + private function disconnect() + { + if ($this->connection && is_resource($this->connection)) { + ldap_unbind($this->connection); + } + + $this->connection = null; + } + + /** + * Stub implementation of the {@link ldap_escape()} function of the ldap + * extension. + * + * Escape strings for safe use in LDAP filters and DNs. + * + * @author Chris Wright + * + * @param string $subject + * @param string $ignore + * @param int $flags + * + * @return string + * + * @see http://stackoverflow.com/a/8561604 + */ + private function doEscape($subject, $ignore = '', $flags = 0) + { + $charMaps = $this->getCharmaps(); + + // Create the base char map to escape + $flags = (int) $flags; + $charMap = array(); + + if ($flags & self::LDAP_ESCAPE_FILTER) { + $charMap += $charMaps[self::LDAP_ESCAPE_FILTER]; + } + + if ($flags & self::LDAP_ESCAPE_DN) { + $charMap += $charMaps[self::LDAP_ESCAPE_DN]; + } + + if (!$charMap) { + $charMap = $charMaps[0]; + } + + // Remove any chars to ignore from the list + $ignore = (string) $ignore; + + for ($i = 0, $l = strlen($ignore); $i < $l; ++$i) { + unset($charMap[$ignore[$i]]); + } + + // Do the main replacement + $result = strtr($subject, $charMap); + + // Encode leading/trailing spaces if LDAP_ESCAPE_DN is passed + if ($flags & self::LDAP_ESCAPE_DN) { + if ($result[0] === ' ') { + $result = '\\20'.substr($result, 1); + } + + if ($result[strlen($result) - 1] === ' ') { + $result = substr($result, 0, -1).'\\20'; + } + } + + return $result; + } + + private function getCharmaps() + { + if (null !== $this->charmaps) { + return $this->charmaps; + } + + $charMaps = array( + self::LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"), + self::LDAP_ESCAPE_DN => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'), + ); + + $charMaps[0] = array(); + + for ($i = 0; $i < 256; ++$i) { + $charMaps[0][chr($i)] = sprintf('\\%02x', $i); + } + + for ($i = 0, $l = count($charMaps[self::LDAP_ESCAPE_FILTER]); $i < $l; ++$i) { + $chr = $charMaps[self::LDAP_ESCAPE_FILTER][$i]; + unset($charMaps[self::LDAP_ESCAPE_FILTER][$i]); + $charMaps[self::LDAP_ESCAPE_FILTER][$chr] = $charMaps[0][$chr]; + } + + for ($i = 0, $l = count($charMaps[self::LDAP_ESCAPE_DN]); $i < $l; ++$i) { + $chr = $charMaps[self::LDAP_ESCAPE_DN][$i]; + unset($charMaps[self::LDAP_ESCAPE_DN][$i]); + $charMaps[self::LDAP_ESCAPE_DN][$chr] = $charMaps[0][$chr]; + } + + $this->charmaps = $charMaps; + + return $this->charmaps; + } +} diff --git a/src/Symfony/Component/Ldap/LdapClientInterface.php b/src/Symfony/Component/Ldap/LdapClientInterface.php new file mode 100644 index 0000000000..75e063d339 --- /dev/null +++ b/src/Symfony/Component/Ldap/LdapClientInterface.php @@ -0,0 +1,49 @@ + + * @author Charles Sarrazin + */ +interface LdapClientInterface +{ + const LDAP_ESCAPE_FILTER = 0x01; + const LDAP_ESCAPE_DN = 0x02; + + /** + * Return a connection bound to the ldap. + * + * @param string $dn A LDAP dn + * @param string $password A password + * + * @throws ConnectionException If dn / password could not be bound. + */ + public function bind($dn = null, $password = null); + + /* + * Find a username into ldap connection. + * + * @param string $dn + * @param string $query + * @param mixed $filter + * + * @return array|null + */ + public function find($dn, $query, $filter = '*'); + + /** + * Escape a string for use in an LDAP filter or DN. + * + * @param string $subject + * @param string $ignore + * @param int $flags + * + * @return string + */ + public function escape($subject, $ignore = '', $flags = 0); +} diff --git a/src/Symfony/Component/Ldap/README.md b/src/Symfony/Component/Ldap/README.md new file mode 100644 index 0000000000..751ce6ad25 --- /dev/null +++ b/src/Symfony/Component/Ldap/README.md @@ -0,0 +1,23 @@ +Ldap Component +============= + +A Ldap client for PHP on top of PHP's ldap extension. + +This component also provides a stub for the missing +`ldap_escape` function in PHP versions lower than 5.6. + +Documentation +------------- + +The documentation for the component can be found [online] [0]. + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Ldap/ + $ composer install + $ phpunit + +[0]: https://symfony.com/doc/2.8/components/ldap.html diff --git a/src/Symfony/Component/Ldap/Tests/LdapClientTest.php b/src/Symfony/Component/Ldap/Tests/LdapClientTest.php new file mode 100644 index 0000000000..5b67ea8540 --- /dev/null +++ b/src/Symfony/Component/Ldap/Tests/LdapClientTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; + +use Symfony\Component\Ldap\LdapClient; + +class LdapClientTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!extension_loaded('ldap')) { + $this->markTestSkipped('The ldap extension is not available'); + } + } + + /** + * @dataProvider provideLdapEscapeValues + */ + public function testLdapEscape($subject, $ignore, $flags, $expected) + { + $ldap = new LdapClient(); + $this->assertSame($expected, $ldap->escape($subject, $ignore, $flags)); + } + + /** + * Provides values for the ldap_escape shim. These tests come from the official + * extension. + * + * @see https://github.com/php/php-src/blob/master/ext/ldap/tests/ldap_escape_dn.phpt + * @see https://github.com/php/php-src/blob/master/ext/ldap/tests/ldap_escape_all.phpt + * @see https://github.com/php/php-src/blob/master/ext/ldap/tests/ldap_escape_both.phpt + * @see https://github.com/php/php-src/blob/master/ext/ldap/tests/ldap_escape_filter.phpt + * @see https://github.com/php/php-src/blob/master/ext/ldap/tests/ldap_escape_ignore.phpt + * + * @return array + */ + public function provideLdapEscapeValues() + { + return array( + array('foo=bar(baz)*', null, LdapClient::LDAP_ESCAPE_DN, 'foo\3dbar(baz)*'), + array('foo=bar(baz)*', null, null, '\66\6f\6f\3d\62\61\72\28\62\61\7a\29\2a'), + array('foo=bar(baz)*', null, LdapClient::LDAP_ESCAPE_DN | LdapClient::LDAP_ESCAPE_FILTER, 'foo\3dbar\28baz\29\2a'), + array('foo=bar(baz)*', null, LdapClient::LDAP_ESCAPE_FILTER, 'foo=bar\28baz\29\2a'), + array('foo=bar(baz)*', 'ao', null, '\66oo\3d\62a\72\28\62a\7a\29\2a'), + ); + } +} diff --git a/src/Symfony/Component/Ldap/composer.json b/src/Symfony/Component/Ldap/composer.json new file mode 100644 index 0000000000..5fa6dda3d7 --- /dev/null +++ b/src/Symfony/Component/Ldap/composer.json @@ -0,0 +1,34 @@ +{ + "name": "symfony/ldap", + "type": "library", + "description": "An abstraction in front of PHP's LDAP functions, compatible with PHP 5.3.9 onwards.", + "keywords": ["ldap", "active directory"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Charles Sarrazin", + "email": "charles@sarraz.in" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.9", + "ext-ldap": "*" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Ldap\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + } +} diff --git a/src/Symfony/Component/Ldap/phpunit.xml.dist b/src/Symfony/Component/Ldap/phpunit.xml.dist new file mode 100644 index 0000000000..82f3331146 --- /dev/null +++ b/src/Symfony/Component/Ldap/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + +