Completed
Pull Request — master (#485)
by André
05:22
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\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
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...
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