feature #17560 [Ldap] Improving the LDAP component (csarrazi)

This PR was merged into the 3.1-dev branch.

Discussion
----------

[Ldap] Improving the LDAP component

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | yes
| Deprecations? | no
| Tests pass?   | no
| Fixed tickets | #14602
| License       | MIT
| Doc PR        | not yet

This PR will address a few issues mentioned in #14602.

* [x] Integrate the Config component in order to simplify the client's configuration
* [x] Separate Connection handling from the Client
* [x] Support for multiple drivers
* [x] Add functional tests
* [x] Update Security component

Commits
-------

34d3c85 Added compatibility layer for previous version of the Security component
81cb79b Improved the Ldap Component
This commit is contained in:
Fabien Potencier 2016-02-14 11:16:17 +01:00
commit 7848a463b2
37 changed files with 1394 additions and 191 deletions

View File

@ -6,6 +6,8 @@ addons:
apt_packages:
- parallel
- language-pack-fr-base
- ldap-utils
- slapd
env:
global:
@ -48,6 +50,11 @@ before_install:
- if [[ $deps != skip ]]; then composer self-update; fi;
- if [[ $deps != skip ]]; then ./phpunit install; fi;
- export PHPUNIT=$(readlink -f ./phpunit)
- mkdir /tmp/slapd
- slapd -f src/Symfony/Component/Ldap/Tests/Fixtures/conf/slapd.conf -h ldap://localhost:3389 &
- sleep 3
- ldapadd -h localhost:3389 -D cn=admin,dc=symfony,dc=com -w symfony -f src/Symfony/Component/Ldap/Tests/Fixtures/data/base.ldif
- ldapadd -h localhost:3389 -D cn=admin,dc=symfony,dc=com -w symfony -f src/Symfony/Component/Ldap/Tests/Fixtures/data/fixtures.ldif
install:
- if [[ $deps != skip ]]; then COMPONENTS=$(find src/Symfony -mindepth 3 -type f -name phpunit.xml.dist -printf '%h\n'); fi;

View File

@ -43,7 +43,6 @@ install:
- IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini-max
- IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini-max
- IF %PHP%==1 echo extension=php_pdo_sqlite.dll >> php.ini-max
- IF %PHP%==1 echo extension=php_ldap.dll >> php.ini-max
- appveyor DownloadFile https://getcomposer.org/composer.phar
- copy /Y php.ini-max php.ini
- cd c:\projects\symfony

View File

