Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 399 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * This code is partially based on the Rack-Cache library by Ryan Tomayko,
9
 * which is released under the MIT license.
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
 
15
namespace Symfony\Component\HttpKernel\HttpCache;
16
 
17
use Symfony\Component\HttpFoundation\Request;
18
use Symfony\Component\HttpFoundation\Response;
19
 
20
/**
21
 * Store implements all the logic for storing cache metadata (Request and Response headers).
22
 *
23
 * @author Fabien Potencier <fabien@symfony.com>
24
 */
25
class Store implements StoreInterface
26
{
27
    protected $root;
28
    /** @var \SplObjectStorage<Request, string> */
29
    private \SplObjectStorage $keyCache;
30
    /** @var array<string, resource> */
31
    private array $locks = [];
32
 
33
    /**
34
     * @throws \RuntimeException
35
     */
36
    public function __construct(string $root)
37
    {
38
        $this->root = $root;
39
        if (!is_dir($this->root) && !@mkdir($this->root, 0777, true) && !is_dir($this->root)) {
40
            throw new \RuntimeException(sprintf('Unable to create the store directory (%s).', $this->root));
41
        }
42
        $this->keyCache = new \SplObjectStorage();
43
    }
44
 
45
    /**
46
     * Cleanups storage.
47
     */
48
    public function cleanup()
49
    {
50
        // unlock everything
51
        foreach ($this->locks as $lock) {
52
            flock($lock, \LOCK_UN);
53
            fclose($lock);
54
        }
55
 
56
        $this->locks = [];
57
    }
58
 
59
    /**
60
     * Tries to lock the cache for a given Request, without blocking.
61
     *
62
     * @return bool|string true if the lock is acquired, the path to the current lock otherwise
63
     */
64
    public function lock(Request $request): bool|string
65
    {
66
        $key = $this->getCacheKey($request);
67
 
68
        if (!isset($this->locks[$key])) {
69
            $path = $this->getPath($key);
70
            if (!is_dir(\dirname($path)) && false === @mkdir(\dirname($path), 0777, true) && !is_dir(\dirname($path))) {
71
                return $path;
72
            }
73
            $h = fopen($path, 'c');
74
            if (!flock($h, \LOCK_EX | \LOCK_NB)) {
75
                fclose($h);
76
 
77
                return $path;
78
            }
79
 
80
            $this->locks[$key] = $h;
81
        }
82
 
83
        return true;
84
    }
85
 
86
    /**
87
     * Releases the lock for the given Request.
88
     *
89
     * @return bool False if the lock file does not exist or cannot be unlocked, true otherwise
90
     */
91
    public function unlock(Request $request): bool
92
    {
93
        $key = $this->getCacheKey($request);
94
 
95
        if (isset($this->locks[$key])) {
96
            flock($this->locks[$key], \LOCK_UN);
97
            fclose($this->locks[$key]);
98
            unset($this->locks[$key]);
99
 
100
            return true;
101
        }
102
 
103
        return false;
104
    }
105
 
106
    public function isLocked(Request $request): bool
107
    {
108
        $key = $this->getCacheKey($request);
109
 
110
        if (isset($this->locks[$key])) {
111
            return true; // shortcut if lock held by this process
112
        }
113
 
114
        if (!is_file($path = $this->getPath($key))) {
115
            return false;
116
        }
117
 
118
        $h = fopen($path, 'r');
119
        flock($h, \LOCK_EX | \LOCK_NB, $wouldBlock);
120
        flock($h, \LOCK_UN); // release the lock we just acquired
121
        fclose($h);
122
 
123
        return (bool) $wouldBlock;
124
    }
125
 
126
    /**
127
     * Locates a cached Response for the Request provided.
128
     */
129
    public function lookup(Request $request): ?Response
130
    {
131
        $key = $this->getCacheKey($request);
132
 
133
        if (!$entries = $this->getMetadata($key)) {
134
            return null;
135
        }
136
 
137
        // find a cached entry that matches the request.
138
        $match = null;
139
        foreach ($entries as $entry) {
140
            if ($this->requestsMatch(isset($entry[1]['vary'][0]) ? implode(', ', $entry[1]['vary']) : '', $request->headers->all(), $entry[0])) {
141
                $match = $entry;
142
 
143
                break;
144
            }
145
        }
146
 
147
        if (null === $match) {
148
            return null;
149
        }
150
 
151
        $headers = $match[1];
152
        if (file_exists($path = $this->getPath($headers['x-content-digest'][0]))) {
153
            return $this->restoreResponse($headers, $path);
154
        }
155
 
156
        // TODO the metaStore referenced an entity that doesn't exist in
157
        // the entityStore. We definitely want to return nil but we should
158
        // also purge the entry from the meta-store when this is detected.
159
        return null;
160
    }
161
 
162
    /**
163
     * Writes a cache entry to the store for the given Request and Response.
164
     *
165
     * Existing entries are read and any that match the response are removed. This
166
     * method calls write with the new list of cache entries.
167
     *
168
     * @throws \RuntimeException
169
     */
170
    public function write(Request $request, Response $response): string
171
    {
172
        $key = $this->getCacheKey($request);
173
        $storedEnv = $this->persistRequest($request);
174
 
175
        if ($response->headers->has('X-Body-File')) {
176
            // Assume the response came from disk, but at least perform some safeguard checks
177
            if (!$response->headers->has('X-Content-Digest')) {
178
                throw new \RuntimeException('A restored response must have the X-Content-Digest header.');
179
            }
180
 
181
            $digest = $response->headers->get('X-Content-Digest');
182
            if ($this->getPath($digest) !== $response->headers->get('X-Body-File')) {
183
                throw new \RuntimeException('X-Body-File and X-Content-Digest do not match.');
184
            }
185
            // Everything seems ok, omit writing content to disk
186
        } else {
187
            $digest = $this->generateContentDigest($response);
188
            $response->headers->set('X-Content-Digest', $digest);
189
 
190
            if (!$this->save($digest, $response->getContent(), false)) {
191
                throw new \RuntimeException('Unable to store the entity.');
192
            }
193
 
194
            if (!$response->headers->has('Transfer-Encoding')) {
195
                $response->headers->set('Content-Length', \strlen($response->getContent()));
196
            }
197
        }
198
 
199
        // read existing cache entries, remove non-varying, and add this one to the list
200
        $entries = [];
201
        $vary = $response->headers->get('vary');
202
        foreach ($this->getMetadata($key) as $entry) {
203
            if (!isset($entry[1]['vary'][0])) {
204
                $entry[1]['vary'] = [''];
205
            }
206
 
207
            if ($entry[1]['vary'][0] != $vary || !$this->requestsMatch($vary ?? '', $entry[0], $storedEnv)) {
208
                $entries[] = $entry;
209
            }
210
        }
211
 
212
        $headers = $this->persistResponse($response);
213
        unset($headers['age']);
214
 
215
        array_unshift($entries, [$storedEnv, $headers]);
216
 
217
        if (!$this->save($key, serialize($entries))) {
218
            throw new \RuntimeException('Unable to store the metadata.');
219
        }
220
 
221
        return $key;
222
    }
223
 
224
    /**
225
     * Returns content digest for $response.
226
     */
227
    protected function generateContentDigest(Response $response): string
228
    {
229
        return 'en'.hash('xxh128', $response->getContent());
230
    }
231
 
232
    /**
233
     * Invalidates all cache entries that match the request.
234
     *
235
     * @throws \RuntimeException
236
     */
237
    public function invalidate(Request $request)
238
    {
239
        $modified = false;
240
        $key = $this->getCacheKey($request);
241
 
242
        $entries = [];
243
        foreach ($this->getMetadata($key) as $entry) {
244
            $response = $this->restoreResponse($entry[1]);
245
            if ($response->isFresh()) {
246
                $response->expire();
247
                $modified = true;
248
                $entries[] = [$entry[0], $this->persistResponse($response)];
249
            } else {
250
                $entries[] = $entry;
251
            }
252
        }
253
 
254
        if ($modified && !$this->save($key, serialize($entries))) {
255
            throw new \RuntimeException('Unable to store the metadata.');
256
        }
257
    }
258
 
259
    /**
260
     * Determines whether two Request HTTP header sets are non-varying based on
261
     * the vary response header value provided.
262
     *
263
     * @param string|null $vary A Response vary header
264
     * @param array       $env1 A Request HTTP header array
265
     * @param array       $env2 A Request HTTP header array
266
     */
267
    private function requestsMatch(?string $vary, array $env1, array $env2): bool
268
    {
269
        if (empty($vary)) {
270
            return true;
271
        }
272
 
273
        foreach (preg_split('/[\s,]+/', $vary) as $header) {
274
            $key = str_replace('_', '-', strtolower($header));
275
            $v1 = $env1[$key] ?? null;
276
            $v2 = $env2[$key] ?? null;
277
            if ($v1 !== $v2) {
278
                return false;
279
            }
280
        }
281
 
282
        return true;
283
    }
284
 
285
    /**
286
     * Gets all data associated with the given key.
287
     *
288
     * Use this method only if you know what you are doing.
289
     */
290
    private function getMetadata(string $key): array
291
    {
292
        if (!$entries = $this->load($key)) {
293
            return [];
294
        }
295
 
296
        return unserialize($entries) ?: [];
297
    }
298
 
299
    /**
300
     * Purges data for the given URL.
301
     *
302
     * This method purges both the HTTP and the HTTPS version of the cache entry.
303
     *
304
     * @return bool true if the URL exists with either HTTP or HTTPS scheme and has been purged, false otherwise
305
     */
306
    public function purge(string $url): bool
307
    {
308
        $http = preg_replace('#^https:#', 'http:', $url);
309
        $https = preg_replace('#^http:#', 'https:', $url);
310
 
311
        $purgedHttp = $this->doPurge($http);
312
        $purgedHttps = $this->doPurge($https);
313
 
314
        return $purgedHttp || $purgedHttps;
315
    }
316
 
317
    /**
318
     * Purges data for the given URL.
319
     */
320
    private function doPurge(string $url): bool
321
    {
322
        $key = $this->getCacheKey(Request::create($url));
323
        if (isset($this->locks[$key])) {
324
            flock($this->locks[$key], \LOCK_UN);
325
            fclose($this->locks[$key]);
326
            unset($this->locks[$key]);
327
        }
328
 
329
        if (is_file($path = $this->getPath($key))) {
330
            unlink($path);
331
 
332
            return true;
333
        }
334
 
335
        return false;
336
    }
337
 
338
    /**
339
     * Loads data for the given key.
340
     */
341
    private function load(string $key): ?string
342
    {
343
        $path = $this->getPath($key);
344
 
345
        return is_file($path) && false !== ($contents = @file_get_contents($path)) ? $contents : null;
346
    }
347
 
348
    /**
349
     * Save data for the given key.
350
     */
351
    private function save(string $key, string $data, bool $overwrite = true): bool
352
    {
353
        $path = $this->getPath($key);
354
 
355
        if (!$overwrite && file_exists($path)) {
356
            return true;
357
        }
358
 
359
        if (isset($this->locks[$key])) {
360
            $fp = $this->locks[$key];
361
            @ftruncate($fp, 0);
362
            @fseek($fp, 0);
363
            $len = @fwrite($fp, $data);
364
            if (\strlen($data) !== $len) {
365
                @ftruncate($fp, 0);
366
 
367
                return false;
368
            }
369
        } else {
370
            if (!is_dir(\dirname($path)) && false === @mkdir(\dirname($path), 0777, true) && !is_dir(\dirname($path))) {
371
                return false;
372
            }
373
 
374
            $tmpFile = tempnam(\dirname($path), basename($path));
375
            if (false === $fp = @fopen($tmpFile, 'w')) {
376
                @unlink($tmpFile);
377
 
378
                return false;
379
            }
380
            @fwrite($fp, $data);
381
            @fclose($fp);
382
 
383
            if ($data != file_get_contents($tmpFile)) {
384
                @unlink($tmpFile);
385
 
386
                return false;
387
            }
388
 
389
            if (false === @rename($tmpFile, $path)) {
390
                @unlink($tmpFile);
391
 
392
                return false;
393
            }
394
        }
395
 
396
        @chmod($path, 0666 & ~umask());
397
 
398
        return true;
399
    }
400
 
401
    public function getPath(string $key)
402
    {
403
        return $this->root.\DIRECTORY_SEPARATOR.substr($key, 0, 2).\DIRECTORY_SEPARATOR.substr($key, 2, 2).\DIRECTORY_SEPARATOR.substr($key, 4, 2).\DIRECTORY_SEPARATOR.substr($key, 6);
404
    }
405
 
406
    /**
407
     * Generates a cache key for the given Request.
408
     *
409
     * This method should return a key that must only depend on a
410
     * normalized version of the request URI.
411
     *
412
     * If the same URI can have more than one representation, based on some
413
     * headers, use a Vary header to indicate them, and each representation will
414
     * be stored independently under the same cache key.
415
     */
416
    protected function generateCacheKey(Request $request): string
417
    {
418
        return 'md'.hash('sha256', $request->getUri());
419
    }
420
 
421
    /**
422
     * Returns a cache key for the given Request.
423
     */
424
    private function getCacheKey(Request $request): string
425
    {
426
        if (isset($this->keyCache[$request])) {
427
            return $this->keyCache[$request];
428
        }
429
 
430
        return $this->keyCache[$request] = $this->generateCacheKey($request);
431
    }
432
 
433
    /**
434
     * Persists the Request HTTP headers.
435
     */
436
    private function persistRequest(Request $request): array
437
    {
438
        return $request->headers->all();
439
    }
440
 
441
    /**
442
     * Persists the Response HTTP headers.
443
     */
444
    private function persistResponse(Response $response): array
445
    {
446
        $headers = $response->headers->all();
447
        $headers['X-Status'] = [$response->getStatusCode()];
448
 
449
        return $headers;
450
    }
451
 
452
    /**
453
     * Restores a Response from the HTTP headers and body.
454
     */
455
    private function restoreResponse(array $headers, string $path = null): Response
456
    {
457
        $status = $headers['X-Status'][0];
458
        unset($headers['X-Status']);
459
 
460
        if (null !== $path) {
461
            $headers['X-Body-File'] = [$path];
462
        }
463
 
464
        return new Response($path, $status, $headers);
465
    }
466
}