diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/RemoveUnusedSessionMarshallingHandlerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/RemoveUnusedSessionMarshallingHandlerPass.php new file mode 100644 index 0000000000..8b6479c4f2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/RemoveUnusedSessionMarshallingHandlerPass.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Ahmed TAILOULOUTE + */ +class RemoveUnusedSessionMarshallingHandlerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('session.marshalling_handler')) { + return; + } + + $isMarshallerDecorated = false; + + foreach ($container->getDefinitions() as $definition) { + $decorated = $definition->getDecoratedService(); + if (null !== $decorated && 'session.marshaller' === $decorated[0]) { + $isMarshallerDecorated = true; + + break; + } + } + + if (!$isMarshallerDecorated) { + $container->removeDefinition('session.marshalling_handler'); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 6cafc4399e..b559253bf2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -18,6 +18,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilder use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass; @@ -130,6 +131,7 @@ class FrameworkBundle extends Bundle $this->addCompilerPassIfExists($container, AddAutoMappingConfigurationPass::class); $container->addCompilerPass(new RegisterReverseContainerPass(true)); $container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING); + $container->addCompilerPass(new RemoveUnusedSessionMarshallingHandlerPass()); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml index 0cb7b4e200..2dc897fc74 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml @@ -71,5 +71,12 @@ + + + + + + + diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 659345c20c..56b76d84b8 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -13,6 +13,7 @@ CHANGELOG * added `Request::preferSafeContent()` and `Response::setContentSafe()` to handle "safe" HTTP preference according to [RFC 8674](https://tools.ietf.org/html/rfc8674) * made the Mime component an optional dependency + * added `MarshallingSessionHandler`, `IdentityMarshaller` 5.0.0 ----- diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/IdentityMarshaller.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/IdentityMarshaller.php new file mode 100644 index 0000000000..bea3a323ed --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/IdentityMarshaller.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Ahmed TAILOULOUTE + */ +class IdentityMarshaller implements MarshallerInterface +{ + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + foreach ($values as $key => $value) { + if (!\is_string($value)) { + throw new \LogicException(sprintf('%s accepts only string as data.', __METHOD__)); + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value): string + { + return $value; + } +} diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MarshallingSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MarshallingSessionHandler.php new file mode 100644 index 0000000000..25cd4ec164 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MarshallingSessionHandler.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Ahmed TAILOULOUTE + */ +class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + private $handler; + private $marshaller; + + public function __construct(AbstractSessionHandler $handler, MarshallerInterface $marshaller) + { + $this->handler = $handler; + $this->marshaller = $marshaller; + } + + /** + * {@inheritdoc} + */ + public function open($savePath, $name) + { + return $this->handler->open($savePath, $name); + } + + /** + * {@inheritdoc} + */ + public function close() + { + return $this->handler->close(); + } + + /** + * {@inheritdoc} + */ + public function destroy($sessionId) + { + return $this->handler->destroy($sessionId); + } + + /** + * {@inheritdoc} + */ + public function gc($maxlifetime) + { + return $this->handler->gc($maxlifetime); + } + + /** + * {@inheritdoc} + */ + public function read($sessionId) + { + return $this->marshaller->unmarshall($this->handler->read($sessionId)); + } + + /** + * {@inheritdoc} + */ + public function write($sessionId, $data) + { + $failed = []; + $marshalledData = $this->marshaller->marshall(['data' => $data], $failed); + + if (isset($failed['data'])) { + return false; + } + + return $this->handler->write($sessionId, $marshalledData['data']); + } + + /** + * {@inheritdoc} + */ + public function validateId($sessionId) + { + return $this->handler->validateId($sessionId); + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + return $this->handler->updateTimestamp($sessionId, $data); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/IdentityMarshallerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/IdentityMarshallerTest.php new file mode 100644 index 0000000000..b26bc7e60a --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/IdentityMarshallerTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\IdentityMarshaller; + +/** + * @author Ahmed TAILOULOUTE + */ +class IdentityMarshallerTest extends Testcase +{ + public function testMarshall() + { + $marshaller = new IdentityMarshaller(); + $values = ['data' => 'string_data']; + $failed = []; + + $this->assertSame($values, $marshaller->marshall($values, $failed)); + } + + /** + * @dataProvider invalidMarshallDataProvider + */ + public function testMarshallInvalidData($values) + { + $marshaller = new IdentityMarshaller(); + $failed = []; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Symfony\Component\HttpFoundation\Session\Storage\Handler\IdentityMarshaller::marshall accepts only string as data'); + + $marshaller->marshall($values, $failed); + } + + public function testUnmarshall() + { + $marshaller = new IdentityMarshaller(); + + $this->assertEquals('data', $marshaller->unmarshall('data')); + } + + public function invalidMarshallDataProvider(): iterable + { + return [ + [['object' => new \stdClass()]], + [['foo' => ['bar']]], + ]; + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php new file mode 100644 index 0000000000..e9eb46801e --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; + +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\MarshallingSessionHandler; + +/** + * @author Ahmed TAILOULOUTE + */ +class MarshallingSessionHandlerTest extends TestCase +{ + /** + * @var MockObject|\SessionHandlerInterface + */ + protected $handler; + + /** + * @var MockObject|MarshallerInterface + */ + protected $marshaller; + + protected function setUp(): void + { + $this->marshaller = $this->getMockBuilder(MarshallerInterface::class)->getMock(); + $this->handler = $this->getMockBuilder(AbstractSessionHandler::class)->getMock(); + } + + public function testOpen() + { + $marshallingSessionHandler = new MarshallingSessionHandler($this->handler, $this->marshaller); + + $this->handler->expects($this->once())->method('open') + ->with('path', 'name')->willReturn(true); + + $marshallingSessionHandler->open('path', 'name'); + } + + public function testClose() + { + $marshallingSessionHandler = new MarshallingSessionHandler($this->handler, $this->marshaller); + + $this->handler->expects($this->once())->method('close')->willReturn(true); + + $this->assertTrue($marshallingSessionHandler->close()); + } + + public function testDestroy() + { + $marshallingSessionHandler = new MarshallingSessionHandler($this->handler, $this->marshaller); + + $this->handler->expects($this->once())->method('destroy') + ->with('session_id')->willReturn(true); + + $marshallingSessionHandler->destroy('session_id'); + } + + public function testGc() + { + $marshallingSessionHandler = new MarshallingSessionHandler($this->handler, $this->marshaller); + + $this->handler->expects($this->once())->method('gc') + ->with('maxlifetime')->willReturn(true); + + $marshallingSessionHandler->gc('maxlifetime'); + } + + public function testRead() + { + $marshallingSessionHandler = new MarshallingSessionHandler($this->handler, $this->marshaller); + + $this->handler->expects($this->once())->method('read')->with('session_id') + ->willReturn('data'); + $this->marshaller->expects($this->once())->method('unmarshall')->with('data') + ->willReturn('unmarshalled_data') + ; + + $result = $marshallingSessionHandler->read('session_id'); + $this->assertEquals('unmarshalled_data', $result); + } + + public function testWrite() + { + $marshallingSessionHandler = new MarshallingSessionHandler($this->handler, $this->marshaller); + + $this->marshaller->expects($this->once())->method('marshall') + ->with(['data' => 'data'], []) + ->willReturn(['data' => 'marshalled_data']); + + $this->handler->expects($this->once())->method('write') + ->with('session_id', 'marshalled_data') + ; + + $marshallingSessionHandler->write('session_id', 'data'); + } + + public function testValidateId() + { + $marshallingSessionHandler = new MarshallingSessionHandler($this->handler, $this->marshaller); + + $this->handler->expects($this->once())->method('validateId') + ->with('session_id')->willReturn(true); + + $marshallingSessionHandler->validateId('session_id'); + } + + public function testUpdateTimestamp() + { + $marshallingSessionHandler = new MarshallingSessionHandler($this->handler, $this->marshaller); + + $this->handler->expects($this->once())->method('updateTimestamp') + ->with('session_id', 'data')->willReturn(true); + + $marshallingSessionHandler->updateTimestamp('session_id', 'data'); + } +} diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json index 9848c97fbb..b214a11562 100644 --- a/src/Symfony/Component/HttpFoundation/composer.json +++ b/src/Symfony/Component/HttpFoundation/composer.json @@ -22,6 +22,7 @@ }, "require-dev": { "predis/predis": "~1.0", + "symfony/cache": "^4.4|^5.0", "symfony/mime": "^4.4|^5.0", "symfony/expression-language": "^4.4|^5.0" },