Merge branch '4.4'

* 4.4:
  sync phpunit script with master
  [HttpFoundation] allow additinal characters in not raw cookies
  [Console] Deprecate abbreviating hidden command names using  Application->find()
  Do not include hidden commands in suggested alternatives
  [Messenger] Improve error message when routing to an invalid transport (closes #31613)
  [DependencyInjection] Fix wrong exception when service is synthetic
  [Security] add "anonymous: lazy" mode to firewalls
This commit is contained in:
Nicolas Grekas 2019-09-28 18:14:00 +02:00
commit 10be999069
41 changed files with 538 additions and 48 deletions

View File

@ -6,6 +6,11 @@ Cache
* Added argument `$prefix` to `AdapterInterface::clear()`
Console
-------
* Deprecated finding hidden commands using an abbreviation, use the full name instead
Debug
-----

View File

@ -31,6 +31,7 @@ Config
Console
-------
* Removed support for finding hidden commands using an abbreviation, use the full name instead
* Removed the `setCrossingChar()` method in favor of the `setDefaultCrossingChar()` method in `TableStyle`.
* Removed the `setHorizontalBorderChar()` method in favor of the `setDefaultCrossingChars()` method in `TableStyle`.
* Removed the `getHorizontalBorderChar()` method in favor of the `getBorderChars()` method in `TableStyle`.

View File

@ -9,6 +9,9 @@ if (!file_exists(__DIR__.'/vendor/symfony/phpunit-bridge/bin/simple-phpunit')) {
}
if (!getenv('SYMFONY_PHPUNIT_VERSION')) {
if (\PHP_VERSION_ID >= 70200) {
if (false === getenv('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT') && false !== strpos(@file_get_contents(__DIR__.'/src/Symfony/Component/HttpKernel/Kernel.php'), 'const MAJOR_VERSION = 3;')) {
putenv('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1');
}
putenv('SYMFONY_PHPUNIT_VERSION=8.3');
} elseif (\PHP_VERSION_ID >= 70000) {
putenv('SYMFONY_PHPUNIT_VERSION=6.5');

View File

@ -1609,6 +1609,16 @@ class FrameworkExtension extends Extension
}
}
$senderReferences = [];
// alias => service_id
foreach ($senderAliases as $alias => $serviceId) {
$senderReferences[$alias] = new Reference($serviceId);
}
// service_id => service_id
foreach ($senderAliases as $serviceId) {
$senderReferences[$serviceId] = new Reference($serviceId);
}
$messageToSendersMapping = [];
foreach ($config['routing'] as $message => $messageConfiguration) {
if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) {
@ -1617,19 +1627,14 @@ class FrameworkExtension extends Extension
// make sure senderAliases contains all senders
foreach ($messageConfiguration['senders'] as $sender) {
if (!isset($senderAliases[$sender])) {
$senderAliases[$sender] = $sender;
if (!isset($senderReferences[$sender])) {
throw new LogicException(sprintf('Invalid Messenger routing configuration: the "%s" class is being routed to a sender called "%s". This is not a valid transport or service id.', $message, $sender));
}
}
$messageToSendersMapping[$message] = $messageConfiguration['senders'];
}
$senderReferences = [];
foreach ($senderAliases as $alias => $serviceId) {
$senderReferences[$alias] = new Reference($serviceId);
}
$container->getDefinition('messenger.senders_locator')
->replaceArgument(0, $messageToSendersMapping)
->replaceArgument(1, ServiceLocatorTagPass::register($container, $senderReferences))

View File

@ -9,5 +9,10 @@ $container->loadFromExtension('framework', [
FooMessage::class => ['sender.bar', 'sender.biz'],
BarMessage::class => 'sender.foo',
],
'transports' => [
'sender.biz' => 'null://',
'sender.bar' => 'null://',
'sender.foo' => 'null://',
],
],
]);

View File

@ -7,7 +7,7 @@ $container->loadFromExtension('framework', [
'default_serializer' => 'messenger.transport.symfony_serializer',
],
'routing' => [
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage' => ['amqp', 'audit'],
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage' => ['amqp', 'messenger.transport.audit'],
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\SecondMessage' => [
'senders' => ['amqp', 'audit'],
],
@ -15,6 +15,7 @@ $container->loadFromExtension('framework', [
],
'transports' => [
'amqp' => 'amqp://localhost/%2f/messages',
'audit' => 'null://',
],
],
]);

