ZV @ qWyVPP BoVp <y`VpP pVO'` poV <VP 2oV <SV V`p EVpx EWyV`P B"GV` \VP GLV@ HXwV` toV <VP 2oV <SV "GV mV +LV` KVP EWyVP BoV <y[V >LV KV ELV  GXwV P tV EWyVP BoV0 <V0` 2oV@ <y[V@ >|JV@ >5V`!@!8;]A5V@!!fA!!\A@!!\A(P!PP!!`!![!!!!M^AM^A!!!!\A@!!\A~P!P!~P!`~P!~P!!!!}P!!!eAH}P!P!!@!;?VP1`p/RmVp@/+|JV01>?V 4V4V05EoV5<y`V85pƃVP5tƃV`5tIV5<?V4/ H>Ry`V`>pƃVP>tƃV`>tIV><?V4/ `p?Ry`VP?pƃVPP?tIV?<?V4/ @Ry`V@pƃVP@tIV @<?V4/ ARy`VApƃVPAtIV A<?V4/ 0BRy`V0BpƃVPBtIV B<?V4/ `PCRy`VPP(CpƃVPPCtIVC<?V4/ p8FRy`VpPFpƃVPFtIVF<V`HpIVH<tVH*IVPpJkVxVEƃVPVtWyVP`VBIVV<tVV*IV@`WkV@cEƃVPPctWyV``cBIVc<VkEƃVPktIVk<|JVm>|JVn>ȟP!hP!v !v !0=! =! =!S !tory = $this->options['lock_factory']; } /** * Locates a cached Response for the Request provided. * * @param Request $request A Request instance * * @return Response|null A Response instance, or null if no cache entry was found */ public function lookup(Request $request): ?Response { $cacheKey = $this->getCacheKey($request); $item = $this->cache->getItem($cacheKey); if (!$item->isHit()) { return null; } $entries = $item->get(); foreach ($entries as $varyKeyResponse => $responseData) { // This can only happen if one entry only if (self::NON_VARYING_KEY === $varyKeyResponse) { return $this->restoreResponse($responseData); } // Otherwise we have to see if Vary headers match $varyKeyRequest = $this->getVaryKey( $responseData['vary'], $request ); if ($varyKeyRequest === $varyKeyResponse) { return $this->restoreResponse($responseData); } } return null; } /** * Writes a cache entry to the store for the given Request and Response. * * Existing entries are read and any that match the response are removed. This * method calls write with the new list of cache entries. * * @param Request $request A Request instance * @param Response $response A Response instance * * @return string The key under which the response is stored */ public function write(Request $request, Response $response): string { if (null === $response->getMaxAge()) { throw new \InvalidArgumentException('HttpCache should not forward any response without any cache expiration time to the store.'); } // Save the content digest if required $this->saveContentDigest($response); $cacheKey = $this->getCacheKey($request); $headers = $response->headers->all(); unset($headers['age']); $item = $this->cache->getItem($cacheKey); if (!$item->isHit()) { $entries = []; } else { $entries = $item->get(); } // Add or replace entry with current Vary header key $varyKey = $this->getVaryKey($response->getVary(), $request); $entries[$varyKey] = [ 'vary' => $response->getVary(), 'headers' => $headers, 'status' => $response->getStatusCode(), 'uri' => $request->getUri(), // For debugging purposes ]; // Add content if content digests are disabled if (!$this->options['generate_content_digests']) { $entries[$varyKey]['content'] = $response->getContent(); } // If the response has a Vary header we remove the non-varying entry if ($response->hasVary()) { unset($entries[self::NON_VARYING_KEY]); } // Tags $tags = []; foreach ($response->headers->all($this->options['cache_tags_header']) as $header) { foreach (explode(',', $header) as $tag) { $tags[] = $tag; } } // Prune expired entries on file system if needed $this->autoPruneExpiredEntries(); $this->saveDeferred($item, $entries, $response->getMaxAge(), $tags); // Commit all deferred cache items $this->cache->commit(); return $cacheKey; } /** * Invalidates all cache entries that match the request. * * @param Request $request A Request instance */ public function invalidate(Request $request): void { $cacheKey = $this->getCacheKey($request); $this->cache->deleteItem($cacheKey); } /** * Locks the cache for a given Request. * * @param Request $request A Request instance * * @return bool|string true if the lock is acquired, the path to the current lock otherwise */ public function lock(Request $request) { $cacheKey = $this->getCacheKey($request); if (isset($this->locks[$cacheKey])) { return false; } $this->locks[$cacheKey] = $this->lockFactory ->createLock($cacheKey); return $this->locks[$cacheKey]->acquire(); } /** * Releases the lock for the given Request. * * @param Request $request A Request instance * * @return bool False if the lock file does not exist or cannot be unlocked, true otherwise */ public function unlock(Request $request): bool { $cacheKey = $this->getCacheKey($request); if (!isset($this->locks[$cacheKey])) { return false; } try { $this->locks[$cacheKey]->release(); } catch (LockReleasingException $e) { return false; } finally { unset($this->locks[$cacheKey]); } return true; } /** * Returns whether or not a lock exists. * * @param Request $request A Request instance * * @return bool true if lock exists, false otherwise */ public function isLocked(Request $request): bool { $cacheKey = $this->getCacheKey($request); if (!isset($this->locks[$cacheKey])) { return false; } return $this->locks[$cacheKey]->isAcquired(); } /** * Purges data for the given URL. * * @param string $url A URL * * @return bool true if the URL exists and has been purged, false otherwise */ public function purge($url): bool { $cacheKey = $this->getCacheKey(Request::create($url)); return $this->cache->deleteItem($cacheKey); } /** * Release all locks. * * {@inheritdoc} */ public function cleanup(): void { try { foreach ($this->locks as $lock) { $lock->release(); } } catch (LockReleasingException $e) { // noop } finally { $this->locks = []; } } /** * The tags are set from the header configured in cache_tags_header. * * {@inheritdoc} */ public function invalidateTags(array $tags): bool { if (!$this->cache instanceof TagAwareAdapterInterface) { throw new \RuntimeException('Cannot invalidate tags on a cache implementation that does not implement the TagAwareAdapterInterface.'); } try { return $this->cache->invalidateTags($tags); } catch (CacheInvalidArgumentException $e) { return false; } } /** * {@inheritdoc} */ public function prune(): void { if (!$this->cache instanceof PruneableInterface) { return; } // Make sure we do not have multiple clearing or pruning processes running $lock = $this->lockFactory->createLock(self::CLEANUP_LOCK_KEY); if ($lock->acquire()) { $this->cache->prune(); $lock->release(); } } /** * {@inheritdoc} */ public function clear(): void { // Make sure we do not have multiple clearing or pruning processes running $lock = $this->lockFactory->createLock(self::CLEANUP_LOCK_KEY); if ($lock->acquire()) { $this->cache->clear(); $lock->release(); } } public function getCacheKey(Request $request): string { // Strip scheme to treat https and http the same $uri = $request->getUri(); $uri = substr($uri, \strlen($request->getScheme().'://')); return 'md'.hash('sha256', $uri); } /** * @internal Do not use in public code, this is for unit testing purposes only */ public function generateContentDigest(Response $response): ?string { if ($response instanceof BinaryFileResponse) { return 'bf'.hash_file('sha256', $response->getFile()->getPathname()); } if (!$this->options['generate_content_digests']) { return null; } return 'en'.hash('sha256', $response->getContent()); } private function getVaryKey(array $vary, Request $request): string { if (0 === \count($vary)) { return self::NON_VARYING_KEY; } // Normalize $vary = array_map('strtolower', $vary); sort($vary); $hashData = ''; foreach ($vary as $headerName) { if ('cookie' === $headerName) { continue; } $hashData .= $headerName.':'.$request->headers->get($headerName); } if (\in_array('cookie', $vary, true)) { $hashData .= 'cookies:'; foreach ($request->cookies->all() as $k => $v) { $hashData .= $k.'='.$v; } } return hash('sha256', $hashData); } private function saveContentDigest(Response $response): void { if ($response->headers->has('X-Content-Digest')) { return; } $contentDigest = $this->generateContentDigest($response); if (null === $contentDigest) { return; } $digestCacheItem = $this->cache->getItem($contentDigest); if ($digestCacheItem->isHit()) { $cacheValue = $digestCacheItem->get(); // BC if (\is_string($cacheValue)) { $cacheValue = [ 'expires' => 0, // Forces update to the new format 'contents' => $cacheValue, ]; } } else { $cacheValue = [ 'expires' => 0, // Forces storing the new entry 'contents' => $this->isBinaryFileResponseContentDigest($contentDigest) ? $response->getFile()->getPathname() : $response->getContent(), ]; } $responseMaxAge = (int) $response->getMaxAge(); // Update expires key and save the entry if required if ($responseMaxAge > $cacheValue['expires']) { $cacheValue['expires'] = $responseMaxAge; if (false === $this->saveDeferred($digestCacheItem, $cacheValue, $responseMaxAge)) { throw new \RuntimeException('Unable to store the entity.'); } } $response->headers->set('X-Content-Digest', $contentDigest); // Make sure the content-length header is present if (!$response->headers->has('Transfer-Encoding')) { $response->headers->set('Content-Length', \strlen((string) $response->getContent())); } } /** * Test whether a given digest identifies a BinaryFileResponse. * * @param string $digest */ private function isBinaryFileResponseContentDigest($digest): bool { return 'bf' === substr($digest, 0, 2); } /** * Increases a counter every time a write action is performed and then * prunes expired cache entries if a configurable threshold is reached. * This only happens during write operations so cache retrieval is not * slowed down. */ private function autoPruneExpiredEntries(): void { if (0 === $this->options['prune_threshold']) { return; } $item = $this->cache->getItem(self::COUNTER_KEY); $counter = (int) $item->get(); if ($counter > $this->options['prune_threshold']) { $this->prune(); $counter = 0; } else { ++$counter; } $item->set($counter); $this->cache->saveDeferred($item); } /** * @param int $expiresAfter * @param array $tags */ private function saveDeferred(CacheItemInterface $item, $data, $expiresAfter = null, $tags = []): bool { $item->set($data); $item->expiresAfter($expiresAfter); if (0 !== \count($tags) && method_exists($item, 'tag')) { $item->tag($tags); } return $this->cache->saveDeferred($item); } /** * Restores a Response from the cached data. * * @param array $cacheData An array containing the cache data */ private function restoreResponse(array $cacheData): ?Response { // Check for content digest header if (!isset($cacheData['headers']['x-content-digest'][0])) { // No digest was generated but the content was stored inline if (isset($cacheData['content'])) { return new Response( $cacheData['content'], $cacheData['status'], $cacheData['headers'] ); } // No content digest and no inline content means we cannot restore the response return null; } $item = $this->cache->getItem($cacheData['headers']['x-content-digest'][0]); if (!$item->isHit()) { return null; } $value = $item->get(); // BC if (\is_string($value)) { $value = ['expires' => 0, 'contents' => $value]; } if ($this->isBinaryFileResponseContentDigest($cacheData['headers']['x-content-digest'][0])) { try { $file = new File($value['contents']); } catch (FileNotFoundException $e) { return null; } return new BinaryFileResponse( $file, $cacheData['status'], $cacheData['headers'] ); } return new Response( $value['contents'], $cacheData['status'], $cacheData['headers'] ); } /** * Build and return a default lock factory for when no explicit factory * was specified. * The default factory uses the best quality lock store that is available * on this system. */ private function getDefaultLockStore(string $cacheDir): PersistingStoreInterface { try { return new SemaphoreStore(); } catch (LockInvalidArgumentException $exception) { return new FlockStore($cacheDir); } } }