Skip to content

Commit b3bf853

Browse files
committed
Merge pull request #71 from FriendsOfSymfony/user-context
User context
2 parents d027ccc + 5f9af58 commit b3bf853

17 files changed

+826
-159
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace FOS\HttpCacheBundle\DependencyInjection\Compiler;
4+
5+
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
6+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
7+
use Symfony\Component\DependencyInjection\ContainerBuilder;
8+
use Symfony\Component\DependencyInjection\Reference;
9+
10+
/**
11+
* Add tagged provider to the hash generator for user context
12+
*/
13+
class UserContextListenerPass implements CompilerPassInterface
14+
{
15+
const TAG_NAME = "fos_http_cache.user_context_provider";
16+
17+
/**
18+
* {@inheritdoc}
19+
*/
20+
public function process(ContainerBuilder $container)
21+
{
22+
if (!$container->has('fos_http_cache.user_context.hash_generator')) {
23+
return;
24+
}
25+
26+
$definition = $container->getDefinition('fos_http_cache.user_context.hash_generator');
27+
28+
$providers = array();
29+
foreach ($container->findTaggedServiceIds(self::TAG_NAME) as $id => $parameters) {
30+
$providers[] = new Reference($id);
31+
}
32+
33+
if (!count($providers)) {
34+
throw new InvalidConfigurationException('No user context providers found. Either tag providers or disable fos_http_cache.user_context');
35+
}
36+
37+
$definition->addArgument($providers);
38+
}
39+
}

DependencyInjection/Configuration.php

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,10 @@ public function getConfigTreeBuilder()
3333
->defaultValue('X-Cache-Debug')
3434
->info('The header to send if debug is true.')
3535
->end()
36-
->booleanNode('authorization_listener')
37-
->defaultFalse()
38-
->info('Whether to activate the authorization listener that early returns head request after the security check.')
39-
->end()
4036
->end()
4137
;
4238

39+
$this->addUserContextListenerSection($rootNode);
4340
$this->addRulesSection($rootNode);
4441
$this->addProxyClientSection($rootNode);
4542
$this->addTagListenerSection($rootNode);
@@ -49,6 +46,55 @@ public function getConfigTreeBuilder()
4946
return $treeBuilder;
5047
}
5148

49+
private function addUserContextListenerSection(ArrayNodeDefinition $rootNode)
50+
{
51+
$rootNode
52+
->fixXmlConfig('user_identifier_header')
53+
->children()
54+
->arrayNode('user_context')
55+
->info('Listener that returns the request for the user context hash as early as possible.')
56+
->addDefaultsIfNotSet()
57+
->canBeEnabled()
58+
->children()
59+
->arrayNode('match')
60+
->addDefaultsIfNotSet()
61+
->children()
62+
->scalarNode('matcher_service')
63+
->defaultValue('fos_http_cache.user_context.request_matcher')
64+
->info('Service id of a request matcher that tells whether the request is a context hash request.')
65+
->end()
66+
->scalarNode('accept')
67+
->defaultValue('application/vnd.fos.user-context-hash')
68+
->info('Specify the accept HTTP header used for context hash requests.')
69+
->end()
70+
->scalarNode('method')
71+
->defaultNull()
72+
->info('Specify the HTTP method used for context hash requests.')
73+
->end()
74+
->end()
75+
->end()
76+
->scalarNode('hash_cache_ttl')
77+
->defaultValue(0)
78+
->info('Cache the response for the hash for the specified number of seconds. Setting this to 0 will not cache those responses at all.')
79+
->end()
80+
->arrayNode('user_identifier_headers')
81+
->prototype('scalar')->end()
82+
->defaultValue(array('Cookie', 'Authorization'))
83+
->info('List of headers that contains the unique identifier for the user in the hash request.')
84+
->end()
85+
->scalarNode('user_hash_header')
86+
->defaultValue('X-User-Context-Hash')
87+
->info('Name of the header that contains the hash information for the context.')
88+
->end()
89+
->booleanNode('role_provider')
90+
->defaultFalse()
91+
->info('Whether to enable a provider that automatically adds all roles of the current user to the context.')
92+
->end()
93+
->end()
94+
->end()
95+
->end();
96+
}
97+
5298
private function addRulesSection(ArrayNodeDefinition $rootNode)
5399
{
54100
$rules = $rootNode

DependencyInjection/FOSHttpCacheExtension.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace FOS\HttpCacheBundle\DependencyInjection;
44

5+
use FOS\HttpCacheBundle\DependencyInjection\Compiler\UserContextListenerPass;
56
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
67
use Symfony\Component\DependencyInjection\ContainerBuilder;
78
use Symfony\Component\Config\FileLocator;
@@ -64,8 +65,8 @@ public function load(array $configs, ContainerBuilder $container)
6465
throw new InvalidConfigurationException('You need to configure a proxy client to use the tag listener.');
6566
}
6667