View File

@ -0,0 +1,16 @@
<?php
$container->loadFromExtension('framework', [
'serializer' => true,
'messenger' => [
'serializer' => [
'default_serializer' => 'messenger.transport.symfony_serializer',
],
'routing' => [
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage' => 'invalid',
],
'transports' => [
'amqp' => 'amqp://localhost/%2f/messages',
],
],
]);

View File

@ -14,6 +14,9 @@
<framework:routing message-class="Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage">
<framework:sender service="sender.foo" />
</framework:routing>
<framework:transport name="sender.bar" dsn="null://" />
<framework:transport name="sender.biz" dsn="null://" />
<framework:transport name="sender.foo" dsn="null://" />
</framework:messenger>
</framework:config>
</container>

View File

@ -11,7 +11,7 @@
<framework:serializer default-serializer="messenger.transport.symfony_serializer" />
<framework:routing message-class="Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage">
<framework:sender service="amqp" />
<framework:sender service="audit" />
<framework:sender service="messenger.transport.audit" />
</framework:routing>
<framework:routing message-class="Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\SecondMessage">
<framework:sender service="amqp" />
@ -21,6 +21,7 @@
<framework:sender service="amqp" />
</framework:routing>
<framework:transport name="amqp" dsn="amqp://localhost/%2f/messages" />
<framework:transport name="audit" dsn="null://" />
</framework:messenger>
</framework:config>
</container>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:serializer enabled="true" />
<framework:messenger>
<framework:serializer default-serializer="messenger.transport.symfony_serializer" />
<framework:routing message-class="Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage">
<framework:sender service="invalid" />
</framework:routing>
<framework:routing message-class="Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\SecondMessage">
<framework:sender service="amqp" />
<framework:sender service="audit" />
</framework:routing>
<framework:transport name="amqp" dsn="amqp://localhost/%2f/messages" />
</framework:messenger>
</framework:config>
</container>

View File

@ -3,3 +3,7 @@ framework:
routing:
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage': ['sender.bar', 'sender.biz']
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage': 'sender.foo'
transports:
sender.biz: 'null://'
sender.bar: 'null://'
sender.foo: 'null://'

View File

@ -4,9 +4,10 @@ framework:
serializer:
default_serializer: messenger.transport.symfony_serializer
routing:
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage': [amqp, audit]
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage': [amqp, messenger.transport.audit]
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\SecondMessage':
senders: [amqp, audit]
'*': amqp
transports:
amqp: 'amqp://localhost/%2f/messages'
audit: 'null://'

View File

@ -0,0 +1,9 @@
framework:
serializer: true
messenger:
serializer:
default_serializer: messenger.transport.symfony_serializer
routing:
'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage': invalid
transports:
amqp: 'amqp://localhost/%2f/messages'

View File

