UserContextListener::onKernelRequest()   B
last analyzed

Complexity

Conditions 9
Paths 9

Size

Total Lines 45

Duplication

Lines 4
Ratio 8.89 %

Code Coverage

Tests 25
CRAP Score 9

Importance

Changes 0
Metric Value
dl 4
loc 45
ccs 25
cts 25
cp 1
rs 7.6444
c 0
b 0
f 0
cc 9
nc 9
nop 1
crap 9
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\ResponseTagger;
15
use FOS\HttpCache\UserContext\HashGenerator;
16
use FOS\HttpCacheBundle\UserContextInvalidator;
17
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
18
use Symfony\Component\HttpFoundation\Request;
19
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
20
use Symfony\Component\HttpFoundation\Response;
21
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
22
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
23
use Symfony\Component\HttpKernel\Event\RequestEvent;
24
use Symfony\Component\HttpKernel\Event\ResponseEvent;
25
use Symfony\Component\HttpKernel\EventListener\AbstractSessionListener;
26
use Symfony\Component\HttpKernel\HttpKernelInterface;
27
use Symfony\Component\HttpKernel\Kernel;
28
use Symfony\Component\HttpKernel\KernelEvents;
29
use Symfony\Component\OptionsResolver\OptionsResolver;
30
31 1 View Code Duplication
if (Kernel::MAJOR_VERSION >= 5) {
0 ignored issues
show
Duplication introduced by
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...
32 1
    class_alias(RequestEvent::class, 'FOS\HttpCacheBundle\EventListener\UserContextRequestEvent');
33 1
    class_alias(ResponseEvent::class, 'FOS\HttpCacheBundle\EventListener\UserContextResponseEvent');
34
} else {
35
    class_alias(GetResponseEvent::class, 'FOS\HttpCacheBundle\EventListener\UserContextRequestEvent');
36
    class_alias(FilterResponseEvent::class, 'FOS\HttpCacheBundle\EventListener\UserContextResponseEvent');
37
}
38
39
/**
40
 * Check requests and responses with the matcher.
41
 *
42
 * Abort context hash requests immediately and return the hash.
43
 * Add the vary information on responses to normal requests.
44
 *
45
 * @author Stefan Paschke <[email protected]>
46
 * @author Joel Wurtz <[email protected]>
47
 */
48
class UserContextListener implements EventSubscriberInterface
49
{
50
    /**
51
     * @var RequestMatcherInterface
52
     */
53
    private $requestMatcher;
54
55
    /**
56
     * @var HashGenerator
57
     */
58
    private $hashGenerator;
59
60
    /**
61
     * If the response tagger is set, the hash lookup response is tagged with the session id for later invalidation.
62
     *
63
     * @var ResponseTagger|null
64
     */
65
    private $responseTagger;
66
67
    /**
68
     * @var array
69
     */
70
    private $options;
71
72
    /**
73
     * Whether the application has a session listener and therefore could
74
     * require the AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER.
75
     *
76
     * @var bool
77
     */
78
    private $hasSessionListener;
79
80
    /**
81
     * @var bool
82
     */
83
    private $wasAnonymous;
84
85
    /**
86
     * Used to exclude anonymous requests (no authentication nor session) from user hash sanity check.
87
     * It prevents issues when the hash generator that is used returns a customized value for anonymous users,
88
     * that differs from the documented, hardcoded one.
89
     *
90
     * @var RequestMatcherInterface|null
91
     */
92
    private $anonymousRequestMatcher;
93
94 37
    public function __construct(
95
        RequestMatcherInterface $requestMatcher,
96
        HashGenerator $hashGenerator,
97
        RequestMatcherInterface $anonymousRequestMatcher = null,
98
        ResponseTagger $responseTagger = null,
99
        array $options = [],
100
        bool $hasSessionListener = true
101
    ) {
102 37
        $this->requestMatcher = $requestMatcher;
103 37
        $this->hashGenerator = $hashGenerator;
104 37
        $this->anonymousRequestMatcher = $anonymousRequestMatcher;
105 37
        $this->responseTagger = $responseTagger;
106 37
        $this->hasSessionListener = $hasSessionListener;
107
108 37
        $resolver = new OptionsResolver();
109 37
        $resolver->setDefaults([
110 37
            'user_identifier_headers' => ['Cookie', 'Authorization'],
111
            'user_hash_header' => 'X-User-Context-Hash',
112
            'ttl' => 0,
113
            'add_vary_on_hash' => true,
114
        ]);
115 37
        $resolver->setRequired(['user_identifier_headers', 'user_hash_header']);
116 37
        $resolver->setAllowedTypes('user_identifier_headers', 'array');
117 37
        $resolver->setAllowedTypes('user_hash_header', 'string');
118 37
        $resolver->setAllowedTypes('ttl', 'int');
119 37
        $resolver->setAllowedTypes('add_vary_on_hash', 'bool');
120
        $resolver->setAllowedValues('user_hash_header', function ($value) {
121 37
            return strlen($value) > 0;
122 37
        });
123
124 37
        $this->options = $resolver->resolve($options);
125 36
    }
126
127
    /**
128
     * Return the response to the context hash request with a header containing
129
     * the generated hash.
130
     *
131
     * If the ttl is bigger than 0, cache headers will be set for this response.
132
     */
133 26
    public function onKernelRequest(UserContextRequestEvent $event)
134
    {
135 26
        if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {
136 2
            return;
137
        }
138
139 25
        $request = $event->getRequest();
140 25
        if (!$this->requestMatcher->matches($request)) {
141 22
            if ($request->headers->has($this->options['user_hash_header'])) {
142
                // Keep track of if user is anonymous when we have user hash header in request
143 5
                $this->wasAnonymous = $this->isAnonymous($request);
144
            }
145
146
            // Return early if request is not a hash lookup
147 22
            return;
148
        }
149
150 3
        $hash = $this->hashGenerator->generateHash();
151
152 3
        if ($this->responseTagger && $request->hasSession()) {
153 1
            $tag = UserContextInvalidator::buildTag($request->getSession()->getId());
154 1
            $this->responseTagger->addTags([$tag]);
155
        }
156
157
        // status needs to be 200 as otherwise varnish will not cache the response.
158 3
        $response = new Response('', 200, [
159 3
            $this->options['user_hash_header'] => $hash,
160 3
            'Content-Type' => 'application/vnd.fos.user-context-hash',
161
        ]);
162
163 3
        if ($this->options['ttl'] > 0) {
164 2
            $response->setClientTtl($this->options['ttl']);
165 2
            $response->setVary($this->options['user_identifier_headers']);
166 2
            $response->setPublic();
167 2 View Code Duplication
            if ($this->hasSessionListener && version_compare('4.1', Kernel::VERSION, '<=')) {
0 ignored issues
show
Duplication introduced by
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...
168
                // header to avoid Symfony SessionListener overwriting the response to private
169 2
                $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 1);
170
            }
171
        } else {
172 1
            $response->setClientTtl(0);
173 1
            $response->headers->addCacheControlDirective('no-cache');
174
        }
175
176 3
        $event->setResponse($response);
177 3
    }
