Merge branch '3.4' into 4.0

* 3.4:
  [HttpFoundation] Add Session::isEmpty(), fix MockFileSessionStorage to behave like the native one
  [HttpKernel] Add a better error messages when passing a private or non-tagged controller
  [VarDumper] Dont use empty(), it chokes on eg GMP objects
  [Dotenv] Changed preg_match flags from null to 0
  remove upgrade instructions for kernel.root_dir
  [HttpKernel] Arrays with scalar values passed to ESI fragment renderer throw deprecation notice
  [HttpKernel] add a test for FilterControllerEvents
This commit is contained in:
Nicolas Grekas 2017-11-30 16:11:43 +01:00
commit 32dfdb30c6
17 changed files with 259 additions and 24 deletions

View File

@ -598,12 +598,6 @@ HttpKernel
tags: ['console.command']
```
* Removed the `kernel.root_dir` parameter. Use the `kernel.project_dir` parameter
instead.
* Removed the `Kernel::getRootDir()` method. Use the `Kernel::getProjectDir()`
method instead.
* The `Extension::addClassesToCompile()` and `Extension::getClassesToCompile()` methods have been removed.
* Possibility to pass non-scalar values as URI attributes to the ESI and SSI

View File

@ -29,7 +29,7 @@ abstract class AbstractFactory implements SecurityFactoryInterface
protected $options = array(
'check_path' => '/login_check',
'use_forward' => false,
'require_previous_session' => true,
'require_previous_session' => false,
);
protected $defaultSuccessHandlerOptions = array(

View File

@ -28,7 +28,6 @@ class JsonLoginFactory extends AbstractFactory
$this->addOption('password_path', 'password');
$this->defaultFailureHandlerOptions = array();
$this->defaultSuccessHandlerOptions = array();
$this->options['require_previous_session'] = false;
}
/**

View File

@ -174,7 +174,7 @@ final class Dotenv
private function lexValue()
{
if (preg_match('/[ \t]*+(?:#.*)?$/Am', $this->data, $matches, null, $this->cursor)) {
if (preg_match('/[ \t]*+(?:#.*)?$/Am', $this->data, $matches, 0, $this->cursor)) {
$this->moveCursor($matches[0]);
$this->skipEmptyLines();
@ -296,7 +296,7 @@ final class Dotenv
private function skipEmptyLines()
{
if (preg_match('/(?:\s*+(?:#[^\n]*+)?+)++/A', $this->data, $match, null, $this->cursor)) {
if (preg_match('/(?:\s*+(?:#[^\n]*+)?+)++/A', $this->data, $match, 0, $this->cursor)) {
$this->moveCursor($match[0]);
}
}

View File

@ -28,6 +28,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
private $flashName;
private $attributeName;
private $data = array();
/**
* @param SessionStorageInterface $storage A SessionStorageInterface instance
@ -108,7 +109,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/
public function clear()
{
$this->storage->getBag($this->attributeName)->clear();
$this->getAttributeBag()->clear();
}
/**
@ -139,6 +140,22 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
return count($this->getAttributeBag()->all());
}
/**
* @return bool
*
* @internal
*/
public function isEmpty()
{
foreach ($this->data as &$data) {
if (!empty($data)) {
return false;
}
}
return true;
}
/**
* {@inheritdoc}
*/
@ -210,7 +227,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/
public function registerBag(SessionBagInterface $bag)
{
$this->storage->registerBag($bag);
$this->storage->registerBag(new SessionBagProxy($bag, $this->data));
}
/**
@ -218,7 +235,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/
public function getBag($name)
{
return $this->storage->getBag($name);
return $this->storage->getBag($name)->getBag();
}
/**
@ -240,6 +257,6 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable
*/
private function getAttributeBag()
{
return $this->storage->getBag($this->attributeName);
return $this->storage->getBag($this->attributeName)->getBag();
}
}

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\HttpFoundation\Session;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class SessionBagProxy implements SessionBagInterface
{
private $bag;
private $data;
public function __construct(SessionBagInterface $bag, array &$data)
{
$this->bag = $bag;
$this->data = &$data;
}
/**
* @return SessionBagInterface
*/
public function getBag()
{
return $this->bag;
}
/**
* @return bool
*/
public function isEmpty()
{
return empty($this->data[$this->bag->getStorageKey()]);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->bag->getName();
}
/**
* {@inheritdoc}
*/
public function initialize(array &$array)
{
$this->data[$this->bag->getStorageKey()] = &$array;
$this->bag->initialize($array);
}
/**
* {@inheritdoc}
*/
public function getStorageKey()
{
return $this->bag->getStorageKey();
}
/**
* {@inheritdoc}
*/
public function clear()
{
return $this->bag->clear();
}
}

View File

@ -91,7 +91,26 @@ class MockFileSessionStorage extends MockArraySessionStorage
throw new \RuntimeException('Trying to save a session that was not started yet or was already closed');
}
file_put_contents($this->getFilePath(), serialize($this->data));
$data = $this->data;
foreach ($this->bags as $bag) {
if (empty($data[$key = $bag->getStorageKey()])) {
unset($data[$key]);
}
}
if (array($key = $this->metadataBag->getStorageKey()) === array_keys($data)) {
unset($data[$key]);
}
try {
if ($data) {
file_put_contents($this->getFilePath(), serialize($data));
} else {
$this->destroy();
}
} finally {
$this->data = $data;
}
// this is needed for Silex, where the session object is re-used across requests
// in functional tests. In Symfony, the container is rebooted, so we don't have

View File

@ -221,4 +221,22 @@ class SessionTest extends TestCase
{
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\Storage\MetadataBag', $this->session->getMetadataBag());
}
public function testIsEmpty()
{
$this->assertTrue($this->session->isEmpty());
$this->session->set('hello', 'world');
$this->assertFalse($this->session->isEmpty());
$this->session->remove('hello');
$this->assertTrue($this->session->isEmpty());
$flash = $this->session->getFlashBag();
$flash->set('hello', 'world');
$this->assertFalse($this->session->isEmpty());
$flash->get('hello');
$this->assertTrue($this->session->isEmpty());
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\HttpKernel\Controller;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpFoundation\Request;
/**
@ -86,6 +87,15 @@ class ContainerControllerResolver extends ControllerResolver
return $this->container->get($class);
}
return parent::instantiateController($class);
try {
return parent::instantiateController($class);
} catch (\ArgumentCountError $e) {
}
if ($this->container instanceof Container && in_array($class, $this->container->getRemovedIds(), true)) {
throw new \LogicException(sprintf('Controller "%s" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?', $class), 0, $e);
}
throw $e;
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\HttpKernel\EventListener;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
@ -60,8 +61,10 @@ abstract class AbstractTestSessionListener implements EventSubscriberInterface
$session = $event->getRequest()->getSession();
if ($session && $session->isStarted()) {
$session->save();
$params = session_get_cookie_params();
$event->getResponse()->headers->setCookie(new Cookie($session->getName(), $session->getId(), 0 === $params['lifetime'] ? 0 : time() + $params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly']));
if (!$session instanceof Session || !\method_exists($session, 'isEmpty') || !$session->isEmpty()) {
$params = session_get_cookie_params();
$event->getResponse()->headers->setCookie(new Cookie($session->getName(), $session->getId(), 0 === $params['lifetime'] ? 0 : time() + $params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly']));
}
}
}

View File

@ -98,8 +98,8 @@ abstract class AbstractSurrogateFragmentRenderer extends RoutableFragmentRendere
private function containsNonScalars(array $values): bool
{
foreach ($values as $value) {
if (is_array($value) && $this->containsNonScalars($value)) {
return true;
if (is_array($value)) {
return $this->containsNonScalars($value);
} elseif (!is_scalar($value) && null !== $value) {
return true;
}

View File

@ -13,6 +13,8 @@ namespace Symfony\Component\HttpKernel\Tests\Controller;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ContainerControllerResolver;
@ -106,6 +108,38 @@ class ContainerControllerResolverTest extends ControllerResolverTest
$this->assertSame(array(NonInstantiableController::class, 'action'), $controller);
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage Controller "Symfony\Component\HttpKernel\Tests\Controller\ImpossibleConstructController" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?
*/
public function testNonConstructController()
{
$container = $this->getMockBuilder(Container::class)->getMock();
$container->expects($this->at(0))
->method('has')
->with(ImpossibleConstructController::class)
->will($this->returnValue(true))
;
$container->expects($this->at(1))
->method('has')
->with(ImpossibleConstructController::class)
->will($this->returnValue(false))
;
$container->expects($this->atLeastOnce())
->method('getRemovedIds')
->with()
->will($this->returnValue(array(ImpossibleConstructController::class)))
;
$resolver = $this->createControllerResolver(null, $container);
$request = Request::create('/');
$request->attributes->set('_controller', array(ImpossibleConstructController::class, 'action'));
$resolver->getController($request);
}
public function testNonInstantiableControllerWithCorrespondingService()
{
$service = new \stdClass();
@ -196,3 +230,14 @@ abstract class NonInstantiableController
{
}
}
class ImpossibleConstructController
{
public function __construct($toto, $controller)
{
}
public function action()
{
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Symfony\Component\HttpKernel\Tests\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\Tests\TestHttpKernel;
class FilterControllerArgumentsEventTest extends TestCase
{
public function testFilterControllerArgumentsEvent()
{
$filterController = new FilterControllerArgumentsEvent(new TestHttpKernel(), function () {}, array('test'), new Request(), 1);
$this->assertEquals($filterController->getArguments(), array('test'));
}
}

View File

@ -73,6 +73,19 @@ class TestSessionListenerTest extends TestCase
$this->assertEquals(0, reset($cookies)->getExpiresTime());
}
/**
* @requires function \Symfony\Component\HttpFoundation\Session\Session::isEmpty
*/
public function testEmptySessionDoesNotSendCookie()
{
$this->sessionHasBeenStarted();
$this->sessionIsEmpty();
$response = $this->filterResponse(new Request(), HttpKernelInterface::MASTER_REQUEST);
$this->assertSame(array(), $response->headers->getCookies());
}
public function testUnstartedSessionIsNotSave()
{
$this->sessionHasNotBeenStarted();
@ -130,6 +143,13 @@ class TestSessionListenerTest extends TestCase
->will($this->returnValue(false));
}
private function sessionIsEmpty()
{
$this->session->expects($this->once())
->method('isEmpty')
->will($this->returnValue(true));
}
private function getSession()
{
$mock = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\Session')

View File

@ -26,6 +26,14 @@ class EsiFragmentRendererTest extends TestCase
$strategy->render('/', Request::create('/'));
}
public function testRenderFallbackWithScalar()
{
$strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy(true), new UriSigner('foo'));
$request = Request::create('/');
$reference = new ControllerReference('main_controller', array('foo' => array(true)), array());
$strategy->render($reference, $request);
}
public function testRender()
{
$strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy());

View File

@ -113,8 +113,8 @@ class Caster
if (null === $v) {
$type |= self::EXCLUDE_NULL & $filter;
}
if (empty($v)) {
$type |= self::EXCLUDE_EMPTY & $filter;
} elseif (false === $v || '' === $v || '0' === $v || 0 === $v || 0.0 === $v || array() === $v) {
$type |= self::EXCLUDE_EMPTY & $filter;
}
if ((self::EXCLUDE_NOT_IMPORTANT & $filter) && !in_array($k, $listedProperties, true)) {

View File

@ -101,13 +101,16 @@ class VarCloner extends AbstractCloner
// Create $stub when the original value $v can not be used directly
// If $v is a nested structure, put that structure in array $a
switch (true) {
case empty($v):
case true === $v:
case null === $v:
case \is_bool($v):
case \is_int($v):
case \is_float($v):
continue 2;
case \is_string($v):
if ('' === $v) {
continue 2;
}
if (!\preg_match('//u', $v)) {
$stub = new Stub();
$stub->type = Stub::TYPE_STRING;
@ -131,6 +134,9 @@ class VarCloner extends AbstractCloner
break;
case \is_array($v):
if (!$v) {
continue 2;
}
$stub = $arrayStub;
$stub->class = Stub::ARRAY_INDEXED;