Merge branch '4.4'

* 4.4:
  fix case
  [Messenger] Removed named parameters and replaced with `?` placeholders for sqlsrv compatibility
  [FrameworkBundle] Detect indirect env vars in routing
  [Form] type cannot be a FormTypeInterface anymore
  [HttpClient] use "idle" instead of "inactivity" when telling about the timeout option
  Create mailBody with only attachments part present
  Remove calls to deprecated function assertAttributeX
  [PhpUnitBridge] make the bridge act as a polyfill for newest PHPUnit features
  [Intl] Order alpha2 to alpha3 mapping
  [Routing] added a warning about the getRouteCollection() method
  Allow sutFqcnResolver to return array
  [Messenger] Fix incompatibility with FrameworkBundle <4.3.1
  Created alias to FlattenException to avoid BC break
  [Ldap] Add security LdapUser and provider
  [HttpFoundation] Revert getClientIp @return docblock
This commit is contained in:
Christian Flothmann 2019-08-05 09:40:44 +02:00
commit 2d594d513b
36 changed files with 746 additions and 200 deletions

View File

@ -149,6 +149,7 @@ Routing
Security
--------
* The `LdapUserProvider` class has been deprecated, use `Symfony\Component\Ldap\Security\LdapUserProvider` instead.
* Implementations of `PasswordEncoderInterface` and `UserPasswordEncoderInterface` should add a new `needsRehash()` method
Stopwatch

View File

@ -377,6 +377,7 @@ Routing
Security
--------
* The `LdapUserProvider` class has been removed, use `Symfony\Component\Ldap\Security\LdapUserProvider` instead.
* Implementations of `PasswordEncoderInterface` and `UserPasswordEncoderInterface` must have a new `needsRehash()` method
* The `Role` and `SwitchUserRole` classes have been removed.
* The `getReachableRoles()` method of the `RoleHierarchy` class has been removed. It has been replaced by the new

View File

@ -6,6 +6,12 @@ CHANGELOG
* removed `weak_vendor` mode, use `max[self]=0` instead
4.4.0
-----
* made the bridge act as a polyfill for newest PHPUnit features
* added `SetUpTearDownTrait` to allow working around the `void` return-type added by PHPUnit 8
4.3.0
-----

View File

@ -76,7 +76,7 @@ class CoverageListenerTrait
$cache = $r->getValue();
$cache = array_replace_recursive($cache, array(
\get_class($test) => array(
'covers' => array($sutFqcn),
'covers' => \is_array($sutFqcn) ? $sutFqcn : array($sutFqcn),
),
));
$r->setValue($testClass, $cache);

View File

@ -1233,7 +1233,7 @@ class Configuration implements ConfigurationInterface
->info('A comma separated list of hosts that do not require a proxy to be reached.')
->end()
->floatNode('timeout')
->info('Defaults to "default_socket_timeout" ini parameter.')
->info('The idle timeout, defaults to the "default_socket_timeout" ini parameter.')
->end()
->scalarNode('bindto')
->info('A network interface name, IP address, a host name or a UNIX socket to bind to.')

View File

@ -82,7 +82,7 @@
</service>
<service id="console.command.messenger_consume_messages" class="Symfony\Component\Messenger\Command\ConsumeMessagesCommand">
<argument type="service" id="messenger.routable_message_bus" />
<argument /> <!-- Routable message bus -->
<argument type="service" id="messenger.receiver_locator" />
<argument type="service" id="logger" on-invalid="null" />
<argument type="collection" /> <!-- Receiver names -->

View File

