Completed
Pull Request — master (#319)
by David
06:22
created

UserContextListener   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 180
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 98.15%

Importance

Changes 0
Metric Value
wmc 21
lcom 1
cbo 8
dl 0
loc 180
ccs 53
cts 54
cp 0.9815
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 20 2
B onKernelRequest() 0 33 6
A isAnonymous() 0 4 2
D onKernelResponse() 0 39 10
A getSubscribedEvents() 0 7 1
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\HttpKernel\Event\FilterResponseEvent;
19
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
20
use Symfony\Component\HttpFoundation\Response;
21
use Symfony\Component\HttpKernel\HttpKernelInterface;
22
use Symfony\Component\HttpKernel\KernelEvents;
23
24
/**
25
 * Check requests and responses with the matcher.
26
 *
27
 * Abort context hash requests immediately and return the hash.
28
 * Add the vary information on responses to normal requests.
29
 *
30
 * @author Stefan Paschke <[email protected]>
31
 * @author Joel Wurtz <[email protected]>
32
 */
33
class UserContextListener implements EventSubscriberInterface
34
{
35
    /**
36
     * @var RequestMatcherInterface
37
     */
38
    private $requestMatcher;
39
40
    /**
41
     * @var HashGenerator
42
     */
43
    private $hashGenerator;
44
45
    /**
46
     * @var string[]
47
     */
48
    private $userIdentifierHeaders;
49
50
    /**
51
     * @var string
52
     */
53
    private $hash;
54
55
    /**
56
     * @var string
57
     */
58
    private $hashHeader;
59
60
    /**
61
     * @var int
62
     */
63
    private $ttl;
64
65
    /**
66
     * @var bool Whether to automatically add the Vary header for the hash / user identifier if there was no hash
67
     */
68
    private $addVaryOnHash;
69
70
    /**
71
     * Used to exclude anonymous requests (no authentication nor session) from user hash sanity check.
72
     * It prevents issues when the hash generator that is used returns a customized value for anonymous users,
73
     * that differs from the documented, hardcoded one.
74
     *
75
     * @var RequestMatcherInterface
76
     */
77
    private $anonymousRequestMatcher;
78
79 26
    public function __construct(
80
        RequestMatcherInterface $requestMatcher,
81
        HashGenerator $hashGenerator,
82
        array $userIdentifierHeaders = array('Cookie', 'Authorization'),
83
        $hashHeader = 'X-User-Context-Hash',
84
        $ttl = 0,
85
        $addVaryOnHash = true,
86
        RequestMatcherInterface $anonymousRequestMatcher = null
87
    ) {
88 26
        if (!count($userIdentifierHeaders)) {
89 1
            throw new \InvalidArgumentException('The user context must vary on some request headers');
90
        }
91 25
        $this->requestMatcher = $requestMatcher;
92 25
        $this->hashGenerator = $hashGenerator;
93 25
        $this->userIdentifierHeaders = $userIdentifierHeaders;
94 25
        $this->hashHeader = $hashHeader;
95 25
        $this->ttl = $ttl;
96 25
        $this->addVaryOnHash = $addVaryOnHash;
97 25
        $this->anonymousRequestMatcher = $anonymousRequestMatcher;
98 25
    }
99
100
    /**
101
     * Return the response to the context hash request with a header containing
102
     * the generated hash.
103
     *
104
     * If the ttl is bigger than 0, cache headers will be set for this response.
105
     *
106
     * @param GetResponseEvent $event
107
     */
108 21
    public function onKernelRequest(GetResponseEvent $event)
109
    {
110 21
        if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
111 3
            return;
112
        }
113
114 20
        if (!$this->requestMatcher->matches($event->getRequest())) {
115 18
            if ($event->getRequest()->headers->has($this->hashHeader) && !$this->isAnonymous($event->getRequest())) {
116 2
                $this->hash = $this->hashGenerator->generateHash();
117
            }
118
119 18
            return;
120
        }
121
122 2
        $hash = $this->hashGenerator->generateHash();
123
124
        // status needs to be 200 as otherwise varnish will not cache the response.
125 2
        $response = new Response('', 200, array(
126 2
            $this->hashHeader => $hash,
127 2
            'Content-Type' => 'application/vnd.fos.user-context-hash',
128
        ));
129
130 2
        if ($this->ttl > 0) {
131 1
            $response->setClientTtl($this->ttl);
132 1
            $response->setVary($this->userIdentifierHeaders);
133 1
            $response->setPublic();
134
        } else {
135 1
            $response->setClientTtl(0);
136 1
            $response->headers->addCacheControlDirective('no-cache');
137
        }
138
139 2
        $event->setResponse($response);
140 2
    }
141
142
    /**
143
     * Tests if $request is an anonymous request or not.
144
     *
145
     * For backward compatibility reasons, true will be returned if no anonymous request matcher was provided.
146
     *
147
     * @param Request $request
148
     *
149
     * @return bool
150
     */
151 3
    private function isAnonymous(Request $request)
152
    {
153 3
        return $this->anonymousRequestMatcher ? $this->anonymousRequestMatcher->matches($request) : false;
154
    }
155
156
    /**
157
     * Add the context hash header to the headers to vary on if the header was
158
     * present in the request.
159
     *
160
     * @param FilterResponseEvent $event
161
     */
162 21
    public function onKernelResponse(FilterResponseEvent $event)
163
    {
164 21
        if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
165 3
            return;
166
        }
167
168 20
        $response = $event->getResponse();
169 20
        $request = $event->getRequest();
170
171 20
        $vary = $response->getVary();
172
173 20
        if ($request->headers->has($this->hashHeader)) {
174
            // hash has changed, session has most certainly changed, prevent setting incorrect cache
175 4
            if (!is_null($this->hash) && $this->hash !== $request->headers->get($this->hashHeader)) {
176 1
                $response->setClientTtl(0);
177 1
                $response->headers->addCacheControlDirective('no-cache');
178
179 1
                return;
180
            }
181
182 3
            if ($this->addVaryOnHash && !in_array($this->hashHeader, $vary)) {
183 3
                $vary[] = $this->hashHeader;
184
            }
185 16
        } elseif ($this->addVaryOnHash) {
186
            /*
187
             * Additional precaution: If for some reason we get requests without a user hash, vary
188
             * on user identifier headers to avoid the caching proxy mixing up caches between
189
             * users. For the hash lookup request, those Vary headers are already added in
190
             * onKernelRequest above.
191
             */
192 16
            foreach ($this->userIdentifierHeaders as $header) {
193 16
                if (!in_array($header, $vary)) {
194 16
                    $vary[] = $header;
195
                }
196
            }
197
        }
198
199 19
        $response->setVary($vary, true);
200 19
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205 23
    public static function getSubscribedEvents()
206
    {
207
        return array(
208 23
            KernelEvents::RESPONSE => 'onKernelResponse',
209
            KernelEvents::REQUEST => array('onKernelRequest', 7),
210
        );
211
    }
212
}
213