| 
<?phpdeclare(strict_types=1);
 namespace ParagonIE\Chronicle;
 
 use Cache\Adapter\Memcached\MemcachedCachePool;
 use ParagonIE\Chronicle\Exception\CacheMisuseException;
 use ParagonIE\ConstantTime\Base32;
 use Psr\Cache\CacheItemInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\StreamInterface;
 use Slim\Http\Headers;
 use Slim\Http\Response;
 use Slim\Http\Stream;
 
 /**
 * Class ResponseCache
 * @package ParagonIE\Chronicle
 */
 class ResponseCache
 {
 /** @var string $cacheKey */
 private $cacheKey = '';
 
 /** @var int|null $lifetime */
 private $lifetime;
 
 /** @var MemcachedCachePool $memcached */
 private $memcached;
 
 /**
 * ResponseCache constructor.
 * @param int $lifetime
 * @throws \Psr\Cache\InvalidArgumentException
 * @throws CacheMisuseException
 */
 public function __construct(int $lifetime = 0)
 {
 if (!self::isAvailable()) {
 throw new CacheMisuseException('Memcached is not installed.');
 }
 $client = new \Memcached();
 $client->addServer('localhost', 11211);
 if ($lifetime > 0) {
 $this->lifetime = $lifetime;
 }
 $this->memcached = new MemcachedCachePool($client);
 $this->loadCacheKey();
 }
 
 /**
 * @return string
 * @throws \Psr\Cache\InvalidArgumentException
 * @throws CacheMisuseException
 */
 public function loadCacheKey()
 {
 if (!empty($this->cacheKey)) {
 return $this->cacheKey;
 }
 if ($this->memcached->hasItem('ChronicleCacheKey')) {
 /** @var CacheItemInterface $item */
 $item = $this->memcached->getItem('ChronicleCacheKey');
 return (string) $item->get();
 }
 try {
 $key = sodium_crypto_shorthash_keygen();
 } catch (\Throwable $ex) {
 throw new CacheMisuseException('CSPRNG failure', 0, $ex);
 }
 /** @var CacheItemInterface $item */
 $item = $this->memcached->getItem('ChronicleCacheKey');
 $item->set($key);
 $item->expiresAfter(null);
 $this->memcached->save($item);
 return $key;
 }
 
 /**
 * @return bool
 */
 public static function isAvailable(): bool
 {
 return extension_loaded('memcached') && class_exists('Memcached');
 }
 
 /**
 * @param string $input
 * @return string
 * @throws CacheMisuseException
 * @throws \Psr\Cache\InvalidArgumentException
 * @throws \SodiumException
 */
 public function getCacheKey(string $input): string
 {
 return 'Chronicle|' . Base32::encodeUnpadded(
 sodium_crypto_shorthash($input, $this->loadCacheKey())
 );
 }
 
 /**
 * @param string $uri
 * @return Response|null
 * @throws CacheMisuseException
 * @throws \Psr\Cache\InvalidArgumentException
 * @throws \SodiumException
 */
 public function loadResponse(string $uri)
 {
 $key = $this->getCacheKey($uri);
 if (!$this->memcached->hasItem($key)) {
 return null;
 }
 /** @var CacheItemInterface $item */
 $item = $this->memcached->getItem($key);
 /** @var string|null $cached */
 $cached = $item->get();
 if (!is_string($cached)) {
 return null;
 }
 return $this->deserializeResponse($cached);
 }
 
 /**
 * @param string $uri
 * @param ResponseInterface $response
 * @return void
 * @throws CacheMisuseException
 * @throws \Psr\Cache\InvalidArgumentException
 * @throws \SodiumException
 */
 public function saveResponse(string $uri, ResponseInterface $response)
 {
 $key = $this->getCacheKey($uri);
 /** @var CacheItemInterface $item */
 $item = $this->memcached->getItem($key);
 $item->set($this->serializeResponse($response));
 $item->expiresAfter($this->lifetime);
 $this->memcached->save($item);
 }
 
 /**
 * @param string $serialized
 * @return Response
 */
 public function deserializeResponse(string $serialized): Response
 {
 /** @var array<string, string|array|int> $decoded */
 $decoded = json_decode($serialized, true);
 $status = (int) $decoded['status'];
 $headers = (array) $decoded['headers'];
 /** @var string $body */
 $body = $decoded['body'];
 
 return new Response(
 $status,
 new Headers($headers),
 self::fromString($body)
 );
 }
 
 /**
 * Create a Stream object from a string.
 *
 * @param string $input
 * @return StreamInterface
 * @throws \Error
 */
 public static function fromString(string $input): StreamInterface
 {
 /** @var resource $stream */
 $stream = \fopen('php://temp', 'w+');
 if (!\is_resource($stream)) {
 throw new \Error('Could not create stream');
 }
 \fwrite($stream, $input);
 \rewind($stream);
 return new Stream($stream);
 }
 
 /**
 * @param ResponseInterface $response
 * @return string
 */
 public function serializeResponse(ResponseInterface $response): string
 {
 return json_encode([
 'status' => $response->getStatusCode(),
 'headers' => $response->getHeaders(),
 'body' => $response->getBody()->getContents()
 ]);
 }
 }
 
 |