@ -160,7 +160,7 @@ class Router extends BaseRouter implements WarmableInterface, ServiceSubscriberI
return '%%';
}
if (preg_match('/^env\(\w+\)$/', $match[1])) {
if (preg_match('/^env\((?:\w++:)*+\w++\)$/', $match[1])) {
throw new RuntimeException(sprintf('Using "%%%s%%" is not allowed in routing configuration.', $match[1]));
}
@ -173,7 +173,7 @@ class Router extends BaseRouter implements WarmableInterface, ServiceSubscriberI
if (\is_string($resolved) || is_numeric($resolved)) {
$this->collectedParameters[$match[1]] = $resolved;
return (string) $resolved;
return (string) $this->resolve($resolved);
}
throw new RuntimeException(sprintf('The container parameter "%s", used in the route configuration value "%s", must be a string or numeric, but it is of type %s.', $match[1], $value, \gettype($resolved)));

View File

@ -16,6 +16,7 @@ use Psr\Container\ContainerInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\Config\ContainerParametersResource;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@ -278,13 +279,13 @@ class RouterTest extends TestCase
$routes->add('foo', new Route('/before/%parameter.foo%/after/%%escaped%%'));
$sc = $this->getServiceContainer($routes);
$sc->setParameter('parameter.foo', 'foo');
$sc->setParameter('parameter.foo', 'foo-%%escaped%%');
$router = new Router($sc, 'foo');
$route = $router->getRouteCollection()->get('foo');
$this->assertEquals(
'/before/foo/after/%escaped%',
'/before/foo-%escaped%/after/%escaped%',
$route->getPath()
);
}
@ -313,6 +314,22 @@ class RouterTest extends TestCase
$router->getRouteCollection();
}
public function testIndirectEnvPlaceholders()
{
$routes = new RouteCollection();
$routes->add('foo', new Route('/%foo%'));
$router = new Router($container = $this->getServiceContainer($routes), 'foo');
$container->setParameter('foo', 'foo-%bar%');
$container->setParameter('bar', '%env(string:FOO)%');
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Using "%env(string:FOO)%" is not allowed in routing configuration.');
$router->getRouteCollection();
}
public function testHostPlaceholders()
{
$routes = new RouteCollection();

View File

@ -170,7 +170,7 @@
<deprecated>The "%service_id%" service is deprecated since Symfony 4.1.</deprecated>
</service>
<service id="security.user.provider.ldap" class="Symfony\Component\Security\Core\User\LdapUserProvider" abstract="true">
<service id="security.user.provider.ldap" class="Symfony\Component\Ldap\Security\LdapUserProvider" abstract="true">
<argument /> <!-- security.ldap.ldap -->
<argument /> <!-- base dn -->
<argument /> <!-- search dn -->

View File

@ -51,7 +51,8 @@
"symfony/twig-bundle": "<4.4",
"symfony/var-dumper": "<4.4",
"symfony/framework-bundle": "<4.4",
"symfony/console": "<4.4"
"symfony/console": "<4.4",
"symfony/ldap": "<4.4"
},
"autoload": {
"psr-4": { "Symfony\\Bundle\\SecurityBundle\\": "" },

View File

@ -364,3 +364,18 @@ class FlattenException
return rtrim($message);
}
}
namespace Symfony\Component\Debug\Exception;
if (!class_exists(FlattenException::class, false)) {
class_alias(\Symfony\Component\ErrorRenderer\Exception\FlattenException::class, FlattenException::class);
}
if (false) {
/**
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorRenderer\Exception\FlattenException instead.
*/
class FlattenException extends \Symfony\Component\ErrorRenderer\Exception\FlattenException
{
}
}

View File

@ -61,14 +61,14 @@ class GenericEventTest extends TestCase
public function testSetArguments()
{
$result = $this->event->setArguments(['foo' => 'bar']);
$this->assertAttributeSame(['foo' => 'bar'], 'arguments', $this->event);
$this->assertSame(['foo' => 'bar'], $this->event->getArguments());
$this->assertSame($this->event, $result);
}
public function testSetArgument()
{
$result = $this->event->setArgument('foo2', 'bar2');
$this->assertAttributeSame(['name' => 'Event', 'foo2' => 'bar2'], 'arguments', $this->event);
$this->assertSame(['name' => 'Event', 'foo2' => 'bar2'], $this->event->getArguments());
$this->assertEquals($this->event, $result);
}
@ -97,13 +97,13 @@ class GenericEventTest extends TestCase
public function testOffsetSet()
{
$this->event['foo2'] = 'bar2';
$this->assertAttributeSame(['name' => 'Event', 'foo2' => 'bar2'], 'arguments', $this->event);
$this->assertSame(['name' => 'Event', 'foo2' => 'bar2'], $this->event->getArguments());
}
public function testOffsetUnset()
{
unset($this->event['name']);
$this->assertAttributeSame([], 'arguments', $this->event);
$this->assertSame([], $this->event->getArguments());
}
public function testOffsetIsset()

View File

@ -69,9 +69,6 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
*
* This method should not be invoked.
*
* @param string|FormBuilderInterface $child
* @param string|FormTypeInterface $type
*
* @throws BadMethodCallException
*/
public function add($child, $type = null, array $options = [])
@ -84,10 +81,6 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
*
* This method should not be invoked.
*
* @param string $name
* @param string|FormTypeInterface $type
* @param array $options
*
* @throws BadMethodCallException
*/
public function create($name, $type = null, array $options = [])

View File