@ -570,8 +570,8 @@ abstract class FrameworkExtensionTest extends TestCase
$container = $this->createContainerFromFile('messenger');
$this->assertTrue($container->hasAlias('messenger.default_bus'));
$this->assertTrue($container->getAlias('messenger.default_bus')->isPublic());
$this->assertFalse($container->hasDefinition('messenger.transport.amqp.factory'));
$this->assertFalse($container->hasDefinition('messenger.transport.redis.factory'));
$this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory'));
$this->assertTrue($container->hasDefinition('messenger.transport.redis.factory'));
$this->assertTrue($container->hasDefinition('messenger.transport_factory'));
$this->assertSame(TransportFactory::class, $container->getDefinition('messenger.transport_factory')->getClass());
}
@ -614,14 +614,11 @@ abstract class FrameworkExtensionTest extends TestCase
$senderLocatorDefinition = $container->getDefinition('messenger.senders_locator');
$sendersMapping = $senderLocatorDefinition->getArgument(0);
$this->assertEquals([
'amqp',
'audit',
], $sendersMapping[DummyMessage::class]);
$this->assertEquals(['amqp', 'messenger.transport.audit'], $sendersMapping[DummyMessage::class]);
$sendersLocator = $container->getDefinition((string) $senderLocatorDefinition->getArgument(1));
$this->assertSame(['amqp', 'audit'], array_keys($sendersLocator->getArgument(0)));
$this->assertSame(['amqp', 'audit', 'messenger.transport.amqp', 'messenger.transport.audit'], array_keys($sendersLocator->getArgument(0)));
$this->assertEquals(new Reference('messenger.transport.amqp'), $sendersLocator->getArgument(0)['amqp']->getValues()[0]);
$this->assertEquals(new Reference('audit'), $sendersLocator->getArgument(0)['audit']->getValues()[0]);
$this->assertEquals(new Reference('messenger.transport.audit'), $sendersLocator->getArgument(0)['messenger.transport.audit']->getValues()[0]);
}
public function testMessengerTransportConfiguration()
@ -676,6 +673,13 @@ abstract class FrameworkExtensionTest extends TestCase
$this->createContainerFromFile('messenger_middleware_factory_erroneous_format');
}
public function testMessengerInvalidTransportRouting()
{
$this->expectException('LogicException');
$this->expectExceptionMessage('Invalid Messenger routing configuration: the "Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage" class is being routed to a sender called "invalid". This is not a valid transport or service id.');
$this->createContainerFromFile('messenger_routing_invalid_transport');
}
public function testTranslator()
{
$container = $this->createContainerFromFile('full');

View File

@ -24,6 +24,7 @@ CHANGELOG
4.3.0
-----
* Added `anonymous: lazy` mode to firewalls to make them (not) start the session as late as possible
* Added new encoder types: `auto` (recommended), `native` and `sodium`
* The normalization of the cookie names configured in the `logout.delete_cookies`
option is deprecated and will be disabled in Symfony 5.0. This affects to cookies

View File

@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
@ -107,7 +108,7 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn
$logoutUrl = null;
try {
if (null !== $this->logoutUrlGenerator) {
if (null !== $this->logoutUrlGenerator && !$token instanceof AnonymousToken) {
$logoutUrl = $this->logoutUrlGenerator->getLogoutPath();
}
} catch (\Exception $e) {

View File

@ -55,7 +55,12 @@ class AnonymousFactory implements SecurityFactoryInterface
public function addConfiguration(NodeDefinition $builder)
{
$builder
->beforeNormalization()
->ifTrue(function ($v) { return 'lazy' === $v; })
->then(function ($v) { return ['lazy' => true]; })
->end()
->children()
->booleanNode('lazy')->defaultFalse()->end()
->scalarNode('secret')->defaultNull()->end()
->end()
;

View File

@ -237,7 +237,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId);
$contextId = 'security.firewall.map.context.'.$name;
$context = $container->setDefinition($contextId, new ChildDefinition('security.firewall.context'));
$context = new ChildDefinition($firewall['stateless'] || empty($firewall['anonymous']['lazy']) ? 'security.firewall.context' : 'security.firewall.lazy_context');
$context = $container->setDefinition($contextId, $context);
$context
->replaceArgument(0, new IteratorArgument($listeners))
->replaceArgument(1, $exceptionListener)
@ -403,7 +404,9 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
}
// Access listener
$listeners[] = new Reference('security.access_listener');
if ($firewall['stateless'] || empty($firewall['anonymous']['lazy'])) {
$listeners[] = new Reference('security.access_listener');
}
// Exception listener
$exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless']));

View File

@ -146,6 +146,16 @@
<argument /> <!-- FirewallConfig -->
</service>
<service id="security.firewall.lazy_context" class="Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext" abstract="true">
<argument type="collection" />
<argument type="service" id="security.exception_listener" />
<argument /> <!-- LogoutListener -->
<argument /> <!-- FirewallConfig -->
<argument type="service" id="security.access_listener" />
<argument type="service" id="security.untracked_token_storage" />
<argument type="service" id="security.access_map" />
</service>
<service id="security.firewall.config" class="Symfony\Bundle\SecurityBundle\Security\FirewallConfig" abstract="true">
<argument /> <!-- name -->
<argument /> <!-- user_checker -->

View File

@ -0,0 +1,73 @@
<?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\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Exception\LazyResponseException;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
use Symfony\Component\Security\Http\Firewall\AccessListener;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
/**
* Lazily calls authentication listeners when actually required by the access listener.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class LazyFirewallContext extends FirewallContext
{
private $accessListener;
private $tokenStorage;
private $map;
public function __construct(iterable $listeners, ?ExceptionListener $exceptionListener, ?LogoutListener $logoutListener, ?FirewallConfig $config, AccessListener $accessListener, TokenStorage $tokenStorage, AccessMapInterface $map)
{
parent::__construct($listeners, $exceptionListener, $logoutListener, $config);
$this->accessListener = $accessListener;
$this->tokenStorage = $tokenStorage;
$this->map = $map;
}
public function getListeners(): iterable
{
return [$this];
}
public function __invoke(RequestEvent $event)
{
$this->tokenStorage->setInitializer(function () use ($event) {
$event = new LazyResponseEvent($event);
foreach (parent::getListeners() as $listener) {
if (\is_callable($listener)) {
$listener($event);
} else {
@trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, implement "__invoke()" instead.', \get_class($listener)), E_USER_DEPRECATED);
$listener->handle($event);
}
}
});
try {
[$attributes] = $this->map->getPatterns($event->getRequest());
if ($attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes) {
($this->accessListener)($event);
}
} catch (LazyResponseException $e) {
$event->setResponse($e->getResponse());
}
}
}

View File

@ -59,6 +59,6 @@ class LocalizedController implements ContainerAwareInterface
public function homepageAction()
{
return new Response('<html><body>Homepage</body></html>');
return (new Response('<html><body>Homepage</body></html>'))->setPublic();
}
}

View File

@ -129,6 +129,16 @@ class SecurityRoutingIntegrationTest extends AbstractWebTestCase
$client->request('GET', '/unprotected_resource');
}
public function testPublicHomepage()
{
$client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'config.yml']);
$client->request('GET', '/en/');
$this->assertEquals(200, $client->getResponse()->getStatusCode(), (string) $client->getResponse());
$this->assertTrue($client->getResponse()->headers->getCacheControlDirective('public'));
$this->assertSame(0, self::$container->get('session')->getUsageIndex());
}
private function assertAllowed($client, $path)
{
$client->request('GET', $path);

View File

@ -27,7 +27,7 @@ security:
check_path: /login_check
default_target_path: /profile
logout: ~
anonymous: ~
anonymous: lazy
# This firewall is here just to check its the logout functionality
second_area:
@ -38,6 +38,7 @@ security:
path: /second/logout
access_control:
- { path: ^/en/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/unprotected_resource$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/secure-but-not-covered-by-access-control$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY }

View File

@ -536,6 +536,10 @@ class Application implements ResetInterface
{
$namespaces = [];
foreach ($this->all() as $command) {
if ($command->isHidden()) {
continue;
}
$namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
foreach ($command->getAliases() as $alias) {
@ -629,6 +633,11 @@ class Application implements ResetInterface
$message = sprintf('Command "%s" is not defined.', $name);
if ($alternatives = $this->findAlternatives($name, $allCommands)) {
// remove hidden commands
$alternatives = array_filter($alternatives, function ($name) {
return !$this->get($name)->isHidden();
});
if (1 == \count($alternatives)) {
$message .= "\n\nDid you mean this?\n ";
} else {
@ -637,7 +646,7 @@ class Application implements ResetInterface
$message .= implode("\n ", $alternatives);
}
throw new CommandNotFoundException($message, $alternatives);
throw new CommandNotFoundException($message, array_values($alternatives));
}
// filter out aliases for commands which are already on the list
@ -658,20 +667,36 @@ class Application implements ResetInterface
foreach ($abbrevs as $abbrev) {
$maxLen = max(Helper::strlen($abbrev), $maxLen);
}
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) {
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
if (!$commandList[$cmd] instanceof Command) {
return $cmd;
$commandList[$cmd] = $this->commandLoader->get($cmd);
}
if ($commandList[$cmd]->isHidden()) {
unset($commands[array_search($cmd, $commands)]);
return false;
}
$abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
}, array_values($commands));
$suggestions = $this->getAbbreviationSuggestions($abbrevs);
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands));
if (\count($commands) > 1) {
$suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands));
}
}
return $this->get(reset($commands));
$command = $this->get(reset($commands));
if ($command->isHidden()) {
@trigger_error(sprintf('Command "%s" is hidden, finding it using an abbreviation is deprecated since Symfony 4.4, use its full name instead.', $command->getName()), E_USER_DEPRECATED);
}
return $command;
}
/**

View File

@ -16,6 +16,7 @@ CHANGELOG
4.4.0
-----
* deprecated finding hidden commands using an abbreviation, use the full name instead
* added `Question::setTrimmable` default to true to allow the answer to be trimmed
* added method `preventRedrawFasterThan()` and `forceRedrawSlowerThan()` on `ProgressBar`
* `Application` implements `ResetInterface`

View File

@ -75,6 +75,8 @@ class ApplicationTest extends TestCase
require_once self::$fixturesPath.'/FooWithoutAliasCommand.php';
require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering.php';
require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering2.php';
require_once self::$fixturesPath.'/FooHiddenCommand.php';
require_once self::$fixturesPath.'/BarHiddenCommand.php';
}
protected function normalizeLineBreaks($text)
@ -440,6 +442,16 @@ class ApplicationTest extends TestCase
];
}
public function testFindWithAmbiguousAbbreviationsFindsCommandIfAlternativesAreHidden()
{
$application = new Application();
$application->add(new \FooCommand());
$application->add(new \FooHiddenCommand());
$this->assertInstanceOf('FooCommand', $application->find('foo:'));
}
public function testFindCommandEqualNamespace()
{
$application = new Application();
@ -664,6 +676,7 @@ class ApplicationTest extends TestCase
$application->add(new \Foo1Command());
$application->add(new \Foo2Command());
$application->add(new \Foo3Command());
$application->add(new \FooHiddenCommand());
$expectedAlternatives = [
'afoobar',
@ -706,6 +719,49 @@ class ApplicationTest extends TestCase
$application->find('foo::bar');
}
public function testFindHiddenWithExactName()
{
$application = new Application();
$application->add(new \FooHiddenCommand());
$this->assertInstanceOf('FooHiddenCommand', $application->find('foo:hidden'));
$this->assertInstanceOf('FooHiddenCommand', $application->find('afoohidden'));
}
/**
* @group legacy
* @expectedDeprecation Command "%s:hidden" is hidden, finding it using an abbreviation is deprecated since Symfony 4.4, use its full name instead.
* @dataProvider provideAbbreviationsForHiddenCommands
*/
public function testFindHiddenWithAbbreviatedName($name)
{
$application = new Application();
$application->add(new \FooHiddenCommand());
$application->add(new \BarHiddenCommand());
$application->find($name);
}
public function provideAbbreviationsForHiddenCommands()
{
return [
['foo:hidde'],
['afoohidd'],
['bar:hidde'],
];
}
public function testFindAmbiguousCommandsIfAllAlternativesAreHidden()
{
$application = new Application();
$application->add(new \FooCommand());
$application->add(new \FooHiddenCommand());
$this->assertInstanceOf('FooCommand', $application->find('foo:'));
}
public function testSetCatchExceptions()
{
$application = new Application();

View File

@ -0,0 +1,21 @@
<?php
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class BarHiddenCommand extends Command
{
protected function configure()
{
$this
->setName('bar:hidden')
->setAliases(['abarhidden'])
->setHidden(true)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
}
}

View File

@ -0,0 +1,21 @@
<?php
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class FooHiddenCommand extends Command
{
protected function configure()
{
$this
->setName('foo:hidden')
->setAliases(['afoohidden'])
->setHidden(true)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
}
}

View File

@ -110,6 +110,10 @@ abstract class AbstractRecursivePass implements CompilerPassInterface
*/
protected function getConstructor(Definition $definition, bool $required)
{
if ($definition->isSynthetic()) {
return null;
}
if (\is_string($factory = $definition->getFactory())) {
if (!\function_exists($factory)) {
throw new RuntimeException(sprintf('Invalid service "%s": function "%s" does not exist.', $this->currentId, $factory));

View File

@ -568,12 +568,10 @@ class AutowirePassTest extends TestCase
);
}
/**
* @exceptedExceptionMessage Invalid service "Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy": method "setLogger()" does not exist.
*/
public function testWithNonExistingSetterAndAutowiring()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Invalid service "Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass": method "setLogger()" does not exist.');
$container = new ContainerBuilder();
$definition = $container->register(CaseSensitiveClass::class, CaseSensitiveClass::class)->setAutowired(true);

View File

@ -15,9 +15,11 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass;
use Symfony\Component\DependencyInjection\Compiler\DefinitionErrorExceptionPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
@ -131,4 +133,25 @@ class ResolveBindingsPassTest extends TestCase
$pass = new ResolveBindingsPass();
$pass->process($container);
}
public function testSyntheticServiceWithBind()
{
$container = new ContainerBuilder();
$argument = new BoundArgument('bar');
$container->register('foo', 'stdClass')
->addArgument(new Reference('synthetic.service'));
$container->register('synthetic.service')
->setSynthetic(true)
->setBindings(['$apiKey' => $argument]);
$container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class)
->setBindings(['$apiKey' => $argument]);
(new ResolveBindingsPass())->process($container);
(new DefinitionErrorExceptionPass())->process($container);
$this->assertSame([1 => 'bar'], $container->getDefinition(NamedArgumentsDummy::class)->getArguments());
}
}

View File

@ -18,6 +18,10 @@ namespace Symfony\Component\HttpFoundation;
*/
class Cookie
{
const SAMESITE_NONE = 'none';
const SAMESITE_LAX = 'lax';
const SAMESITE_STRICT = 'strict';
protected $name;
protected $value;
protected $domain;
@ -25,13 +29,14 @@ class Cookie
protected $path;
protected $secure;
protected $httpOnly;
private $raw;
private $sameSite;
private $secureDefault = false;
const SAMESITE_NONE = 'none';
const SAMESITE_LAX = 'lax';
const SAMESITE_STRICT = 'strict';
private static $reservedCharsList = "=,; \t\r\n\v\f";
private static $reservedCharsFrom = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"];
private static $reservedCharsTo = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C'];
/**
* Creates cookie from raw header string.
@ -86,7 +91,7 @@ class Cookie
public function __construct(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = 'lax')
{
// from PHP source code
if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
if ($raw && false !== strpbrk($name, self::$reservedCharsList)) {
throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
}
@ -134,7 +139,13 @@ class Cookie
*/
public function __toString()
{
$str = ($this->isRaw() ? $this->getName() : urlencode($this->getName())).'=';
if ($this->isRaw()) {
$str = $this->getName();
} else {
$str = str_replace(self::$reservedCharsFrom, self::$reservedCharsTo, $this->getName());
}
$str .= '=';
if ('' === (string) $this->getValue()) {
$str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0';

View File

@ -335,7 +335,7 @@ class Response
// cookies
foreach ($this->headers->getCookies() as $cookie) {
header('Set-Cookie: '.$cookie->getName().strstr($cookie, '='), false, $this->statusCode);
header('Set-Cookie: '.$cookie, false, $this->statusCode);
}
// status

View File

@ -24,10 +24,9 @@ use Symfony\Component\HttpFoundation\Cookie;
*/
class CookieTest extends TestCase
{
public function invalidNames()
public function namesWithSpecialCharacters()
{
return [
[''],
[',MyName'],
[';MyName'],
[' MyName'],
@ -40,12 +39,26 @@ class CookieTest extends TestCase
}
/**
* @dataProvider invalidNames
* @dataProvider namesWithSpecialCharacters
*/
public function testInstantiationThrowsExceptionIfCookieNameContainsInvalidCharacters($name)
public function testInstantiationThrowsExceptionIfRawCookieNameContainsSpecialCharacters($name)
{
$this->expectException('InvalidArgumentException');
Cookie::create($name);
Cookie::create($name, null, 0, null, null, null, false, true);
}
/**
* @dataProvider namesWithSpecialCharacters
*/
public function testInstantiationSucceedNonRawCookieNameContainsSpecialCharacters($name)
{
$this->assertInstanceOf(Cookie::class, Cookie::create($name));
}
public function testInstantiationThrowsExceptionIfCookieNameIsEmpty()
{
$this->expectException('InvalidArgumentException');
Cookie::create('');
}
public function testInvalidExpiration()

View File

@ -4,7 +4,8 @@ Array
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: no-cache, private
[2] => Date: Sat, 12 Nov 1955 20:04:00 GMT
[3] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/
[3] => Set-Cookie: %3D%2C%3B%20%09%0D%0A%0B%0C=%3D%2C%3B%20%09%0D%0A%0B%0C; path=/
[4] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/
[5] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/
)
shutdown

View File

@ -4,9 +4,12 @@ use Symfony\Component\HttpFoundation\Cookie;
$r = require __DIR__.'/common.inc';
$str = '?*():@&+$/%#[]';
$str1 = "=,; \t\r\n\v\f";
$r->headers->setCookie(new Cookie($str1, $str1, 0, '', null, false, false, false, null));
$r->headers->setCookie(new Cookie($str, $str, 0, '', null, false, false, false, null));
$str2 = '?*():@&+$/%#[]';
$r->headers->setCookie(new Cookie($str2, $str2, 0, '', null, false, false, false, null));
$r->sendHeaders();
setcookie($str, $str, 0, '/');
setcookie($str2, $str2, 0, '/');

View File

@ -5,7 +5,7 @@ use Symfony\Component\HttpFoundation\Cookie;
$r = require __DIR__.'/common.inc';
try {
$r->headers->setCookie(Cookie::create('Hello + world', 'hodor'));
$r->headers->setCookie(new Cookie('Hello + world', 'hodor', 0, null, null, null, false, true));
} catch (\InvalidArgumentException $e) {
echo $e->getMessage();
}

View File

@ -25,12 +25,18 @@ use Symfony\Contracts\Service\ResetInterface;
class TokenStorage implements TokenStorageInterface, ResetInterface
{
private $token;
private $initializer;
/**
* {@inheritdoc}
*/
public function getToken()
{
if ($initializer = $this->initializer) {
$this->initializer = null;
$initializer();
}
return $this->token;
}
@ -39,9 +45,15 @@ class TokenStorage implements TokenStorageInterface, ResetInterface
*/
public function setToken(TokenInterface $token = null)
{
$this->initializer = null;
$this->token = $token;
}
public function setInitializer(?callable $initializer): void
{
$this->initializer = $initializer;
}
public function reset()
{
$this->setToken(null);

View File

@ -0,0 +1,34 @@
<?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\Security\Core\Exception;
use Symfony\Component\HttpFoundation\Response;
/**
* A signaling exception that wraps a lazily computed response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class LazyResponseException extends \Exception implements ExceptionInterface
{
private $response;
public function __construct(Response $response)
{
$this->response = $response;
}
public function getResponse(): Response
{
return $this->response;
}
}

View File

@ -0,0 +1,76 @@
<?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\Security\Http\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Exception\LazyResponseException;
/**
* Wraps a lazily computed response in a signaling exception.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class LazyResponseEvent extends RequestEvent
{
private $event;
public function __construct(parent $event)
{
$this->event = $event;
}
/**
* {@inheritdoc}
*/
public function setResponse(Response $response)
{
$this->stopPropagation();
$this->event->stopPropagation();
throw new LazyResponseException($response);
}
/**
* {@inheritdoc}
*/
public function getKernel(): HttpKernelInterface
{
return $this->event->getKernel();
}
/**
* {@inheritdoc}
*/
public function getRequest(): Request
{
return $this->event->getRequest();
}
/**
* {@inheritdoc}
*/
public function getRequestType(): int
{
return $this->event->getRequestType();
}
/**
* {@inheritdoc}
*/
public function isMasterRequest(): bool
{
return $this->event->isMasterRequest();
}
}

View File

@ -26,6 +26,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException;
use Symfony\Component\Security\Core\Exception\LazyResponseException;
use Symfony\Component\Security\Core\Exception\LogoutException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
@ -103,6 +104,12 @@ class ExceptionListener
return;
}
if ($exception instanceof LazyResponseException) {
$event->setResponse($exception->getResponse());
return;
}
if ($exception instanceof LogoutException) {
$this->handleLogoutException($exception);