Ed_WU@`oqz_WU`Pptz_WUP`qtz_WU@pst_WUo<r_WU\_vp_WUv<I_WUc_p xRHg_WUpx>,T_WUy>`taWUaWU@(aWUx(aWU!aWUraWUaWUdaWUdaWUNaWUxaWUaWU'_WUP?0Q_WUPp f_WUp@~_WU Q_WUr_WUl_WU r_WU8Q_WUPr_WUP,T_WU>8#`aWU8#`aWU8#`aWUhaWU7P"`aWUȜ`aWU'_WUPJ?I_WU@`KRi_WU`0Kp_WUK<I_WU(LRi_WU@LpdU_WUPLj_WULp_WUPLt_WUL<,T_WUM>{~aWUdaWUxdaWU{~aWUUaWUUaWUaWU aWUЬ7P/** * Specifies an item that is to be returned in the query result. * Replaces any previously specified selections, if any. * * * $qb = $conn->getQueryBuilder() * ->select('u.id', 'p.id') * ->from('users', 'u') * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); * * * @param mixed ...$selects The selection expressions. * * '@return $this This QueryBuilder instance. */SaWUSaWU/** * Turns the query being built into a bulk update query that ranges over * a certain table * * * $qb = $conn->getQueryBuilder() * ->update('users', 'u') * ->set('u.password', md5('password')) * ->where('u.id = ?'); * * * @param string $update The table whose rows are subject to the update. * @param string $alias The table alias used in the constructed query. * * @return $this This QueryBuilder instance. *//** * Turns the query being built into an insert query that inserts into * a certain table * * * $qb = $conn->getQueryBuilder() * ->insert('users') * ->values( * array( * 'name' => '?', * 'password' => '?' * ) * ); * * * @param string $insert The table into which the rows should be inserted. * * @return $this This QueryBuilder instance. *//** * Creates and adds a query root corresponding to the table identified by the * given alias, forming a cartesian product with any existing query roots. * * * $qb = $conn->getQueryBuilder() * ->select('u.id') * ->from('users', 'u') * * * @param string|IQueryFunction $from The table. * @param string|null $alias The alias of the table. * * @return $this This QueryBuilder instance. *//** * Sets a new value for a column in a bulk update query. * * * $qb = $conn->getQueryBuilder() * ->update('users', 'u') * ->set('u.password', md5('password')) * ->where('u.id = ?'); * * * @param string $key The column to set. * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc. * * @return $this This QueryBuilder instance. */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); } } }