67-
if ($config['authorization_listener']) {
68-
$loader->load('authorization_request_listener.xml');
68+
if ($config['user_context']['enabled']) {
69+
$this->loadUserContext($container, $loader, $config['user_context']);
6970
}
7071

7172
if (!empty($config['flash_message_listener']) && $config['flash_message_listener']['enabled']) {
@@ -135,6 +136,27 @@ protected function createRuleMatcher(ContainerBuilder $container, Reference $req
135136
return new Reference($id);
136137
}
137138

139+
protected function loadUserContext(ContainerBuilder $container, XmlFileLoader $loader, array $config)
140+
{
141+
$loader->load('user_context.xml');
142+
143+
$container->getDefinition($this->getAlias().'.user_context.request_matcher')
144+
->replaceArgument(0, $config['match']['accept'])
145+
->replaceArgument(1, $config['match']['method']);
146+
147+
$container->getDefinition($this->getAlias().'.event_listener.user_context')
148+
->replaceArgument(0, new Reference($config['match']['matcher_service']))
149+
->replaceArgument(2, $config['user_identifier_headers'])
150+
->replaceArgument(3, $config['user_hash_header'])
151+
->replaceArgument(4, $config['hash_cache_ttl']);
152+
153+
if ($config['role_provider']) {
154+
$container->getDefinition($this->getAlias().'.user_context.role_provider')
155+
->addTag(UserContextListenerPass::TAG_NAME)
156+
->setAbstract(false);
157+
}
158+
}
159+
138160
protected function createRequestMatcher(ContainerBuilder $container, $path = null, $host = null, $methods = null, $ips = null, array $attributes = array())
139161
{
140162
$arguments = array($path, $host, $methods, $ips, $attributes);

EventListener/CacheAuthorizationSubscriber.php

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace FOS\HttpCacheBundle\EventListener;
4+
5+
use FOS\HttpCache\UserContext\HashGenerator;
6+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
7+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
8+
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
9+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
10+
use Symfony\Component\HttpFoundation\Response;
11+
use Symfony\Component\HttpKernel\HttpKernelInterface;
12+
use Symfony\Component\HttpKernel\KernelEvents;
13+
14+
/**
15+
* Check requests and responses with the matcher.
16+
*
17+
* Abort context hash requests immediately and return the hash.
18+
* Add the vary information on responses to normal requests.
19+
*
20+
* @author Stefan Paschke <[email protected]>
21+
* @author Joel Wurtz <[email protected]>
22+
*/
23+
class UserContextSubscriber implements EventSubscriberInterface
24+
{
25+
/**
26+
* @var RequestMatcherInterface
27+
*/
28+
private $requestMatcher;
29+
30+
/**
31+
* @var HashGenerator
32+
*/
33+
private $hashGenerator;
34+
35+
/**
36+
* @var string[]
37+
*/
38+
private $userIdentifierHeaders;
39+
40+
/**
41+
* @var string
42+
*/
43+
private $hashHeader;
44+
45+
/**
46+
* @var integer
47+
*/
48+
private $ttl;
49+
50+
public function __construct(
51+
RequestMatcherInterface $requestMatcher,
52+
HashGenerator $hashGenerator,
53+
array $userIdentifierHeaders = array('Cookie', 'Authorization'),
54+
$hashHeader = "X-User-Context-Hash",
55+
$ttl = 0
56+
)
57+
{
58+
if (!count($userIdentifierHeaders)) {
59+
throw new \InvalidArgumentException('The user context must vary on some request headers');
60+
}
61+
$this->requestMatcher = $requestMatcher;
62+
$this->hashGenerator = $hashGenerator;
63+
$this->userIdentifierHeaders = $userIdentifierHeaders;
64+
$this->hashHeader = $hashHeader;
65+
$this->ttl = $ttl;
66+
}
67+
68+
/**
69+
* Return the response to the context hash request with a header containing
70+
* the generated hash.
71+
*
72+
* If the ttl is bigger than 0, cache headers will be set for this response.
73+
*
74+
* @param GetResponseEvent $event
75+
*/
76+
public function onKernelRequest(GetResponseEvent $event)
77+
{
78+
if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
79+
return;
80+
}
81+
82+
if (!$this->requestMatcher->matches($event->getRequest())) {
83+
return;
84+
}
85+
86+
$hash = $this->hashGenerator->generateHash();
87+
88+
// status needs to be 200 as otherwise varnish will not cache the response.
89+
$response = new Response('', 200, array(
90+
$this->hashHeader => $hash,
91+
'Content-Type' => 'application/vnd.fos.user-context-hash'
92+
));
93+
94+
if ($this->ttl > 0) {
95+
$response->setClientTtl($this->ttl);
96+
$response->setVary($this->userIdentifierHeaders);
97+
} else {
98+
$response->setClientTtl(0);
99+
$response->headers->addCacheControlDirective('no-cache');
100+
}
101+
102+
$event->setResponse($response);
103+
}
104+
105+
/**
106+
* Add the context hash header to the headers to vary on if the header was
107+
* present in the request.
108+
*
109+
* @param FilterResponseEvent $event
110+
*/
111+
public function onKernelResponse(FilterResponseEvent $event)
112+
{
113+
if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
114+
return;
115+
}
116+
117+
$response = $event->getResponse();
118+
$vary = $response->getVary();
119+
120+
if ($event->getRequest()->headers->has($this->hashHeader)) {
121+
if (!in_array($this->hashHeader, $vary)) {
122+
$vary[] = $this->hashHeader;
123+
}
124+
} else {
125+
foreach ($this->userIdentifierHeaders as $header) {
126+
if (!in_array($header, $vary)) {
127+
$vary[] = $header;
128+
}
129+
}
130+
}
131+
132+
$response->setVary($vary, true);
133+
}
134+
135+
/**
136+
* {@inheritdoc}
137+
*/
138+
public static function getSubscribedEvents()
139+
{
140+
return array(
141+
KernelEvents::RESPONSE => 'onKernelResponse',
142+
KernelEvents::REQUEST => array('onKernelRequest', 7),
143+
);
144+
}
145+
}

FOSHttpCacheBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use FOS\HttpCacheBundle\DependencyInjection\Compiler\LoggerPass;
66
use FOS\HttpCacheBundle\DependencyInjection\Compiler\TagListenerPass;
7+
use FOS\HttpCacheBundle\DependencyInjection\Compiler\UserContextListenerPass;
78
use Symfony\Component\DependencyInjection\ContainerBuilder;
89
use Symfony\Component\HttpKernel\Bundle\Bundle;
910

@@ -16,5 +17,6 @@ public function build(ContainerBuilder $container)
1617
{
1718
$container->addCompilerPass(new LoggerPass());
1819
$container->addCompilerPass(new TagListenerPass());
20+
$container->addCompilerPass(new UserContextListenerPass());
1921
}
2022
}