@ -848,8 +848,8 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
$child = (string) $child;
if (null !== $type && !\is_string($type) && !$type instanceof FormTypeInterface) {
throw new UnexpectedTypeException($type, 'string or Symfony\Component\Form\FormTypeInterface');
if (null !== $type && !\is_string($type)) {
throw new UnexpectedTypeException($type, 'string or null');
}
// Never initialize child forms automatically

View File

@ -66,8 +66,8 @@ class FormBuilder extends FormConfigBuilder implements \IteratorAggregate, FormB
throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormBuilderInterface');
}
if (null !== $type && !\is_string($type) && !$type instanceof FormTypeInterface) {
throw new UnexpectedTypeException($type, 'string or Symfony\Component\Form\FormTypeInterface');
if (null !== $type && !\is_string($type)) {
throw new UnexpectedTypeException($type, 'string or null');
}
// Add to "children" to maintain order

View File

@ -120,13 +120,6 @@ class FormBuilderTest extends TestCase
$this->assertSame(['foo', 'bar', 'baz'], array_keys($children));
}
public function testAddFormType()
{
$this->assertFalse($this->builder->has('foo'));
$this->builder->add('foo', $this->getMockBuilder('Symfony\Component\Form\FormTypeInterface')->getMock());
$this->assertTrue($this->builder->has('foo'));
}
public function testRemove()
{
$this->builder->add('foo', 'Symfony\Component\Form\Extension\Core\Type\TextType');

View File

@ -30,7 +30,7 @@ class ErrorChunk implements ChunkInterface
{
$this->offset = $offset;
$this->error = $error;
$this->errorMessage = null !== $error ? $error->getMessage() : 'Reading from the response stream reached the inactivity timeout.';
$this->errorMessage = null !== $error ? $error->getMessage() : 'Reading from the response stream reached the idle timeout.';
}
/**

View File

@ -795,7 +795,11 @@ class Request
* being the original client, and each successive proxy that passed the request
* adding the IP address where it received the request from.
*
* @return string|null The client IP address
* If your reverse proxy uses a different header name than "X-Forwarded-For",
* ("Client-Ip" for instance), configure it via the $trustedHeaderSet
* argument of the Request::setTrustedProxies() method instead.
*
* @return string The client IP address
*
* @see getClientIps()
* @see http://en.wikipedia.org/wiki/X-Forwarded-For

View File

@ -317,15 +317,15 @@ class PdoSessionHandlerTest extends TestCase
public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPassword = null)
{
$storage = new PdoSessionHandler($url);
$reflection = new \ReflectionClass(PdoSessionHandler::class);
$this->assertAttributeEquals($expectedDsn, 'dsn', $storage);
if (null !== $expectedUser) {
$this->assertAttributeEquals($expectedUser, 'username', $storage);
}
if (null !== $expectedPassword) {
$this->assertAttributeEquals($expectedPassword, 'password', $storage);
foreach (['dsn' => $expectedDsn, 'username' => $expectedUser, 'password' => $expectedPassword] as $property => $expectedValue) {
if (!isset($expectedValue)) {
continue;
}
$property = $reflection->getProperty($property);
$property->setAccessible(true);
$this->assertSame($expectedValue, $property->getValue($storage));
}
}

View File

@ -205,6 +205,8 @@ class LanguageDataGenerator extends AbstractDataGenerator
}
}
asort($alpha2ToAlpha3);
return $alpha2ToAlpha3;
}
}

View File

@ -622,14 +622,11 @@
"Alpha2ToAlpha3": {
"aa": "aar",
"ab": "abk",
"dz": "dzo",
"af": "afr",
"ak": "aka",
"sq": "sqi",
"am": "amh",
"ar": "ara",
"an": "arg",
"hy": "hye",
"as": "asm",
"av": "ava",
"ae": "ave",
@ -637,7 +634,6 @@
"az": "aze",
"ba": "bak",
"bm": "bam",
"eu": "eus",
"be": "bel",
"bn": "ben",
"bi": "bis",
@ -645,12 +641,10 @@
"bs": "bos",
"br": "bre",
"bg": "bul",
"my": "mya",
"ca": "cat",
"cs": "ces",
"ch": "cha",
"ce": "che",
"zh": "zho",
"cu": "chu",
"cv": "chv",
"kw": "cor",
@ -660,13 +654,12 @@
"da": "dan",
"de": "deu",
"dv": "div",
"mn": "mon",
"nl": "nld",
"et": "est",
"dz": "dzo",
"el": "ell",
"en": "eng",
"eo": "epo",
"ik": "ipk",
"et": "est",
"eu": "eus",
"ee": "ewe",
"fo": "fao",
"fa": "fas",
@ -675,8 +668,6 @@
"fr": "fra",
"fy": "fry",
"ff": "ful",
"om": "orm",
"ka": "kat",
"gd": "gla",
"ga": "gle",
"gl": "glg",
@ -691,31 +682,34 @@
"ho": "hmo",
"hr": "hrv",
"hu": "hun",
"hy": "hye",
"ig": "ibo",
"is": "isl",
"io": "ido",
"ii": "iii",
"iu": "iku",
"ie": "ile",
"ia": "ina",
"id": "ind",
"ik": "ipk",
"is": "isl",
"it": "ita",
"jv": "jav",
"ja": "jpn",
"kl": "kal",
"kn": "kan",
"ks": "kas",
"ka": "kat",
"kr": "kau",
"kk": "kaz",
"km": "khm",
"ki": "kik",
"rw": "kin",
"ky": "kir",
"ku": "kur",
"kg": "kon",
"kv": "kom",
"kg": "kon",
"ko": "kor",
"kj": "kua",
"ku": "kur",
"lo": "lao",
"la": "lat",
"lv": "lav",
@ -725,40 +719,43 @@
"lb": "ltz",
"lu": "lub",
"lg": "lug",
"mk": "mkd",
"mh": "mah",
"ml": "mal",
"mi": "mri",
"mr": "mar",
"ms": "msa",
"mk": "mkd",
"mg": "mlg",
"mt": "mlt",
"ro": "ron",
"mn": "mon",
"mi": "mri",
"ms": "msa",
"my": "mya",
"na": "nau",
"nv": "nav",
"nr": "nbl",
"nd": "nde",
"ng": "ndo",
"ne": "nep",
"nl": "nld",
"nn": "nno",
"nb": "nob",
"ny": "nya",
"oc": "oci",
"oj": "oji",
"or": "ori",
"om": "orm",
"os": "oss",
"pa": "pan",
"ps": "pus",
"pi": "pli",
"pl": "pol",
"pt": "por",
"ps": "pus",
"qu": "que",
"rm": "roh",
"ro": "ron",
"rn": "run",
"ru": "rus",
"sg": "sag",
"sa": "san",
"sr": "srp",
"si": "sin",
"sk": "slk",
"sl": "slv",
@ -769,7 +766,9 @@
"so": "som",
"st": "sot",
"es": "spa",
"sq": "sqi",
"sc": "srd",
"sr": "srp",
"ss": "ssw",
"su": "sun",
"sw": "swa",
@ -799,6 +798,7 @@
"yi": "yid",
"yo": "yor",
"za": "zha",
"zh": "zho",
"zu": "zul"
}
}

View File

@ -0,0 +1,91 @@
<?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\Security;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @final
*/
class LdapUser implements UserInterface
{
private $entry;
private $username;
private $password;
private $roles;
private $extraFields;
public function __construct(Entry $entry, string $username, ?string $password, array $roles = [], array $extraFields = [])
{
if (!$username) {
throw new \InvalidArgumentException('The username cannot be empty.');
}
$this->entry = $entry;
$this->username = $username;
$this->password = $password;
$this->roles = $roles;
$this->extraFields = $extraFields;
}
public function getEntry(): Entry
{
return $this->entry;
}
/**
* {@inheritdoc}
*/
public function getRoles()
{
return $this->roles;
}
/**
* {@inheritdoc}
*/
public function getPassword()
{
return $this->password;
}
/**
* {@inheritdoc}
*/
public function getSalt()
{
}
/**
* {@inheritdoc}
*/
public function getUsername()
{
return $this->username;
}
/**
* {@inheritdoc}
*/
public function eraseCredentials()
{
$this->password = null;
}
public function getExtraFields(): array
{
return $this->extraFields;
}
}

View File

@ -0,0 +1,155 @@
<?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\Security;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* LdapUserProvider is a simple user provider on top of LDAP.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class LdapUserProvider implements UserProviderInterface
{
private $ldap;
private $baseDn;
private $searchDn;
private $searchPassword;
private $defaultRoles;
private $uidKey;
private $defaultSearch;
private $passwordAttribute;
private $extraFields;
public function __construct(LdapInterface $ldap, string $baseDn, string $searchDn = null, string $searchPassword = null, array $defaultRoles = [], string $uidKey = null, string $filter = null, string $passwordAttribute = null, array $extraFields = [])
{
if (null === $uidKey) {
$uidKey = 'sAMAccountName';
}
if (null === $filter) {
$filter = '({uid_key}={username})';
}
$this->ldap = $ldap;
$this->baseDn = $baseDn;
$this->searchDn = $searchDn;
$this->searchPassword = $searchPassword;
$this->defaultRoles = $defaultRoles;
$this->uidKey = $uidKey;
$this->defaultSearch = str_replace('{uid_key}', $uidKey, $filter);
$this->passwordAttribute = $passwordAttribute;
$this->extraFields = $extraFields;
}
/**
* {@inheritdoc}
*/
public function loadUserByUsername($username)
{
try {
$this->ldap->bind($this->searchDn, $this->searchPassword);
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
$query = str_replace('{username}', $username, $this->defaultSearch);
$search = $this->ldap->query($this->baseDn, $query);
} catch (ConnectionException $e) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username), 0, $e);
}
$entries = $search->execute();
$count = \count($entries);
if (!$count) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
if ($count > 1) {
throw new UsernameNotFoundException('More than one user found');
}
$entry = $entries[0];
try {
if (null !== $this->uidKey) {
$username = $this->getAttributeValue($entry, $this->uidKey);
}
} catch (InvalidArgumentException $e) {
}
return $this->loadUser($username, $entry);
}
/**
* {@inheritdoc}
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof LdapUser) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
}
return new LdapUser($user->getEntry(), $user->getUsername(), $user->getPassword(), $user->getRoles());
}
/**
* {@inheritdoc}
*/
public function supportsClass($class)
{
return LdapUser::class === $class;
}
/**
* Loads a user from an LDAP entry.
*
* @return LdapUser
*/
protected function loadUser($username, Entry $entry)
{
$password = null;
$extraFields = [];
if (null !== $this->passwordAttribute) {
$password = $this->getAttributeValue($entry, $this->passwordAttribute);
}
foreach ($this->extraFields as $field) {
$extraFields[$field] = $this->getAttributeValue($entry, $field);
}
return new LdapUser($entry, $username, $password, $this->defaultRoles, $extraFields);
}
private function getAttributeValue(Entry $entry, string $attribute)
{
if (!$entry->hasAttribute($attribute)) {
throw new InvalidArgumentException(sprintf('Missing attribute "%s" for user "%s".', $attribute, $entry->getDn()));
}
$values = $entry->getAttribute($attribute);
if (1 !== \count($values)) {
throw new InvalidArgumentException(sprintf('Attribute "%s" has multiple values.', $attribute));
}
return $values[0];
}
}

