279 lines
8.9 KiB
PHP

<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Service;
use Base64Url\Base64Url;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
final class StoredObjectManager implements StoredObjectManagerInterface
{
private const ALGORITHM = 'AES-256-CBC';
private array $inMemory = [];
public function __construct(
private readonly HttpClientInterface $client,
private readonly TempUrlGeneratorInterface $tempUrlGenerator
) {}
public function getLastModified(StoredObject $document): \DateTimeInterface
{
if ($this->hasCache($document)) {
$response = $this->getResponseFromCache($document);
} else {
try {
$response = $this
->client
->request(
Request::METHOD_HEAD,
$this
->tempUrlGenerator
->generate(
Request::METHOD_HEAD,
$document->getFilename()
)
->url
);
} catch (TransportExceptionInterface $exception) {
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
}
}
return $this->extractLastModifiedFromResponse($response);
}
public function getContentLength(StoredObject $document): int
{
if ([] === $document->getKeyInfos()) {
if ($this->hasCache($document)) {
$response = $this->getResponseFromCache($document);
} else {
try {
$response = $this
->client
->request(
Request::METHOD_HEAD,
$this
->tempUrlGenerator
->generate(
Request::METHOD_HEAD,
$document->getFilename()
)
->url
);
} catch (TransportExceptionInterface $exception) {
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
}
}
return $this->extractContentLengthFromResponse($response);
}
return strlen($this->read($document));
}
public function etag(StoredObject $document): string
{
if ($this->hasCache($document)) {
$response = $this->getResponseFromCache($document);
} else {
try {
$response = $this
->client
->request(
Request::METHOD_HEAD,
$this
->tempUrlGenerator
->generate(
Request::METHOD_HEAD,
$document->getFilename()
)
->url
);
} catch (TransportExceptionInterface $exception) {
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
}
}
return $this->extractEtagFromResponse($response, $document);
}
public function read(StoredObject $document): string
{
$response = $this->getResponseFromCache($document);
try {
$data = $response->getContent();
} catch (\Throwable $e) {
throw StoredObjectManagerException::unableToGetResponseContent($e);
}
if (false === $this->hasKeysAndIv($document)) {
return $data;
}
$clearData = openssl_decrypt(
$data,
self::ALGORITHM,
// TODO: Why using this library and not use base64_decode() ?
Base64Url::decode($document->getKeyInfos()['k']),
\OPENSSL_RAW_DATA,
pack('C*', ...$document->getIv())
);
if (false === $clearData) {
throw StoredObjectManagerException::unableToDecrypt(openssl_error_string());
}
return $clearData;
}
public function write(StoredObject $document, string $clearContent): void
{
if ($this->hasCache($document)) {
unset($this->inMemory[$document->getUuid()->toString()]);
}
$encryptedContent = $this->hasKeysAndIv($document)
? openssl_encrypt(
$clearContent,
self::ALGORITHM,
// TODO: Why using this library and not use base64_decode() ?
Base64Url::decode($document->getKeyInfos()['k']),
\OPENSSL_RAW_DATA,
pack('C*', ...$document->getIv())
)
: $clearContent;
$headers = [];
if (null !== $document->getDeleteAt()) {
$headers['X-Delete-At'] = $document->getDeleteAt()->getTimestamp();
}
try {
$response = $this
->client
->request(
Request::METHOD_PUT,
$this
->tempUrlGenerator
->generate(
Request::METHOD_PUT,
$document->getFilename()
)
->url,
[
'body' => $encryptedContent,
'headers' => $headers,
]
);
} catch (TransportExceptionInterface $exception) {
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
}
if (Response::HTTP_CREATED !== $response->getStatusCode()) {
throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode());
}
}
public function clearCache(): void
{
$this->inMemory = [];
}
private function extractLastModifiedFromResponse(ResponseInterface $response): \DateTimeImmutable
{
$lastModifiedString = (($response->getHeaders()['last-modified'] ?? [])[0] ?? '');
$date = \DateTimeImmutable::createFromFormat(
\DateTimeImmutable::RFC7231,
$lastModifiedString,
new \DateTimeZone('GMT')
);
if (false === $date) {
throw new \RuntimeException('the date from remote storage could not be parsed: '.$lastModifiedString);
}
return $date;
}
private function extractContentLengthFromResponse(ResponseInterface $response): int
{
return (int) ($response->getHeaders()['content-length'] ?? ['0'])[0];
}
private function extractEtagFromResponse(ResponseInterface $response, StoredObject $storedObject): ?string
{
$etag = ($response->getHeaders()['etag'] ?? [''])[0];
if ('' === $etag) {
return null;
}
return $etag;
}
private function fillCache(StoredObject $document): void
{
try {
$response = $this
->client
->request(
Request::METHOD_GET,
$this
->tempUrlGenerator
->generate(
Request::METHOD_GET,
$document->getFilename()
)
->url
);
} catch (\Throwable $e) {
throw StoredObjectManagerException::errorDuringHttpRequest($e);
}
if (Response::HTTP_OK !== $response->getStatusCode()) {
throw StoredObjectManagerException::invalidStatusCode($response->getStatusCode());
}
$this->inMemory[$document->getUuid()->toString()] = $response;
}
private function getResponseFromCache(StoredObject $document): ResponseInterface
{
if (!$this->hasCache($document)) {
$this->fillCache($document);
}
return $this->inMemory[$document->getUuid()->toString()];
}
private function hasCache(StoredObject $document): bool
{
return \array_key_exists($document->getUuid()->toString(), $this->inMemory);
}
private function hasKeysAndIv(StoredObject $storedObject): bool
{
return ([] !== $storedObject->getKeyInfos()) && ([] !== $storedObject->getIv());
}
}