Completed
Push — master ( a4efda...49f2e8 )
by David
15s queued 11s
created

src/EventListener/UserContextListener.php (2 issues)

Upgrade to new PHP Analysis Engine

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