View File

@ -0,0 +1,339 @@
<?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\Security\User;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Ldap\Adapter\CollectionInterface;
use Symfony\Component\Ldap\Adapter\QueryInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Ldap\Security\LdapUser;
use Symfony\Component\Ldap\Security\LdapUserProvider;
/**
* @group legacy
* @requires extension ldap
*/
class LdapUserProviderTest extends TestCase
{
/**
* @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
public function testLoadUserByUsernameFailsIfCantConnectToLdap()
{
$ldap = $this->createMock(LdapInterface::class);
$ldap
->expects($this->once())
->method('bind')
->willThrowException(new ConnectionException())
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
$provider->loadUserByUsername('foo');
}
/**
* @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
public function testLoadUserByUsernameFailsIfNoLdapEntries()
{
$result = $this->createMock(CollectionInterface::class);
$query = $this->createMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->willReturn($result)
;
$result
->expects($this->once())
->method('count')
->willReturn(0)
;
$ldap = $this->createMock(LdapInterface::class);
$ldap
->expects($this->once())
->method('escape')
->willReturn('foo')
;
$ldap
->expects($this->once())
->method('query')
->willReturn($query)
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
$provider->loadUserByUsername('foo');
}
/**
* @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
public function testLoadUserByUsernameFailsIfMoreThanOneLdapEntry()
{
$result = $this->createMock(CollectionInterface::class);
$query = $this->createMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->willReturn($result)
;
$result
->expects($this->once())
->method('count')
->willReturn(2)
;
$ldap = $this->createMock(LdapInterface::class);
$ldap
->expects($this->once())
->method('escape')
->willReturn('foo')
;
$ldap
->expects($this->once())
->method('query')
->willReturn($query)
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
$provider->loadUserByUsername('foo');
}
/**
* @expectedException \Symfony\Component\Security\Core\Exception\InvalidArgumentException
*/
public function testLoadUserByUsernameFailsIfMoreThanOneLdapPasswordsInEntry()
{
$result = $this->createMock(CollectionInterface::class);
$query = $this->createMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->willReturn($result)
;
$ldap = $this->createMock(LdapInterface::class);
$result
->expects($this->once())
->method('offsetGet')
->with(0)
->willReturn(new Entry('foo', [
'sAMAccountName' => ['foo'],
'userpassword' => ['bar', 'baz'],
]))
;
$result
->expects($this->once())
->method('count')
->willReturn(1)
;
$ldap
->expects($this->once())
->method('escape')
->willReturn('foo')
;
$ldap
->expects($this->once())
->method('query')
->willReturn($query)
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com', null, null, [], 'sAMAccountName', '({uid_key}={username})', 'userpassword');
$this->assertInstanceOf(LdapUser::class, $provider->loadUserByUsername('foo'));
}
public function testLoadUserByUsernameShouldNotFailIfEntryHasNoUidKeyAttribute()
{
$result = $this->createMock(CollectionInterface::class);
$query = $this->createMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->willReturn($result)
;
$ldap = $this->createMock(LdapInterface::class);
$result
->expects($this->once())
->method('offsetGet')
->with(0)
->willReturn(new Entry('foo', []))
;
$result
->expects($this->once())
->method('count')
->willReturn(1)
;
$ldap
->expects($this->once())
->method('escape')
->willReturn('foo')
;
$ldap
->expects($this->once())
->method('query')
->willReturn($query)
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com', null, null, [], 'sAMAccountName', '({uid_key}={username})');
$this->assertInstanceOf(LdapUser::class, $provider->loadUserByUsername('foo'));
}
/**
* @expectedException \Symfony\Component\Security\Core\Exception\InvalidArgumentException
*/
public function testLoadUserByUsernameFailsIfEntryHasNoPasswordAttribute()
{
$result = $this->createMock(CollectionInterface::class);
$query = $this->createMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->willReturn($result)
;
$ldap = $this->createMock(LdapInterface::class);
$result
->expects($this->once())
->method('offsetGet')
->with(0)
->willReturn(new Entry('foo', ['sAMAccountName' => ['foo']]))
;
$result
->expects($this->once())
->method('count')
->willReturn(1)
;
$ldap
->expects($this->once())
->method('escape')
->willReturn('foo')
;
$ldap
->expects($this->once())
->method('query')
->willReturn($query)
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com', null, null, [], 'sAMAccountName', '({uid_key}={username})', 'userpassword');
$this->assertInstanceOf(LdapUser::class, $provider->loadUserByUsername('foo'));
}
public function testLoadUserByUsernameIsSuccessfulWithoutPasswordAttribute()
{
$result = $this->createMock(CollectionInterface::class);
$query = $this->createMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->willReturn($result)
;
$ldap = $this->createMock(LdapInterface::class);
$result
->expects($this->once())
->method('offsetGet')
->with(0)
->willReturn(new Entry('foo', ['sAMAccountName' => ['foo']]))
;
$result
->expects($this->once())
->method('count')
->willReturn(1)
;
$ldap
->expects($this->once())
->method('escape')
->willReturn('foo')
;
$ldap
->expects($this->once())
->method('query')
->willReturn($query)
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
$this->assertInstanceOf(LdapUser::class, $provider->loadUserByUsername('foo'));
}
public function testLoadUserByUsernameIsSuccessfulWithoutPasswordAttributeAndWrongCase()
{
$result = $this->createMock(CollectionInterface::class);
$query = $this->createMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->willReturn($result)
;
$ldap = $this->createMock(LdapInterface::class);
$result
->expects($this->once())
->method('offsetGet')
->with(0)
->willReturn(new Entry('foo', ['sAMAccountName' => ['foo']]))
;
$result
->expects($this->once())
->method('count')
->willReturn(1)
;
$ldap
->expects($this->once())
->method('escape')
->willReturn('Foo')
;
$ldap
->expects($this->once())
->method('query')
->willReturn($query)
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
$this->assertSame('foo', $provider->loadUserByUsername('Foo')->getUsername());
}
public function testLoadUserByUsernameIsSuccessfulWithPasswordAttribute()
{
$result = $this->createMock(CollectionInterface::class);
$query = $this->createMock(QueryInterface::class);
$query
->expects($this->once())
->method('execute')
->willReturn($result)
;
$ldap = $this->createMock(LdapInterface::class);
$result
->expects($this->once())
->method('offsetGet')
->with(0)
->willReturn(new Entry('foo', [
'sAMAccountName' => ['foo'],
'userpassword' => ['bar'],
'email' => ['elsa@symfony.com'],
]))
;
$result
->expects($this->once())
->method('count')
->willReturn(1)
;
$ldap
->expects($this->once())
->method('escape')
->willReturn('foo')
;
$ldap
->expects($this->once())
->method('query')
->willReturn($query)
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com', null, null, [], 'sAMAccountName', '({uid_key}={username})', 'userpassword', ['email']);
$this->assertInstanceOf(LdapUser::class, $provider->loadUserByUsername('foo'));
}
}