178
179
    /**
180
     * Tests if $request is an anonymous request or not.
181
     *
182
     * For backward compatibility reasons, true will be returned if no anonymous request matcher was provided.
183
     *
184
     * @return bool
185
     */
186 5
    private function isAnonymous(Request $request)
187
    {
188 5
        return $this->anonymousRequestMatcher ? $this->anonymousRequestMatcher->matches($request) : false;
189
    }
190
191
    /**
192
     * Add the context hash header to the headers to vary on if the header was
193
     * present in the request.
194
     */
195 30
    public function onKernelResponse(UserContextResponseEvent $event)
196
    {
197 30
        if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {
198 2
            return;
199
        }
200
201 29
        $response = $event->getResponse();
202 29
        $request = $event->getRequest();
203 29
        $vary = $response->getVary();
204
205 29
        if ($request->headers->has($this->options['user_hash_header'])) {
206 10
            $requestHash = $request->headers->get($this->options['user_hash_header']);
207
208
            // Generate hash to see if it might have changed during request if user was, or is "logged in" (session)
209
            // But only needed if user was, or is, logged in
210 10
            if (!$this->wasAnonymous || !$this->isAnonymous($request)) {
211 9
                $hash = $this->hashGenerator->generateHash();
212
            }
213
214 10
            if (isset($hash) && $hash !== $requestHash) {
215
                // hash has changed, session has most certainly changed, prevent setting incorrect cache
216 1
                $response->setCache([
217 1
                    'max_age' => 0,
218
                    's_maxage' => 0,
219
                    'private' => true,
220
                ]);
221 1
                $response->headers->addCacheControlDirective('no-cache');
222 1
                $response->headers->addCacheControlDirective('no-store');
223
224 1
                return;
225
            }
226
227 9
            if ($this->options['add_vary_on_hash']
228 9
                && !in_array($this->options['user_hash_header'], $vary)
229
            ) {
230 6
                $vary[] = $this->options['user_hash_header'];
231
            }
232
233
            // For Symfony 4.1+ if user hash header was in vary or just added here by "add_vary_on_hash"
234 9 View Code Duplication
            if ($this->hasSessionListener && \version_compare('4.1', Kernel::VERSION, '<=') && in_array($this->options['user_hash_header'], $vary)) {
0 ignored issues
show
Duplication introduced by
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...
235
                // header to avoid Symfony SessionListener overwriting the response to private
236 9
                $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 1);
237
            }
238 19
        } elseif ($this->options['add_vary_on_hash']) {
239
            /*
240
             * Additional precaution: If for some reason we get requests without a user hash, vary
241
             * on user identifier headers to avoid the caching proxy mixing up caches between
242
             * users. For the hash lookup request, those Vary headers are already added in
243
             * onKernelRequest above.
244
             */
245 19
            foreach ($this->options['user_identifier_headers'] as $header) {
246 19
                if (!in_array($header, $vary)) {
247 18
                    $vary[] = $header;
248
                }
249
            }
250
        }
251
252 28
        $response->setVary($vary, true);
253 28
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258 2
    public static function getSubscribedEvents()
259
    {
260
        return [
261 2
            KernelEvents::RESPONSE => 'onKernelResponse',
262 2
            KernelEvents::REQUEST => ['onKernelRequest', 7],
263
        ];
264
    }
265
}
266