@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
abstract class AbstractConnection implements ConnectionInterface
{
protected $config;
public function __construct(array $config = array())
{
$resolver = new OptionsResolver();
$resolver->setDefaults(array(
'host' => null,
'port' => 389,
'version' => 3,
'useSsl' => false,
'useStartTls' => false,
'optReferrals' => false,
));
$resolver->setNormalizer('host', function (Options $options, $value) {
if ($value && $options['useSsl']) {
return 'ldaps://'.$value;
}
return $value;
});
$this->config = $resolver->resolve($config);
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
abstract class AbstractQuery implements QueryInterface
{
protected $connection;
protected $dn;
protected $query;
protected $options;
public function __construct(ConnectionInterface $connection, $dn, $query, array $options = array())
{
$resolver = new OptionsResolver();
$resolver->setDefaults(array(
'filter' => '*',
'maxItems' => 0,
'sizeLimit' => 0,
'timeout' => 0,
'deref' => static::DEREF_NEVER,
'attrsOnly' => 0,
));
$resolver->setAllowedValues('deref', array(static::DEREF_ALWAYS, static::DEREF_NEVER, static::DEREF_FINDING, static::DEREF_SEARCHING));
$resolver->setNormalizer('filter', function (Options $options, $value) {
return is_array($value) ? $value : array($value);
});
$this->connection = $connection;
$this->dn = $dn;
$this->query = $query;
$this->options = $resolver->resolve($options);
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
interface AdapterInterface
{
/**
* Returns the current connection.
*
* @return ConnectionInterface
*/
public function getConnection();
/**
* Creates a new Query.
*
* @param $dn
* @param $query
* @param array $options
*
* @return QueryInterface
*/
public function createQuery($dn, $query, array $options = array());
/**
* 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);
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter;
use Symfony\Component\Ldap\Entry;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
interface CollectionInterface extends \Countable, \IteratorAggregate, \ArrayAccess
{
/**
* @return Entry[]
*/
public function toArray();
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
interface ConnectionInterface
{
/**
* Checks whether the connection was already bound or not.
*
* @return bool
*/
public function isBound();
/**
* Binds the connection against a DN and password.
*
* @param string $dn The user's DN
* @param string $password The associated password
*/
public function bind($dn = null, $password = null);
}

View File

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\AdapterInterface;
use Symfony\Component\Ldap\Exception\LdapException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Adapter implements AdapterInterface
{
private $config;
private $connection;
public function __construct(array $config = array())
{
if (!extension_loaded('ldap')) {
throw new LdapException('The LDAP PHP extension is not enabled.');
}
$this->config = $config;
}
/**
* {@inheritdoc}
*/
public function getConnection()
{
if (null === $this->connection) {
$this->connection = new Connection($this->config);
}
return $this->connection;
}
/**
* {@inheritdoc}
*/
public function createQuery($dn, $query, array $options = array())
{
return new Query($this->getConnection(), $dn, $query, $options);
}
/**
* {@inheritdoc}
*/
public function escape($subject, $ignore = '', $flags = 0)
{
$value = ldap_escape($subject, $ignore, $flags);
// Per RFC 4514, leading/trailing spaces should be encoded in DNs, as well as carriage returns.
if ((int) $flags & LDAP_ESCAPE_DN) {
if (!empty($value) && $value[0] === ' ') {
$value = '\\20'.substr($value, 1);
}
if (!empty($value) && $value[strlen($value) - 1] === ' ') {
$value = substr($value, 0, -1).'\\20';
}
$value = str_replace("\r", '\0d', $value);
}
return $value;
}
}

View File

@ -0,0 +1,108 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\CollectionInterface;
use Symfony\Component\Ldap\Entry;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Collection implements CollectionInterface
{
private $connection;
private $search;
private $entries;
public function __construct(Connection $connection, Query $search, array $entries = array())
{
$this->connection = $connection;
$this->search = $search;
$this->entries = array();
}
/**
* {@inheritdoc}
*/
public function toArray()
{
$this->initialize();
return $this->entries;
}
public function count()
{
$this->initialize();
return count($this->entries);
}
public function getIterator()
{
return new ResultIterator($this->connection, $this->search);
}
public function offsetExists($offset)
{
$this->initialize();
return isset($this->entries[$offset]);
}
public function offsetGet($offset)
{
return isset($this->entries[$offset]) ? $this->entries[$offset] : null;
}
public function offsetSet($offset, $value)
{
$this->initialize();
$this->entries[$offset] = $value;
}
public function offsetUnset($offset)
{
$this->initialize();
unset($this->entries[$offset]);
}
private function initialize()
{
if (null === $this->entries) {
return;
}
$entries = ldap_get_entries($this->connection->getResource(), $this->search->getResource());
if (0 === $entries['count']) {
return array();
}
unset($entries['count']);
$this->entries = array_map(function (array $entry) {
$dn = $entry['dn'];
$attributes = array_diff_key($entry, array_flip(range(0, $entry['count'] - 1)) + array(
'count' => null,
'dn' => null,
));
array_walk($attributes, function (&$value) {
unset($value['count']);
});
return new Entry($dn, $attributes);
}, $entries);
}
}

View File

@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\AbstractConnection;
use Symfony\Component\Ldap\Exception\ConnectionException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Connection extends AbstractConnection
{
/** @var bool */
private $bound = false;
/** @var resource */
private $connection;
public function __destruct()
{
$this->disconnect();
}
public function isBound()
{
return $this->bound;
}
/**
* {@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));
}
$this->bound = true;
}
/**
* Returns a link resource.
*
* @return resource
*/
public function getResource()
{
return $this->connection;
}
private function connect()
{
if ($this->connection) {
return;
}
$host = $this->config['host'];
ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
ldap_set_option($this->connection, LDAP_OPT_REFERRALS, $this->config['optReferrals']);
$this->connection = ldap_connect($host, $this->config['port']);
if ($this->config['useStartTls']) {
ldap_start_tls($this->connection);
}
}
private function disconnect()
{
if ($this->connection && is_resource($this->connection)) {
ldap_close($this->connection);
}
$this->connection = null;
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\AbstractQuery;
use Symfony\Component\Ldap\Exception\LdapException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Query extends AbstractQuery
{
/** @var Connection */
protected $connection;
/** @var resource */
private $search;
public function __construct(Connection $connection, $dn, $query, array $options = array())
{
parent::__construct($connection, $dn, $query, $options);
}
/**
* {@inheritdoc}
*/
public function execute()
{
// If the connection is not bound, then we try an anonymous bind.
if (!$this->connection->isBound()) {
$this->connection->bind();
}
$con = $this->connection->getResource();
$this->search = ldap_search(
$con,
$this->dn,
$this->query,
$this->options['filter'],
$this->options['attrsOnly'],
$this->options['maxItems'],
$this->options['timeout'],
$this->options['deref']
);
if (!$this->search) {
throw new LdapException(sprintf('Could not complete search with dn "%s", query "%s" and filters "%s"', $this->dn, $this->query, implode(',', $this->options['filter'])));
};
return new Collection($this->connection, $this);
}
/**
* Returns a LDAP search resource.
*
* @return resource
*/
public function getResource()
{
return $this->search;
}
}

View File

@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Entry;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class ResultIterator implements \Iterator
{
private $connection;
private $search;
private $current;
private $key;
public function __construct(Connection $connection, Query $search)
{
$this->connection = $connection->getResource();
$this->search = $search->getResource();
}
/**
* Fetches the current entry.
*
* @return Entry
*/
public function current()
{
$attributes = ldap_get_attributes($this->connection, $this->current);
$dn = ldap_get_dn($this->connection, $this->current);
return new Entry($dn, $attributes);
}
/**
* Sets the cursor to the next entry.
*/
public function next()
{
$this->current = ldap_next_entry($this->connection, $this->current);
++$this->key;
}
/**
* Returns the current key.
*
* @return int
*/
public function key()
{
return $this->key;
}
/**
* Checks whether the current entry is valid or not.
*
* @return bool
*/
public function valid()
{
return false !== $this->current;
}
/**
* Rewinds the iterator to the first entry.
*/
public function rewind()
{
$this->current = ldap_first_entry($this->connection, $this->search);
}
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
namespace Symfony\Component\Ldap\Adapter;
use Symfony\Component\Ldap\Entry;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
interface QueryInterface
{
const DEREF_NEVER = 0;
const DEREF_SEARCHING = 1;
const DEREF_FINDING = 2;
const DEREF_ALWAYS = 3;
/**
* Executes a query and returns the list of Ldap entries.
*
* @return CollectionInterface|Entry[]
*/
public function execute();
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap;
use Symfony\Component\Ldap\Exception\ConnectionException;
/**
* Base Ldap interface.
*
* This interface is here for reusability in the BC layer,
* and will be merged in LdapInterface in Symfony 4.0.
*
* @author Charles Sarrazin <charles@sarraz.in>
*
* @internal
*/
interface BaseLdapInterface
{
/**
* 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);
/**
* 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);
}

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Entry
{
private $dn;
private $attributes;
public function __construct($dn, array $attributes = array())
{
$this->dn = $dn;
$this->attributes = $attributes;
}
/**
* Returns the entry's DN.
*
* @return string
*/
public function getDn()
{
return $this->dn;
}
/**
* Returns a specific attribute's value.
*
* As LDAP can return multiple values for a single attribute,
* this value is returned as an array.
*
* @param $name string The name of the attribute
*
* @return null|array
*/
public function getAttribute($name)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : null;
}
/**
* Returns the complete list of attributes.
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
}

View File

@ -15,9 +15,7 @@ namespace Symfony\Component\Ldap\Exception;
* ConnectionException is throw if binding to ldap can not be established.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*
* @internal
*/
class ConnectionException extends \RuntimeException
class ConnectionException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Exception;
/**
* LdapException is throw if php ldap module is not loaded.
*
* @author Charles Sarrazin <charles@sarraz.in>
*/
class DriverNotFoundException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Exception;
/**
* Base ExceptionInterface for the Ldap component.
*
* @author Charles Sarrazin <charles@sarraz.in>
*/
interface ExceptionInterface
{
}

View File

@ -15,9 +15,7 @@ namespace Symfony\Component\Ldap\Exception;
* LdapException is throw if php ldap module is not loaded.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*
* @internal
*/
class LdapException extends \RuntimeException
class LdapException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap;
use Symfony\Component\Ldap\Adapter\AdapterInterface;
use Symfony\Component\Ldap\Exception\DriverNotFoundException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
final class Ldap implements LdapInterface
{
private $adapter;
private static $adapterMap = array(
'ext_ldap' => 'Symfony\Component\Ldap\Adapter\ExtLdap\Adapter',
);
public function __construct(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
/**
* {@inheritdoc}
*/
public function bind($dn = null, $password = null)
{
$this->adapter->getConnection()->bind($dn, $password);
}
/**
* {@inheritdoc}
*/
public function query($dn, $query, array $options = array())
{
return $this->adapter->createQuery($dn, $query, $options);
}
/**
* {@inheritdoc}
*/
public function escape($subject, $ignore = '', $flags = 0)
{
return $this->adapter->escape($subject, $ignore, $flags);
}
/**
* Creates a new Ldap instance.
*
* @param string $adapter The adapter name
* @param array $config The adapter's configuration
*
* @return static
*/
public static function create($adapter, array $config = array())
{
if (!isset(self::$adapterMap[$adapter])) {
throw new DriverNotFoundException(sprintf(
'Adapter "%s" not found. You should use one of: %s',
$adapter,
implode(', ', self::$adapterMap)
));
}
$class = self::$adapterMap[$adapter];
return new self(new $class($config));
}
}

View File

@ -11,53 +11,29 @@
namespace Symfony\Component\Ldap;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\Exception\LdapException;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Francis Besset <francis.besset@gmail.com>
* @author Charles Sarrazin <charles@sarraz.in>
*
* @internal
* @deprecated The LdapClient class will be removed in Symfony 4.0. You should use the Ldap class instead.
*/
class LdapClient implements LdapClientInterface
final class LdapClient implements LdapClientInterface
{
private $host;
private $port;
private $version;
private $useSsl;
private $useStartTls;
private $optReferrals;
private $connection;
private $ldap;
/**
* 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)
public function __construct($host = null, $port = 389, $version = 3, $useSsl = false, $useStartTls = false, $optReferrals = false, LdapInterface $ldap = null)
{
if (!extension_loaded('ldap')) {
throw new LdapException('The ldap module is needed.');
}
$config = array(
'host' => $host,
'port' => $port,
'version' => $version,
'useSsl' => (bool) $useSsl,
'useStartTls' => (bool) $useStartTls,
'optReferrals' => (bool) $optReferrals,
);
$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();
$this->ldap = null !== $ldap ? $ldap : Ldap::create('ext_ldap', $config);
}
/**
@ -65,13 +41,7 @@ class LdapClient implements LdapClientInterface
*/
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));
}
$this->ldap->bind($dn, $password);
}
/**
@ -79,18 +49,30 @@ class LdapClient implements LdapClientInterface
*/
public function find($dn, $query, $filter = '*')
{
if (!is_array($filter)) {
$filter = array($filter);
@trigger_error('The "find" method is deprecated since version 3.1 and will be removed in 4.0. Use the "query" method instead.', E_USER_DEPRECATED);
$query = $this->ldap->query($dn, $query, array('filter' => $filter));
$entries = $query->execute();
$result = array();
foreach ($entries as $entry) {
$resultEntry = array();
foreach ($entry->getAttributes() as $attribute => $values) {
$resultAttribute = $values;
$resultAttribute['count'] = count($values);
$resultEntry[] = $resultAttribute;
$resultEntry[$attribute] = $resultAttribute;
}
$resultEntry['count'] = count($resultEntry) / 2;
$result[] = $resultEntry;
}
$search = ldap_search($this->connection, $dn, $query, $filter);
$infos = ldap_get_entries($this->connection, $search);
$result['count'] = count($result);
if (0 === $infos['count']) {
return;
}
return $infos;
return $result;
}
/**
@ -98,48 +80,6 @@ class LdapClient implements LdapClientInterface
*/
public function escape($subject, $ignore = '', $flags = 0)
{
$value = ldap_escape($subject, $ignore, $flags);
// Per RFC 4514, leading/trailing spaces should be encoded in DNs, as well as carriage returns.
if ((int) $flags & LDAP_ESCAPE_DN) {
if (!empty($value) && $value[0] === ' ') {
$value = '\\20'.substr($value, 1);
}
if (!empty($value) && $value[strlen($value) - 1] === ' ') {
$value = substr($value, 0, -1).'\\20';
}
$value = str_replace("\r", '\0d', $value);
}
return $value;
}
private function connect()
{
if (!$this->connection) {
$host = $this->host;
if ($this->useSsl) {
$host = 'ldaps://'.$host;
}
$this->connection = ldap_connect($host, $this->port);
ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $this->version);
ldap_set_option($this->connection, LDAP_OPT_REFERRALS, $this->optReferrals);
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;
return $this->ldap->escape($subject, $ignore, $flags);
}
}

View File

@ -11,28 +11,18 @@
namespace Symfony\Component\Ldap;
use Symfony\Component\Ldap\Exception\ConnectionException;
/**
* Ldap interface.
*
* This interface is used for the BC layer with branch 2.8 and 3.0.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*
* @internal
* @deprecated You should use LdapInterface instead
*/
interface LdapClientInterface
interface LdapClientInterface extends BaseLdapInterface
{
/**
* 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.
*
@ -43,15 +33,4 @@ interface LdapClientInterface
* @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);
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap;
use Symfony\Component\Ldap\Adapter\QueryInterface;
/**
* Ldap interface.
*
* @author Charles Sarrazin <charles@sarraz.in>
*/
interface LdapInterface extends BaseLdapInterface
{
const ESCAPE_FILTER = 0x01;
const ESCAPE_DN = 0x02;
/**
* Queries a ldap server for entries matching the given criteria.
*
* @param string $dn
* @param string $query
* @param array $options
*
* @return QueryInterface
*/
public function query($dn, $query, array $options = array());
}

View File

@ -6,9 +6,10 @@ A Ldap client for PHP on top of PHP's ldap extension.
Disclaimer
----------
This component is currently marked as internal, as it
still needs some work. Breaking changes will be introduced
in the next minor version of Symfony.
This component is only stable since Symfony 3.1. Earlier versions
have been marked as internal as they still needed some work.
Breaking changes were introduced in Symfony 3.1, so code relying on
previous version of the component will break with this version.
Documentation
-------------
@ -24,4 +25,4 @@ You can run the unit tests with the following command:
$ composer install
$ phpunit
[0]: https://symfony.com/doc/2.8/components/ldap.html
[0]: https://symfony.com/doc/3.1/components/ldap.html

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Tests;
use Symfony\Component\Ldap\Adapter\ExtLdap\Adapter;
use Symfony\Component\Ldap\Adapter\ExtLdap\Collection;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\LdapInterface;
/**
* @requires extension ldap
*/
class AdapterTest extends \PHPUnit_Framework_TestCase
{
public function testLdapEscape()
{
$ldap = new Adapter();
$this->assertEquals('\20foo\3dbar\0d(baz)*\20', $ldap->escape(" foo=bar\r(baz)* ", null, LdapInterface::ESCAPE_DN));
}
/**
* @group functional
*/
public function testLdapQuery()
{
$ldap = new Adapter(array('host' => 'localhost', 'port' => 3389));
$ldap->getConnection()->bind('cn=admin,dc=symfony,dc=com', 'symfony');
$query = $ldap->createQuery('dc=symfony,dc=com', '(&(objectclass=person)(ou=Maintainers))', array());
$result = $query->execute();
$this->assertInstanceOf(Collection::class, $result);
$this->assertCount(1, $result);
$entry = $result[0];
$this->assertInstanceOf(Entry::class, $entry);
$this->assertEquals(array('Fabien Potencier'), $entry->getAttribute('cn'));
$this->assertEquals(array('fabpot@symfony.com', 'fabien@potencier.com'), $entry->getAttribute('mail'));
}
}

View File

@ -0,0 +1,17 @@
# See slapd.conf(5) for details on configuration options.
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
include /etc/ldap/schema/nis.schema
pidfile /tmp/slapd/slapd.pid
argsfile /tmp/slapd/slapd.args
modulepath /usr/lib/openldap
database ldif
directory /tmp/slapd
suffix "dc=symfony,dc=com"
rootdn "cn=admin,dc=symfony,dc=com"
rootpw {SSHA}btWUi971ytYpVMbZLkaQ2A6ETh3VA0lL

View File

@ -0,0 +1,4 @@
dn: dc=symfony,dc=com
objectClass: dcObject
objectClass: organizationalUnit
ou: Organization

View File

@ -0,0 +1,14 @@
dn: cn=Fabien Potencier,dc=symfony,dc=com
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
cn: Fabien Potencier
sn: fabpot
mail: fabpot@symfony.com
mail: fabien@potencier.com
ou: People
ou: Maintainers
ou: Founder
givenName: Fabien Potencier
description: Founder and project lead @Symfony

View File

@ -11,18 +11,160 @@
namespace Symfony\Component\Ldap\Tests;
use Symfony\Component\Ldap\Adapter\CollectionInterface;
use Symfony\Component\Ldap\Adapter\QueryInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\LdapClient;
use Symfony\Polyfill\Php56\Php56 as p;
use Symfony\Component\Ldap\LdapInterface;
/**
* @requires extension ldap
* @group legacy
*/
class LdapClientTest extends \PHPUnit_Framework_TestCase
{
/** @var LdapClient */
private $client;
/** @var \PHPUnit_Framework_MockObject_MockObject */
private $ldap;
protected function setUp()
{
$this->ldap = $this->getMock(LdapInterface::class);
$this->client = new LdapClient(null, 389, 3, false, false, false, $this->ldap);
}
public function testLdapBind()
{
$this->ldap
->expects($this->once())
->method('bind')
->with('foo', 'bar')
;
$this->client->bind('foo', 'bar');
}
public function testLdapEscape()
{
$ldap = new LdapClient();
$this->ldap
->expects($this->once())
->method('escape')
->with('foo', 'bar', 'baz')
;
$this->client->escape('foo', 'bar', 'baz');
}
$this->assertEquals('\20foo\3dbar\0d(baz)*\20', $ldap->escape(" foo=bar\r(baz)* ", null, p::LDAP_ESCAPE_DN));
public function testLdapFind()
{
$collection = $this->getMock(CollectionInterface::class);
$collection
->expects($this->once())
->method('getIterator')
->will($this->returnValue(new \ArrayIterator(array(
new Entry('cn=qux,dc=foo,dc=com', array(
'dn' => array('cn=qux,dc=foo,dc=com'),
'cn' => array('qux'),
'dc' => array('com', 'foo'),
'givenName' => array('Qux'),
)),
new Entry('cn=baz,dc=foo,dc=com', array(
'dn' => array('cn=baz,dc=foo,dc=com'),
'cn' => array('baz'),
'dc' => array('com', 'foo'),
'givenName' => array('Baz'),
)),
))))
;
$query = $this->getMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->will($this->returnValue($collection))
;
$this->ldap
->expects($this->once())
->method('query')
->with('dc=foo,dc=com', 'bar', array('filter' => 'baz'))
->willReturn($query)
;
$expected = array(
'count' => 2,
0 => array(
'count' => 4,
0 => array(
'count' => 1,
0 => 'cn=qux,dc=foo,dc=com',
),
'dn' => array(
'count' => 1,
0 => 'cn=qux,dc=foo,dc=com',
),
1 => array(
'count' => 1,
0 => 'qux',
),
'cn' => array(
'count' => 1,
0 => 'qux',
),
2 => array(
'count' => 2,
0 => 'com',
1 => 'foo',
),
'dc' => array(
'count' => 2,
0 => 'com',
1 => 'foo',
),
3 => array(
'count' => 1,
0 => 'Qux',
),
'givenName' => array(
'count' => 1,
0 => 'Qux',
),
),
1 => array(
'count' => 4,
0 => array(
'count' => 1,
0 => 'cn=baz,dc=foo,dc=com',
),
'dn' => array(
'count' => 1,
0 => 'cn=baz,dc=foo,dc=com',
),
1 => array(
'count' => 1,
0 => 'baz',
),
'cn' => array(
'count' => 1,
0 => 'baz',
),
2 => array(
'count' => 2,
0 => 'com',
1 => 'foo',
),
'dc' => array(
'count' => 2,
0 => 'com',
1 => 'foo',
),
3 => array(
'count' => 1,
0 => 'Baz',
),
'givenName' => array(
'count' => 1,
0 => 'Baz',
),
),
);
$this->assertEquals($expected, $this->client->find('dc=foo,dc=com', 'bar', 'baz'));
}
}

View File

@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Tests;
use Symfony\Component\Ldap\Adapter\AdapterInterface;
use Symfony\Component\Ldap\Adapter\ConnectionInterface;
use Symfony\Component\Ldap\Exception\DriverNotFoundException;
use Symfony\Component\Ldap\Ldap;
class LdapTest extends \PHPUnit_Framework_TestCase
{
/** @var \PHPUnit_Framework_MockObject_MockObject */
private $adapter;
/** @var Ldap */
private $ldap;
protected function setUp()
{
$this->adapter = $this->getMock(AdapterInterface::class);
$this->ldap = new Ldap($this->adapter);
}
public function testLdapBind()
{
$connection = $this->getMock(ConnectionInterface::class);
$connection
->expects($this->once())
->method('bind')
->with('foo', 'bar')
;
$this->adapter
->expects($this->once())
->method('getConnection')
->will($this->returnValue($connection))
;
$this->ldap->bind('foo', 'bar');
}
public function testLdapEscape()
{
$this->adapter
->expects($this->once())
->method('escape')
->with('foo', 'bar', 'baz')
;
$this->ldap->escape('foo', 'bar', 'baz');
}
public function testLdapQuery()
{
$this->adapter
->expects($this->once())
->method('createQuery')
->with('foo', 'bar', array('baz'))
;
$this->ldap->query('foo', 'bar', array('baz'));
}
/**
* @requires extension ldap
*/
public function testLdapCreate()
{
$ldap = Ldap::create('ext_ldap');
$this->assertInstanceOf(Ldap::class, $ldap);
}
public function testCreateWithInvalidAdapterName()
{
$this->setExpectedException(DriverNotFoundException::class);
Ldap::create('foo');
}
}

View File

@ -18,6 +18,7 @@
"require": {
"php": ">=5.5.9",
"symfony/polyfill-php56": "~1.0",
"symfony/options-resolver": "~2.8|~3.0",
"ext-ldap": "*"
},
"autoload": {

View File

@ -17,7 +17,7 @@ use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Ldap\LdapClientInterface;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Ldap\Exception\ConnectionException;
/**
@ -40,11 +40,11 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
* @param UserProviderInterface $userProvider A UserProvider
* @param UserCheckerInterface $userChecker A UserChecker
* @param string $providerKey The provider key
* @param LdapClientInterface $ldap An Ldap client
* @param LdapInterface $ldap A Ldap client
* @param string $dnString A string used to create the bind DN
* @param bool $hideUserNotFoundExceptions Whether to hide user not found exception or not
*/
public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, LdapClientInterface $ldap, $dnString = '{username}', $hideUserNotFoundExceptions = true)
public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, LdapInterface $ldap, $dnString = '{username}', $hideUserNotFoundExceptions = true)
{
parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);
@ -74,7 +74,7 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
$password = $token->getCredentials();
try {
$username = $this->ldap->escape($username, '', LDAP_ESCAPE_DN);
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN);
$dn = str_replace('{username}', $username, $this->dnString);
$this->ldap->bind($dn, $password);

View File

@ -11,10 +11,13 @@
namespace Symfony\Component\Security\Core\Tests\Authentication\Provider;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Security\Core\Authentication\Provider\LdapBindAuthenticationProvider;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* @requires extension ldap
@ -27,14 +30,14 @@ class LdapBindAuthenticationProviderTest extends \PHPUnit_Framework_TestCase
*/
public function testBindFailureShouldThrowAnException()
{
$userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface');
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$userProvider = $this->getMock(UserProviderInterface::class);
$ldap = $this->getMock(LdapInterface::class);
$ldap
->expects($this->once())
->method('bind')
->will($this->throwException(new ConnectionException()))
;
$userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface');
$userChecker = $this->getMock(UserCheckerInterface::class);
$provider = new LdapBindAuthenticationProvider($userProvider, $userChecker, 'key', $ldap);
$reflection = new \ReflectionMethod($provider, 'checkAuthentication');
@ -45,15 +48,15 @@ class LdapBindAuthenticationProviderTest extends \PHPUnit_Framework_TestCase
public function testRetrieveUser()
{
$userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface');
$userProvider = $this->getMock(UserProviderInterface::class);
$userProvider
->expects($this->once())
->method('loadUserByUsername')
->with('foo')
;
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$ldap = $this->getMock(LdapInterface::class);
$userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface');
$userChecker = $this->getMock(UserCheckerInterface::class);
$provider = new LdapBindAuthenticationProvider($userProvider, $userChecker, 'key', $ldap);
$reflection = new \ReflectionMethod($provider, 'retrieveUser');

View File

@ -11,6 +11,10 @@
namespace Symfony\Component\Security\Core\Tests\User;
use Symfony\Component\Ldap\Adapter\CollectionInterface;
use Symfony\Component\Ldap\Adapter\QueryInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Security\Core\User\LdapUserProvider;
use Symfony\Component\Ldap\Exception\ConnectionException;
@ -24,7 +28,7 @@ class LdapUserProviderTest extends \PHPUnit_Framework_TestCase
*/
public function testLoadUserByUsernameFailsIfCantConnectToLdap()
{
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$ldap = $this->getMock(LdapInterface::class);
$ldap
->expects($this->once())
->method('bind')
@ -40,12 +44,29 @@ class LdapUserProviderTest extends \PHPUnit_Framework_TestCase
*/
public function testLoadUserByUsernameFailsIfNoLdapEntries()
{
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$result = $this->getMock(CollectionInterface::class);
$query = $this->getMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->will($this->returnValue($result))
;
$result
->expects($this->once())
->method('count')
->will($this->returnValue(0))
;
$ldap = $this->getMock(LdapInterface::class);
$ldap
->expects($this->once())
->method('escape')
->will($this->returnValue('foo'))
;
$ldap
->expects($this->once())
->method('query')
->will($this->returnValue($query))
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
$provider->loadUserByUsername('foo');
@ -56,7 +77,19 @@ class LdapUserProviderTest extends \PHPUnit_Framework_TestCase
*/
public function testLoadUserByUsernameFailsIfMoreThanOneLdapEntry()
{
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$result = $this->getMock(CollectionInterface::class);
$query = $this->getMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->will($this->returnValue($result))
;
$result
->expects($this->once())
->method('count')
->will($this->returnValue(2))
;
$ldap = $this->getMock(LdapInterface::class);
$ldap
->expects($this->once())
->method('escape')
@ -64,12 +97,8 @@ class LdapUserProviderTest extends \PHPUnit_Framework_TestCase
;
$ldap
->expects($this->once())
->method('find')
->will($this->returnValue(array(
array(),
array(),
'count' => 2,
)))
->method('query')
->will($this->returnValue($query))
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
@ -78,7 +107,29 @@ class LdapUserProviderTest extends \PHPUnit_Framework_TestCase
public function testSuccessfulLoadUserByUsername()
{
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$result = $this->getMock(CollectionInterface::class);
$query = $this->getMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->will($this->returnValue($result))
;
$ldap = $this->getMock(LdapInterface::class);
$result
->expects($this->once())
->method('offsetGet')
->with(0)
->will($this->returnValue(new Entry('foo', array(
'sAMAccountName' => 'foo',
'userpassword' => 'bar',
)
)))
;
$result
->expects($this->once())
->method('count')
->will($this->returnValue(1))
;
$ldap
->expects($this->once())
->method('escape')
@ -86,14 +137,8 @@ class LdapUserProviderTest extends \PHPUnit_Framework_TestCase
;
$ldap
->expects($this->once())
->method('find')
->will($this->returnValue(array(
array(
'sAMAccountName' => 'foo',
'userpassword' => 'bar',
),
'count' => 1,
)))
->method('query')
->will($this->returnValue($query))
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');

View File

@ -11,10 +11,11 @@
namespace Symfony\Component\Security\Core\User;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\LdapClientInterface;
use Symfony\Component\Ldap\LdapInterface;
/**
* LdapUserProvider is a simple user provider on top of ldap.
@ -32,15 +33,15 @@ class LdapUserProvider implements UserProviderInterface
private $defaultSearch;
/**
* @param LdapClientInterface $ldap
* @param string $baseDn
* @param string $searchDn
* @param string $searchPassword
* @param array $defaultRoles
* @param string $uidKey
* @param string $filter
* @param LdapInterface $ldap
* @param string $baseDn
* @param string $searchDn
* @param string $searchPassword
* @param array $defaultRoles
* @param string $uidKey
* @param string $filter
*/
public function __construct(LdapClientInterface $ldap, $baseDn, $searchDn = null, $searchPassword = null, array $defaultRoles = array(), $uidKey = 'sAMAccountName', $filter = '({uid_key}={username})')
public function __construct(LdapInterface $ldap, $baseDn, $searchDn = null, $searchPassword = null, array $defaultRoles = array(), $uidKey = 'sAMAccountName', $filter = '({uid_key}={username})')
{
$this->ldap = $ldap;
$this->baseDn = $baseDn;
@ -57,33 +58,25 @@ class LdapUserProvider implements UserProviderInterface
{
try {
$this->ldap->bind($this->searchDn, $this->searchPassword);
$username = $this->ldap->escape($username, '', LDAP_ESCAPE_FILTER);
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
$query = str_replace('{username}', $username, $this->defaultSearch);
$search = $this->ldap->find($this->baseDn, $query);
$search = $this->ldap->query($this->baseDn, $query);
} catch (ConnectionException $e) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username), 0, $e);
}
if (!$search) {
$entries = $search->execute();
$count = count($entries);
if (!$count) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
if ($search['count'] > 1) {
if ($count > 1) {
throw new UsernameNotFoundException('More than one user found');
}
$user = $search[0];
return $this->loadUser($username, $user);
}
public function loadUser($username, $user)
{
$password = isset($user['userpassword']) ? $user['userpassword'] : null;
$roles = $this->defaultRoles;
return new User($username, $password, $roles);
return $this->loadUser($username, $entries[0]);
}
/**
@ -105,4 +98,9 @@ class LdapUserProvider implements UserProviderInterface
{
return $class === 'Symfony\Component\Security\Core\User\User';
}
private function loadUser($username, Entry $entry)
{
return new User($username, $entry->getAttribute('userpassword'), $this->defaultRoles);
}
}

View File

@ -24,7 +24,7 @@
"symfony/event-dispatcher": "~2.8|~3.0",
"symfony/expression-language": "~2.8|~3.0",
"symfony/http-foundation": "~2.8|~3.0",
"symfony/ldap": "~2.8|~3.0.0",
"symfony/ldap": "~3.1",
"symfony/validator": "~2.8|~3.0",
"psr/log": "~1.0"
},

View File

@ -37,7 +37,7 @@
"symfony/routing": "~2.8|~3.0",
"symfony/validator": "~2.8|~3.0",
"symfony/expression-language": "~2.8|~3.0",
"symfony/ldap": "~2.8|~3.0.0",
"symfony/ldap": "~3.1",
"psr/log": "~1.0"
},
"suggest": {