<?php
declare(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()
]);
}
}
|