Skip to content

Commit 5e9a1b2

Browse files
emodricdbu
authored andcommitted
[1.3] Backport Symfony session listener for Symfony 3.4+ to restore user context functionality (#441)
* Decorate default Symfony session listener for Symfony 3.4+ to restore user context functionality
1 parent e8a825b commit 5e9a1b2

File tree

9 files changed

+249
-2
lines changed

9 files changed

+249
-2
lines changed

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ matrix:
5050
env:
5151
- SYMFONY_VERSION='3.2.*'
5252
- FRAMEWORK_EXTRA_VERSION='~3.0'
53+
- php: 5.6
54+
env:
55+
- SYMFONY_VERSION='3.4.*'
56+
- PHPUNIT_FLAGS="--group sf34"
57+
- FRAMEWORK_EXTRA_VERSION='~3.0'
5358
- php: hhvm
5459
dist: trusty
5560

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
1.3.14
5+
------
6+
7+
* User context compatibility which was broken due to Symfony making responses
8+
private if the session is started as of Symfony 3.4+.
9+
410
1.3.13
511
------
612

DependencyInjection/FOSHttpCacheExtension.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
2020
use Symfony\Component\DependencyInjection\Reference;
2121
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
22+
use Symfony\Component\HttpKernel\Kernel;
2223
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
2324

2425
/**
@@ -224,6 +225,15 @@ private function loadUserContext(ContainerBuilder $container, XmlFileLoader $loa
224225
->addTag(HashGeneratorPass::TAG_NAME)
225226
->setAbstract(false);
226227
}
228+
229+
// Only decorate default session listener for Symfony 3.4+
230+
if (version_compare(Kernel::VERSION, '3.4', '>=')) {
231+
$container->getDefinition('fos_http_cache.user_context.session_listener')
232+
->setArgument(1, strtolower($config['user_hash_header']))
233+
->setArgument(2, array_map('strtolower', $config['user_identifier_headers']));
234+
} else {
235+
$container->removeDefinition('fos_http_cache.user_context.session_listener');
236+
}
227237
}
228238

229239
private function createRequestMatcher(ContainerBuilder $container, $path = null, $host = null, $methods = null, $ips = null, array $attributes = array())

EventListener/SessionListener.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCacheBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\HttpCacheBundle\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
16+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
17+
use Symfony\Component\HttpKernel\EventListener\SessionListener as BaseSessionListener;
18+
19+
/**
20+
* Decorates the default Symfony session listener.
21+
*
22+
* The default Symfony session listener automatically makes responses private
23+
* in case the session was started. This kills the user context feature of
24+
* FOSHttpCache. We disable the default behaviour only if the user context header
25+
* is part of the Vary headers to reduce the possible impacts on other parts
26+
* of your application.
27+
*
28+
* @author Yanick Witschi <yanick.witschi@terminal42.ch>
29+
*/
30+
final class SessionListener implements EventSubscriberInterface
31+
{
32+
/**
33+
* @var BaseSessionListener
34+
*/
35+
private $inner;
36+
37+
/**
38+
* @var string
39+
*/
40+
private $userHashHeader;
41+
42+
/**
43+
* @var array
44+
*/
45+
private $userIdentifierHeaders;
46+
47+
/**
48+
* @param BaseSessionListener $inner
49+
* @param string $userHashHeader Must be lower-cased
50+
* @param array $userIdentifierHeaders Must be lower-cased
51+
*/
52+
public function __construct(BaseSessionListener $inner, $userHashHeader, array $userIdentifierHeaders)
53+
{
54+
$this->inner = $inner;
55+
$this->userHashHeader = $userHashHeader;
56+
$this->userIdentifierHeaders = $userIdentifierHeaders;
57+
}
58+
59+
public function onKernelRequest(GetResponseEvent $event)
60+
{
61+
return $this->inner->onKernelRequest($event);
62+
}
63+
64+
public function onKernelResponse(FilterResponseEvent $event)
65+
{
66+
if (!$event->isMasterRequest()) {
67+
return;
68+
}
69+
70+
$varyHeaders = array_map('strtolower', $event->getResponse()->getVary());
71+
$relevantHeaders = array_merge($this->userIdentifierHeaders, array($this->userHashHeader));
72+
73+
// Call default behaviour if it's an irrelevant request for the user context
74+
if (0 === count(array_intersect($varyHeaders, $relevantHeaders))) {
75+
$this->inner->onKernelResponse($event);
76+
}
77+
78+
// noop, see class description
79+
}
80+
81+
public static function getSubscribedEvents()
82+
{
83+
return BaseSessionListener::getSubscribedEvents();
84+
}
85+
}

Resources/config/user_context.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
<argument />
4242
</service>
4343

44+
<service id="fos_http_cache.user_context.session_listener" class="FOS\HttpCacheBundle\EventListener\SessionListener" decorates="session_listener" public="false">
45+
<argument type="service" id="fos_http_cache.user_context.session_listener.inner" />
46+
<argument /> <!-- set by extension -->
47+
<argument /> <!-- set by extension -->
48+
</service>
49+
4450
<service id="fos_http_cache.user_context.anonymous_request_matcher" class="FOS\HttpCache\UserContext\AnonymousRequestMatcher">
4551
<argument type="collection" />
4652
</service>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
fos_http_cache:
1+
fos_http_cache: []

Tests/Unit/DependencyInjection/FOSHttpCacheExtensionTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\DependencyInjection\Definition;
1717
use Symfony\Component\DependencyInjection\DefinitionDecorator;
1818
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
19+
use Symfony\Component\HttpKernel\Kernel;
1920

2021
class FOSHttpCacheExtensionTest extends \PHPUnit_Framework_TestCase
2122
{
@@ -346,6 +347,37 @@ public function testConfigWithoutUserContext()
346347
$this->assertFalse($container->has('fos_http_cache.user_context.request_matcher'));
347348
$this->assertFalse($container->has('fos_http_cache.user_context.role_provider'));
348349
$this->assertFalse($container->has('fos_http_cache.user_context.logout_handler'));
350+
$this->assertFalse($container->has('fos_http_cache.user_context.session_listener'));
351+
}
352+
353+
/**
354+
* @group sf34
355+
*/
356+
public function testSessionListenerIsDecoratedIfNeeded()
357+
{
358+
$config = array(
359+
array('user_context' => array(
360+
'user_identifier_headers' => array('X-Foo'),
361+
'user_hash_header' => 'X-Bar',
362+
'hash_cache_ttl' => 30,
363+
'role_provider' => true,
364+
)),
365+
);
366+
367+
$container = $this->createContainer();
368+
$this->extension->load($config, $container);
369+
370+
// The whole definition should be removed for Symfony < 3.4
371+
if (version_compare(Kernel::VERSION, '3.4', '<')) {
372+
$this->assertFalse($container->hasDefinition('fos_http_cache.user_context.session_listener'));
373+
} else {
374+
$this->assertTrue($container->hasDefinition('fos_http_cache.user_context.session_listener'));
375+
376+
$definition = $container->getDefinition('fos_http_cache.user_context.session_listener');
377+
378+
$this->assertSame('x-bar', $definition->getArgument(1));
379+
$this->assertSame(array('x-foo'), $definition->getArgument(2));
380+
}
349381
}
350382

351383
public function testConfigLoadFlashMessageSubscriber()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCacheBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\HttpCacheBundle\Tests\Unit\EventListener;
13+
14+
use FOS\HttpCacheBundle\EventListener\SessionListener;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
19+
use Symfony\Component\HttpKernel\EventListener\SessionListener as BaseSessionListener;
20+
use Symfony\Component\HttpKernel\HttpKernelInterface;
21+
use Symfony\Component\HttpKernel\Kernel;
22+
23+
/**
24+
* @group sf34
25+
*/
26+
class SessionListenerTest extends TestCase
27+
{
28+
public function testOnKernelRequestRemainsUntouched()
29+
{
30+
$event = $this
31+
->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
32+
->disableOriginalConstructor()
33+
->getMock();
34+
35+
$inner = $this
36+
->getMockBuilder('Symfony\Component\HttpKernel\EventListener\SessionListener')
37+
->disableOriginalConstructor()
38+
->getMock();
39+
40+
$inner
41+
->expects($this->once())
42+
->method('onKernelRequest')
43+
->with($event)
44+
;
45+
46+
$listener = $this->getListener($inner);
47+
$listener->onKernelRequest($event);
48+
}
49+
50+
/**
51+
* @dataProvider onKernelResponseProvider
52+
*/
53+
public function testOnKernelResponse(Response $response, $shouldCallDecoratedListener)
54+
{
55+
if (version_compare(Kernel::VERSION, '3.4', '<')) {
56+
$this->markTestSkipped('Irrelevant for Symfony < 3.4');
57+
}
58+
59+
$httpKernel = $this
60+
->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')
61+
->disableOriginalConstructor()
62+
->getMock();
63+
64+
$event = new FilterResponseEvent(
65+
$httpKernel,
66+
new Request(),
67+
HttpKernelInterface::MASTER_REQUEST,
68+
$response
69+
);
70+
71+
$inner = $this
72+
->getMockBuilder('Symfony\Component\HttpKernel\EventListener\SessionListener')
73+
->disableOriginalConstructor()
74+
->getMock();
75+
76+
$inner
77+
->expects($shouldCallDecoratedListener ? $this->once() : $this->never())
78+
->method('onKernelResponse')
79+
->with($event)
80+
;
81+
82+
$listener = $this->getListener($inner);
83+
$listener->onKernelResponse($event);
84+
}
85+
86+
public function onKernelResponseProvider()
87+
{
88+
// Response, decorated listener should be called or not
89+
return array(
90+
'Irrelevant response' => array(new Response(), true),
91+
'Irrelevant response header' => array(new Response('', 200, array('Content-Type' => 'Foobar')), true),
92+
'Context hash header is present in Vary' => array(new Response('', 200, array('Vary' => 'X-User-Context-Hash')), false),
93+
'User identifier header is present in Vary' => array(new Response('', 200, array('Vary' => 'cookie')), false),
94+
'Both, context hash and identifier headers are present in Vary' => array(new Response('', 200, array('Vary' => 'Cookie, X-User-Context-Hash')), false),
95+
);
96+
}
97+
98+
private function getListener(BaseSessionListener $inner, $userHashHeader = 'x-user-context-hash', $userIdentifierHeaders = array('cookie', 'authorization'))
99+
{
100+
return new SessionListener($inner, $userHashHeader, $userIdentifierHeaders);
101+
}
102+
}

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"symfony/expression-language": "^2.4||^3.0",
3535
"symfony/monolog-bundle": "^2.3||^3.0",
3636
"polishsymfonycommunity/symfony-mocker-container": "^1.0",
37-
"matthiasnoback/symfony-dependency-injection-test": "^0.7.4"
37+
"matthiasnoback/symfony-dependency-injection-test": "^0.7.4",
38+
"sebastian/exporter": "^1.2||^2.0||^3.0"
3839
},
3940
"suggest": {
4041
"sensio/framework-extra-bundle": "For Tagged Cache Invalidation",

0 commit comments

Comments
 (0)