View File

@ -20,6 +20,9 @@
"symfony/options-resolver": "^4.4|^5.0",
"ext-ldap": "*"
},
"require-dev": {
"symfony/security-core": "^4.4"
},
"conflict": {
"symfony/options-resolver": "<4.4"
},

View File

@ -251,14 +251,19 @@ class MessengerPass implements CompilerPassInterface
$buses[$busId] = new Reference($busId);
}
if ($container->hasDefinition('messenger.routable_message_bus')) {
if ($hasRoutableMessageBus = $container->hasDefinition('messenger.routable_message_bus')) {
$container->getDefinition('messenger.routable_message_bus')
->replaceArgument(0, ServiceLocatorTagPass::register($container, $buses));
}
if ($container->hasDefinition('console.command.messenger_consume_messages')) {
$container->getDefinition('console.command.messenger_consume_messages')
->replaceArgument(3, array_values($receiverNames));
$consumeCommandDefinition = $container->getDefinition('console.command.messenger_consume_messages');
if ($hasRoutableMessageBus) {
$consumeCommandDefinition->replaceArgument(0, new Reference('messenger.routable_message_bus'));
}
$consumeCommandDefinition->replaceArgument(3, array_values($receiverNames));
}
if ($container->hasDefinition('console.command.messenger_setup_transports')) {

View File

@ -109,19 +109,19 @@ class Connection
$queryBuilder = $this->driverConnection->createQueryBuilder()
->insert($this->configuration['table_name'])
->values([
'body' => ':body',
'headers' => ':headers',
'queue_name' => ':queue_name',
'created_at' => ':created_at',
'available_at' => ':available_at',
'body' => '?',
'headers' => '?',
'queue_name' => '?',
'created_at' => '?',
'available_at' => '?',
]);
$this->executeQuery($queryBuilder->getSQL(), [
':body' => $body,
':headers' => json_encode($headers),
':queue_name' => $this->configuration['queue_name'],
':created_at' => self::formatDateTime($now),
':available_at' => self::formatDateTime($availableAt),
$body,
json_encode($headers),
$this->configuration['queue_name'],
self::formatDateTime($now),
self::formatDateTime($availableAt),
]);
return $this->driverConnection->lastInsertId();
@ -154,12 +154,12 @@ class Connection
$queryBuilder = $this->driverConnection->createQueryBuilder()
->update($this->configuration['table_name'])
->set('delivered_at', ':delivered_at')
->where('id = :id');
->set('delivered_at', '?')
->where('id = ?');
$now = new \DateTime();
$this->executeQuery($queryBuilder->getSQL(), [
':id' => $doctrineEnvelope['id'],
':delivered_at' => self::formatDateTime($now),
self::formatDateTime($now),
$doctrineEnvelope['id'],
]);
$this->driverConnection->commit();
@ -247,10 +247,10 @@ class Connection
}
$queryBuilder = $this->createQueryBuilder()
->where('m.id = :id');
->where('m.id = ?');
$data = $this->executeQuery($queryBuilder->getSQL(), [
'id' => $id,
$id,
])->fetch();
return false === $data ? null : $this->decodeEnvelopeHeaders($data);
@ -262,13 +262,13 @@ class Connection
$redeliverLimit = (clone $now)->modify(sprintf('-%d seconds', $this->configuration['redeliver_timeout']));
return $this->createQueryBuilder()
->where('m.delivered_at is null OR m.delivered_at < :redeliver_limit')
->andWhere('m.available_at <= :now')
->andWhere('m.queue_name = :queue_name')
->where('m.delivered_at is null OR m.delivered_at < ?')
->andWhere('m.available_at <= ?')
->andWhere('m.queue_name = ?')
->setParameters([
':now' => self::formatDateTime($now),
':queue_name' => $this->configuration['queue_name'],
':redeliver_limit' => self::formatDateTime($redeliverLimit),
self::formatDateTime($redeliverLimit),
self::formatDateTime($now),
$this->configuration['queue_name'],
]);
}

