Completed
Push — 8.x-1.x ( 518784...5a1527 )
by
unknown
28:30
created

HttpCacheEventSubscriber::calculateExpires()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
cc 1
eloc 5
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Drupal\controller_annotations\EventSubscriber;
4
5
use Drupal\controller_annotations\Configuration\Cache;
6
use Symfony\Component\HttpFoundation\Request;
7
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
8
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
9
use Symfony\Component\HttpKernel\KernelEvents;
10
use Symfony\Component\HttpFoundation\Response;
11
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
12
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
13
14
class HttpCacheEventSubscriber implements EventSubscriberInterface
15
{
16
17
    /**
18
     * @var \SplObjectStorage
19
     */
20
    private $lastModifiedDates;
21
22
    /**
23
     * @var \SplObjectStorage
24
     */
25
    private $eTags;
26
27
    /**
28
     * @var ExpressionLanguage
29
     */
30
    private $expressionLanguage;
31
32
    /**
33
     */
34 20
    public function __construct()
35
    {
36 20
        $this->lastModifiedDates = new \SplObjectStorage();
37 20
        $this->eTags = new \SplObjectStorage();
38 20
    }
39
40
    /**
41
     * Handles HTTP validation headers.
42
     *
43
     * @param FilterControllerEvent $event
44
     */
45 11
    public function onKernelController(FilterControllerEvent $event)
46
    {
47 11
        $request = $event->getRequest();
48 11
        if (!$configuration = $request->attributes->get('_cache')) {
49 7
            return;
50
        }
51
52 4
        $response = new Response();
53
54 4
        if ($configuration->getLastModified()) {
55 2
            $this->setLastModified($request, $response, $configuration);
56
        }
57 4
        if ($configuration->getETag()) {
58 2
            $this->setETag($request, $response, $configuration);
59
        }
60 4
        if ($response->isNotModified($request)) {
61 2
            $event->setController(
62 2
              function () use ($response) {
63 2
                  return $response;
64 2
              }
65
            );
66 2
            $event->stopPropagation();
67
        }
68 4
    }
69
70
    /**
71
     * @param Request $request
72
     * @param Response $response
73
     * @param Cache $configuration
74
     */
75 2
    protected function setLastModified(
76
      Request $request,
77
      Response $response,
78
      Cache $configuration
79
    ) {
80 2
        $lastModifiedDate = $this->getExpressionLanguage()->evaluate(
81 2
          $configuration->getLastModified(),
82 2
          $request->attributes->all()
83
        );
84 2
        $response->setLastModified($lastModifiedDate);
0 ignored issues
show
Documentation introduced by
$lastModifiedDate is of type array, but the function expects a null|object<DateTime>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
85 2
        $this->lastModifiedDates[$request] = $lastModifiedDate;
86 2
    }
87
88
    /**
89
     * @param Request $request
90
     * @param Response $response
91
     * @param Cache $configuration
92
     */
93 2
    protected function setETag(
94
      Request $request,
95
      Response $response,
96
      Cache $configuration
97
    ) {
98 2
        $eTag = $this->createETag($request, $configuration);
99 2
        $response->setETag($eTag);
100 2
        $this->eTags[$request] = $eTag;
101 2
    }
102
103
    /**
104
     * @param Request $request
105
     * @param Cache $configuration
106
     *
107
     * @return string
108
     */
109 2
    protected function createETag(Request $request, Cache $configuration)
110
    {
111 2
        return hash(
112 2
          'sha256',
113 2
          $this->getExpressionLanguage()->evaluate(
114 2
            $configuration->getETag(),
115 2
            $request->attributes->all()
116
          )
117
        );
118
    }
119
120
    /**
121
     * @param $age
122
     *
123
     * @return float
124
     */
125 1
    protected function calculateAge($age)
126
    {
127 1
        $now = microtime(true);
128 1
        return ceil(strtotime($age, $now) - $now);
129
    }
130
131
    /**
132
     * Modifies the response to apply HTTP cache headers when needed.
133
     *
134
     * @param FilterResponseEvent $event
135
     */
136 18
    public function onKernelResponse(FilterResponseEvent $event)
137
    {
138 18
        $request = $event->getRequest();
139 18
        if (!$configuration = $request->attributes->get('_cache')) {
140 8
            return;
141
        }
142
143 10
        $response = $event->getResponse();
144 10
        if ($this->hasUncachableStatusCode($response)) {
145 1
            return;
146
        }
147
148 9 View Code Duplication
        if (null !== $age = $configuration->getSMaxAge()) {
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...
149 2
            if (!is_numeric($age)) {
150 1
                $age = $this->calculateAge($configuration->getSMaxAge());
151
            }
152
153 2
            $response->setSharedMaxAge($age);
154
        }
155
156 9 View Code Duplication
        if (null !== $age = $configuration->getMaxAge()) {
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...
157 2
            if (!is_numeric($age)) {
158 1
                $age = $this->calculateAge($configuration->getMaxAge());
159
            }
160
161 2
            $response->setMaxAge($age);
162
        }
163
164 9
        if (null !== $configuration->getExpires()) {
165 1
            $response->setExpires($this->calculateExpires($configuration));
0 ignored issues
show
Security Bug introduced by
It seems like $this->calculateExpires($configuration) targeting Drupal\controller_annota...ber::calculateExpires() can also be of type false; however, Symfony\Component\HttpFo...\Response::setExpires() does only seem to accept null|object<DateTime>, did you maybe forget to handle an error condition?
Loading history...
166
        }
167
168 9
        if (null !== $configuration->getVary()) {
169 1
            $response->setVary($configuration->getVary());
170
        }
171
172 9
        if ($configuration->isPublic()) {
173 1
            $response->setPublic();
174
        }
175
176 9
        if ($configuration->isPrivate()) {
177 1
            $response->setPrivate();
178
        }
179
180 9
        if (isset($this->lastModifiedDates[$request])) {
181 1
            $response->setLastModified($this->lastModifiedDates[$request]);
182
183 1
            unset($this->lastModifiedDates[$request]);
184
        }
185
186 9
        if (isset($this->eTags[$request])) {
187 1
            $response->setETag($this->eTags[$request]);
188
189 1
            unset($this->eTags[$request]);
190
        }
191 9
    }
192
193
    /**
194
     * @param Cache $configuration
195
     *
196
     * @return bool|\DateTime
197
     */
198 1
    protected function calculateExpires(Cache $configuration)
199
    {
200 1
        return \DateTime::createFromFormat(
201 1
          'U',
202 1
          strtotime($configuration->getExpires()),
203 1
          new \DateTimeZone('UTC')
204
        );
205
    }
206
207
    /**
208
     * http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-12#section-3.1
209
     *
210
     * @param Response $response
211
     *
212
     * @return bool
213
     */
214 10
    protected function hasUncachableStatusCode(Response $response)
215
    {
216 10
        if (!in_array(
217 10
          $response->getStatusCode(),
218 10
          [200, 203, 300, 301, 302, 304, 404, 410]
219
        )) {
220 1
            return true;
221
        }
222
223 9
        return false;
224
    }
225
226
    /**
227
     * @codeCoverageIgnore
228
     * @return ExpressionLanguage
229
     */
230
    private function getExpressionLanguage()
231
    {
232
        if (null === $this->expressionLanguage) {
233
            if (!class_exists(ExpressionLanguage::class)) {
234
                throw new \RuntimeException(
235
                  'Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'
236
                );
237
            }
238
            $this->expressionLanguage = new ExpressionLanguage();
239
        }
240
241
        return $this->expressionLanguage;
242
    }
243
244
    /**
245
     * @return array
246
     */
247 7
    public static function getSubscribedEvents()
248
    {
249
        return [
250
          KernelEvents::CONTROLLER => [
251
            ['onKernelController', 0],
252 7
          ],
253
          KernelEvents::RESPONSE => [
254
            ['onKernelResponse', 100],
255
          ],
256
        ];
257
    }
258
}
259