annotate vendor/symfony/http-kernel/HttpCache/Store.php @ 0:4c8ae668cc8c

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