View File

@ -421,12 +421,12 @@ class Email extends Message
*/
private function generateBody(): AbstractPart
{
if (null === $this->text && null === $this->html) {
throw new LogicException('A message must have a text and/or an HTML part.');
[$htmlPart, $attachmentParts, $inlineParts] = $this->prepareParts();
if (null === $this->text && null === $this->html && !$attachmentParts) {
throw new LogicException('A message must have a text or an HTML part or attachments.');
}
$part = null === $this->text ? null : new TextPart($this->text, $this->textCharset);
[$htmlPart, $attachmentParts, $inlineParts] = $this->prepareParts();
if (null !== $htmlPart) {
if (null !== $part) {
$part = new AlternativePart($part, $htmlPart);
@ -440,7 +440,11 @@ class Email extends Message
}
if ($attachmentParts) {
$part = new MixedPart($part, ...$attachmentParts);
if ($part) {
$part = new MixedPart($part, ...$attachmentParts);
} else {
$part = new MixedPart(...$attachmentParts);
}
}
return $part;

View File

@ -284,6 +284,10 @@ class EmailTest extends TestCase
$e->html('html content');
$this->assertEquals(new MixedPart($html, $att), $e->getBody());
$e = new Email();
$e->attach($file);
$this->assertEquals(new MixedPart($att), $e->getBody());
$e = new Email();
$e->html('html content');
$e->text('text content');

View File

@ -26,6 +26,9 @@ interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface
/**
* Gets the RouteCollection instance associated with this Router.
*
* WARNING: This method should never be used at runtime as it is SLOW.
* You might use it in a cache warmer though.
*
* @return RouteCollection A RouteCollection instance
*/
public function getRouteCollection();

View File

@ -32,6 +32,7 @@ CHANGELOG
4.4.0
-----
* Deprecated class `LdapUserProvider`, use `Symfony\Component\Ldap\Security\LdapUserProvider` instead
* Added method `needsRehash()` to `PasswordEncoderInterface` and `UserPasswordEncoderInterface`
* Added `MigratingPasswordEncoder`

View File

@ -20,6 +20,7 @@ use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Security\Core\User\LdapUserProvider;
/**
* @group legacy
* @requires extension ldap
*/
class LdapUserProviderTest extends TestCase

View File

@ -11,89 +11,22 @@
namespace Symfony\Component\Security\Core\User;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', LdapUserProvider::class, BaseLdapUserProvider::class), E_USER_DEPRECATED);
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Ldap\Security\LdapUserProvider as BaseLdapUserProvider;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
/**
* LdapUserProvider is a simple user provider on top of ldap.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*
* @deprecated since Symfony 4.4, use "Symfony\Component\Ldap\Security\LdapUserProvider" instead
*/
class LdapUserProvider implements UserProviderInterface
class LdapUserProvider extends BaseLdapUserProvider
{
private $ldap;
private $baseDn;
private $searchDn;
private $searchPassword;
private $defaultRoles;
private $uidKey;
private $defaultSearch;
private $passwordAttribute;
private $extraFields;
public function __construct(LdapInterface $ldap, string $baseDn, string $searchDn = null, string $searchPassword = null, array $defaultRoles = [], string $uidKey = null, string $filter = null, string $passwordAttribute = null, array $extraFields = [])
{
if (null === $uidKey) {
$uidKey = 'sAMAccountName';
}
if (null === $filter) {
$filter = '({uid_key}={username})';
}
$this->ldap = $ldap;
$this->baseDn = $baseDn;
$this->searchDn = $searchDn;
$this->searchPassword = $searchPassword;
$this->defaultRoles = $defaultRoles;
$this->uidKey = $uidKey;
$this->defaultSearch = str_replace('{uid_key}', $uidKey, $filter);
$this->passwordAttribute = $passwordAttribute;
$this->extraFields = $extraFields;
}
/**
* {@inheritdoc}
*/
public function loadUserByUsername(string $username)
{
try {
$this->ldap->bind($this->searchDn, $this->searchPassword);
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
$query = str_replace('{username}', $username, $this->defaultSearch);
$search = $this->ldap->query($this->baseDn, $query);
} catch (ConnectionException $e) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username), 0, $e);
}
$entries = $search->execute();
$count = \count($entries);
if (!$count) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
if ($count > 1) {
throw new UsernameNotFoundException('More than one user found');
}
$entry = $entries[0];
try {
if (null !== $this->uidKey) {
$username = $this->getAttributeValue($entry, $this->uidKey);
}
} catch (InvalidArgumentException $e) {
}
return $this->loadUser($username, $entry);
}
/**
* {@inheritdoc}
*/
@ -121,35 +54,8 @@ class LdapUserProvider implements UserProviderInterface
*/
protected function loadUser(string $username, Entry $entry)
{
$password = null;
$extraFields = [];
$ldapUser = parent::loadUser($username, $entry);
if (null !== $this->passwordAttribute) {
$password = $this->getAttributeValue($entry, $this->passwordAttribute);
}
foreach ($this->extraFields as $field) {
$extraFields[$field] = $this->getAttributeValue($entry, $field);
}
return new User($username, $password, $this->defaultRoles, true, true, true, true, $extraFields);
}
/**
* Fetches a required unique attribute value from an LDAP entry.
*/
private function getAttributeValue(Entry $entry, string $attribute)
{
if (!$entry->hasAttribute($attribute)) {
throw new InvalidArgumentException(sprintf('Missing attribute "%s" for user "%s".', $attribute, $entry->getDn()));
}
$values = $entry->getAttribute($attribute);
if (1 !== \count($values)) {
throw new InvalidArgumentException(sprintf('Attribute "%s" has multiple values.', $attribute));
}
return $values[0];
return new User($ldapUser->getUsername(), $ldapUser->getPassword(), $ldapUser->getRoles(), true, true, true, true, $ldapUser->getExtraFields());
}
}

