These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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 FOS\HttpCache\UserContext\HashGenerator; |
||
15 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
||
16 | use Symfony\Component\HttpFoundation\Request; |
||
17 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; |
||
18 | use Symfony\Component\HttpFoundation\Response; |
||
19 | use Symfony\Component\HttpKernel\Event\FilterResponseEvent; |
||
20 | use Symfony\Component\HttpKernel\Event\GetResponseEvent; |
||
21 | use Symfony\Component\HttpKernel\EventListener\AbstractSessionListener; |
||
22 | use Symfony\Component\HttpKernel\HttpKernelInterface; |
||
23 | use Symfony\Component\HttpKernel\Kernel; |
||
24 | use Symfony\Component\HttpKernel\KernelEvents; |
||
25 | use Symfony\Component\OptionsResolver\OptionsResolver; |
||
26 | |||
27 | /** |
||
28 | * Check requests and responses with the matcher. |
||
29 | * |
||
30 | * Abort context hash requests immediately and return the hash. |
||
31 | * Add the vary information on responses to normal requests. |
||
32 | * |
||
33 | * @author Stefan Paschke <[email protected]> |
||
34 | * @author Joel Wurtz <[email protected]> |
||
35 | */ |
||
36 | class UserContextListener implements EventSubscriberInterface |
||
37 | { |
||
38 | /** |
||
39 | * @var RequestMatcherInterface |
||
40 | */ |
||
41 | private $requestMatcher; |
||
42 | |||
43 | /** |
||
44 | * @var HashGenerator |
||
45 | */ |
||
46 | private $hashGenerator; |
||
47 | |||
48 | /** |
||
49 | * @var array |
||
50 | */ |
||
51 | private $options; |
||
52 | |||
53 | /** |
||
54 | * @var string |
||
55 | */ |
||
56 | private $hash; |
||
57 | |||
58 | /** |
||
59 | * Used to exclude anonymous requests (no authentication nor session) from user hash sanity check. |
||
60 | * It prevents issues when the hash generator that is used returns a customized value for anonymous users, |
||
61 | * that differs from the documented, hardcoded one. |
||
62 | * |
||
63 | * @var RequestMatcherInterface |
||
64 | */ |
||
65 | private $anonymousRequestMatcher; |
||
66 | |||
67 | /** |
||
68 | * Constructor. |
||
69 | * |
||
70 | * @param RequestMatcherInterface $requestMatcher |
||
71 | * @param HashGenerator $hashGenerator |
||
72 | * @param RequestMatcherInterface|null $anonymousRequestMatcher |
||
73 | * @param array $options |
||
74 | */ |
||
75 | 30 | public function __construct( |
|
76 | RequestMatcherInterface $requestMatcher, |
||
77 | HashGenerator $hashGenerator, |
||
78 | RequestMatcherInterface $anonymousRequestMatcher = null, |
||
79 | array $options = [] |
||
80 | ) { |
||
81 | 30 | $this->requestMatcher = $requestMatcher; |
|
82 | 30 | $this->hashGenerator = $hashGenerator; |
|
83 | 30 | $this->anonymousRequestMatcher = $anonymousRequestMatcher; |
|
84 | |||
85 | 30 | $resolver = new OptionsResolver(); |
|
86 | 30 | $resolver->setDefaults([ |
|
87 | 30 | 'user_identifier_headers' => ['Cookie', 'Authorization'], |
|
88 | 'user_hash_header' => 'X-User-Context-Hash', |
||
89 | 'ttl' => 0, |
||
90 | 'add_vary_on_hash' => true, |
||
91 | ]); |
||
92 | 30 | $resolver->setRequired(['user_identifier_headers', 'user_hash_header']); |
|
93 | 30 | $resolver->setAllowedTypes('user_identifier_headers', 'array'); |
|
94 | 30 | $resolver->setAllowedTypes('user_hash_header', 'string'); |
|
95 | 30 | $resolver->setAllowedTypes('ttl', 'int'); |
|
96 | 30 | $resolver->setAllowedTypes('add_vary_on_hash', 'bool'); |
|
97 | 30 | $resolver->setAllowedValues('user_hash_header', function ($value) { |
|
98 | 30 | return strlen($value) > 0; |
|
99 | 30 | }); |
|
100 | |||
101 | 30 | $this->options = $resolver->resolve($options); |
|
102 | 29 | } |
|
103 | |||
104 | /** |
||
105 | * Return the response to the context hash request with a header containing |
||
106 | * the generated hash. |
||
107 | * |
||
108 | * If the ttl is bigger than 0, cache headers will be set for this response. |
||
109 | * |
||
110 | * @param GetResponseEvent $event |
||
111 | */ |
||
112 | 24 | public function onKernelRequest(GetResponseEvent $event) |
|
113 | { |
||
114 | 24 | if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) { |
|
115 | 2 | return; |
|
116 | } |
||
117 | |||
118 | 23 | if (!$this->requestMatcher->matches($event->getRequest())) { |
|
119 | 20 | if ($event->getRequest()->headers->has($this->options['user_hash_header']) |
|
120 | 20 | && !$this->isAnonymous($event->getRequest()) |
|
121 | ) { |
||
122 | 3 | $this->hash = $this->hashGenerator->generateHash(); |
|
123 | } |
||
124 | |||
125 | 20 | return; |
|
126 | } |
||
127 | |||
128 | 3 | $hash = $this->hashGenerator->generateHash(); |
|
129 | |||
130 | // status needs to be 200 as otherwise varnish will not cache the response. |
||
131 | 3 | $response = new Response('', 200, [ |
|
132 | 3 | $this->options['user_hash_header'] => $hash, |
|
133 | 3 | 'Content-Type' => 'application/vnd.fos.user-context-hash', |
|
134 | ]); |
||
135 | |||
136 | 3 | if ($this->options['ttl'] > 0) { |
|
137 | 2 | $response->setClientTtl($this->options['ttl']); |
|
138 | 2 | $response->setVary($this->options['user_identifier_headers']); |
|
139 | 2 | $response->setPublic(); |
|
140 | 2 | View Code Duplication | if (4 <= Kernel::MAJOR_VERSION && 1 <= Kernel::MINOR_VERSION) { |
0 ignored issues
–
show
|
|||
141 | // header to avoid Symfony SessionListener overwriting the response to private |
||
142 | 2 | $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 1); |
|
143 | } |
||
144 | } else { |
||
145 | 1 | $response->setClientTtl(0); |
|
146 | 1 | $response->headers->addCacheControlDirective('no-cache'); |
|
147 | } |
||
148 | |||
149 | 3 | $event->setResponse($response); |
|
150 | 3 | } |
|
151 | |||
152 | /** |
||
153 | * Tests if $request is an anonymous request or not. |
||
154 | * |
||
155 | * For backward compatibility reasons, true will be returned if no anonymous request matcher was provided. |
||
156 | * |
||
157 | * @param Request $request |
||
158 | * |
||
159 | * @return bool |
||
160 | */ |
||
161 | 4 | private function isAnonymous(Request $request) |
|
162 | { |
||
163 | 4 | return $this->anonymousRequestMatcher ? $this->anonymousRequestMatcher->matches($request) : false; |
|
164 | } |
||
165 | |||
166 | /** |
||
167 | * Add the context hash header to the headers to vary on if the header was |
||
168 | * present in the request. |
||
169 | * |
||
170 | * @param FilterResponseEvent $event |
||
171 | */ |
||
172 | 24 | public function onKernelResponse(FilterResponseEvent $event) |
|
173 | { |
||
174 | 24 | if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) { |
|
175 | 2 | return; |
|
176 | } |
||
177 | |||
178 | 23 | $response = $event->getResponse(); |
|
179 | 23 | $request = $event->getRequest(); |
|
180 | |||
181 | 23 | $vary = $response->getVary(); |
|
182 | |||
183 | 23 | if ($request->headers->has($this->options['user_hash_header'])) { |
|
184 | 5 | if (null !== $this->hash && $this->hash !== $request->headers->get($this->options['user_hash_header'])) { |
|
185 | // hash has changed, session has most certainly changed, prevent setting incorrect cache |
||
186 | 1 | $response->setCache([ |
|
187 | 1 | 'max_age' => 0, |
|
188 | 's_maxage' => 0, |
||
189 | 'private' => true, |
||
190 | ]); |
||
191 | 1 | $response->headers->addCacheControlDirective('no-cache'); |
|
192 | 1 | $response->headers->addCacheControlDirective('no-store'); |
|
193 | |||
194 | 1 | return; |
|
195 | } |
||
196 | |||
197 | 4 | if ($this->options['add_vary_on_hash'] |
|
198 | 4 | && !in_array($this->options['user_hash_header'], $vary) |
|
199 | ) { |
||
200 | 4 | $vary[] = $this->options['user_hash_header']; |
|
201 | 4 | View Code Duplication | if (4 <= Kernel::MAJOR_VERSION && 1 <= Kernel::MINOR_VERSION) { |
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository.
Loading history...
|
|||
202 | // header to avoid Symfony SessionListener overwriting the response to private |
||
203 | 4 | $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 1); |
|
204 | } |
||
205 | } |
||
206 | 18 | } elseif ($this->options['add_vary_on_hash']) { |
|
207 | /* |
||
208 | * Additional precaution: If for some reason we get requests without a user hash, vary |
||
209 | * on user identifier headers to avoid the caching proxy mixing up caches between |
||
210 | * users. For the hash lookup request, those Vary headers are already added in |
||
211 | * onKernelRequest above. |
||
212 | */ |
||
213 | 18 | foreach ($this->options['user_identifier_headers'] as $header) { |
|
214 | 18 | if (!in_array($header, $vary)) { |
|
215 | 18 | $vary[] = $header; |
|
216 | } |
||
217 | } |
||
218 | } |
||
219 | |||
220 | 22 | $response->setVary($vary, true); |
|
221 | 22 | } |
|
222 | |||
223 | /** |
||
224 | * {@inheritdoc} |
||
225 | */ |
||
226 | 2 | public static function getSubscribedEvents() |
|
227 | { |
||
228 | return [ |
||
229 | 2 | KernelEvents::RESPONSE => 'onKernelResponse', |
|
230 | 2 | KernelEvents::REQUEST => ['onKernelRequest', 7], |
|
231 | ]; |
||
232 | } |
||
233 | } |
||
234 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.