annotate vendor/symfony/http-kernel/HttpCache/Store.php @ 19:fa3358dc1485 tip

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