Resources/config/user_context.xml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<parameters>
8+
<parameter key="fos_http_cache.event_listener.user_context.class">FOS\HttpCacheBundle\EventListener\UserContextSubscriber</parameter>
9+
<parameter key="fos_http_cache.user_context.hash_generator.class">FOS\HttpCache\UserContext\HashGenerator</parameter>
10+
<parameter key="fos_http_cache.user_context.role_provider.class">FOS\HttpCacheBundle\UserContext\RoleProvider</parameter>
11+
<parameter key="fos_http_cache.user_context.request_matcher.class">FOS\HttpCacheBundle\UserContext\RequestMatcher</parameter>
12+
</parameters>
13+
14+
<services>
15+
<service id="fos_http_cache.user_context.hash_generator" class="%fos_http_cache.user_context.hash_generator.class%">
16+
</service>
17+
18+
<service id="fos_http_cache.user_context.request_matcher" class="%fos_http_cache.user_context.request_matcher.class%">
19+
<argument />
20+
<argument />
21+
</service>
22+
23+
<service id="fos_http_cache.event_listener.user_context" class="%fos_http_cache.event_listener.user_context.class%">
24+
<argument type="service" id="fos_http_cache.user_context.request_matcher" />
25+
<argument type="service" id="fos_http_cache.user_context.hash_generator" />
26+
<argument />
27+
<argument />
28+
<argument />
29+
<tag name="kernel.event_subscriber" />
30+
</service>
31+
32+
<service id="fos_http_cache.user_context.role_provider" class="%fos_http_cache.user_context.role_provider.class%" abstract="true">
33+
<argument type="service" id="security.context" on-invalid="ignore" />
34+
</service>
35+
</services>
36+
</container>

0 commit comments

Comments
 (0)