View File

@ -31,7 +31,8 @@
},
"conflict": {
"symfony/event-dispatcher": "<4.4",
"symfony/security-guard": "<4.4"
"symfony/security-guard": "<4.4",
"symfony/ldap": "<4.4"
},
"suggest": {
"psr/container-implementation": "To instantiate the Security class",

View File

@ -27,7 +27,7 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
interface ChunkInterface
{
/**
* Tells when the inactivity timeout has been reached.
* Tells when the idle timeout has been reached.
*
* @throws TransportExceptionInterface on a network error
*/
@ -36,21 +36,21 @@ interface ChunkInterface
/**
* Tells when headers just arrived.
*
* @throws TransportExceptionInterface on a network error or when the inactivity timeout is reached
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function isFirst(): bool;
/**
* Tells when the body just completed.
*
* @throws TransportExceptionInterface on a network error or when the inactivity timeout is reached
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function isLast(): bool;
/**
* Returns the content of the response chunk.
*
* @throws TransportExceptionInterface on a network error or when the inactivity timeout is reached
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function getContent(): string;

View File

@ -52,7 +52,7 @@ interface HttpClientInterface
'resolve' => [], // string[] - a map of host to IP address that SHOULD replace DNS resolution
'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored
'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached
'timeout' => null, // float - the inactivity timeout - defaults to ini_get('default_socket_timeout')
'timeout' => null, // float - the idle timeout - defaults to ini_get('default_socket_timeout')
'bindto' => '0', // string - the interface or the local socket to bind to
'verify_peer' => true, // see https://php.net/context.ssl for the following options
'verify_host' => true,
@ -85,7 +85,7 @@ interface HttpClientInterface
* Yields responses chunk by chunk as they complete.
*
* @param ResponseInterface|ResponseInterface[]|iterable $responses One or more responses created by the current HTTP client
* @param float|null $timeout The inactivity timeout before exiting the iterator
* @param float|null $timeout The idle timeout before yielding timeout chunks
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface;
}