mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge MR !636 into master: Merge async upload bundle into chill-bundles
See https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/636 Merge remote-tracking branch 'origin/async-upload-merge' into upgrade-sf5
This commit is contained in:
commit
b64ac8fd20
5
.changes/unreleased/Feature-20231212-154841.yaml
Normal file
5
.changes/unreleased/Feature-20231212-154841.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
kind: Feature
|
||||
body: '[DX] move async-upload-bundle features into chill-bundles'
|
||||
time: 2023-12-12T15:48:41.954970271+01:00
|
||||
custom:
|
||||
Issue: "221"
|
@ -12,7 +12,6 @@
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-redis": "*",
|
||||
"champs-libres/async-uploader-bundle": "dev-master@dev",
|
||||
"champs-libres/wopi-bundle": "dev-master@dev",
|
||||
"champs-libres/wopi-lib": "dev-master@dev",
|
||||
"doctrine/doctrine-bundle": "^2.1",
|
||||
|
@ -0,0 +1,90 @@
|
||||
<?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\AsyncUpload\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class ConfigureOpenstackObjectStorageCommand extends Command
|
||||
{
|
||||
private readonly string $basePath;
|
||||
|
||||
private readonly string $tempUrlKey;
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client, ParameterBagInterface $parameterBag)
|
||||
{
|
||||
$config = $parameterBag->get('chill_doc_store')['openstack']['temp_url'];
|
||||
|
||||
$this->tempUrlKey = $config['temp_url_key'];
|
||||
$this->basePath = $config['temp_url_base_path'];
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setDescription('Configure openstack container to store documents')
|
||||
->setName('chill:doc-store:configure-openstack')
|
||||
->addOption('os_token', 'o', InputOption::VALUE_REQUIRED, 'Openstack token')
|
||||
->addOption('domain', 'd', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Domain name')
|
||||
;
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
if (!$input->hasOption('os_token')) {
|
||||
$output->writeln('The option os_token is required');
|
||||
|
||||
throw new \RuntimeException('The option os_token is required');
|
||||
}
|
||||
|
||||
if (0 === count($input->getOption('domain'))) {
|
||||
$output->writeln('At least one occurence of option domain is required');
|
||||
|
||||
throw new \RuntimeException('At least one occurence of option domain is required');
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$domains = trim(implode(' ', $input->getOption('domain')));
|
||||
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln(['Domains configured will be', $domains]);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->client->request('POST', $this->basePath, [
|
||||
'headers' => [
|
||||
'X-Auth-Token' => $input->getOption('os_token'),
|
||||
'X-Container-Meta-Access-Control-Allow-Origin' => $domains,
|
||||
'X-Container-Meta-Temp-URL-Key' => $this->tempUrlKey,
|
||||
],
|
||||
]);
|
||||
$response->getContent();
|
||||
} catch (HttpExceptionInterface $e) {
|
||||
$output->writeln('Error');
|
||||
$output->writeln($e->getMessage());
|
||||
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
<?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\AsyncUpload\Driver\OpenstackObjectStore;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Event\TempUrlGenerateEvent;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlGeneratorException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Generate a temp url.
|
||||
*/
|
||||
final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterface
|
||||
{
|
||||
private const CHARACTERS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
private string $base_url;
|
||||
private string $key;
|
||||
private int $max_expire_delay;
|
||||
private int $max_submit_delay;
|
||||
private int $max_post_file_size;
|
||||
private int $max_file_count;
|
||||
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private EventDispatcherInterface $event_dispatcher,
|
||||
private ClockInterface $clock,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$config = $parameterBag->get('chill_doc_store')['openstack']['temp_url'];
|
||||
|
||||
$this->key = $config['temp_url_key'];
|
||||
$this->base_url = $config['temp_url_base_path'];
|
||||
$this->max_expire_delay = $config['max_expires_delay'];
|
||||
$this->max_submit_delay = $config['max_submit_delay'];
|
||||
$this->max_post_file_size = $config['max_post_file_size'];
|
||||
$this->max_file_count = $config['max_post_file_count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TempUrlGeneratorException
|
||||
*/
|
||||
public function generatePost(
|
||||
int $expire_delay = null,
|
||||
int $submit_delay = null,
|
||||
int $max_file_count = 1,
|
||||
): SignedUrlPost {
|
||||
$delay = $expire_delay ?? $this->max_expire_delay;
|
||||
$submit_delay ??= $this->max_submit_delay;
|
||||
|
||||
if ($delay < 2) {
|
||||
throw new TempUrlGeneratorException("The delay of {$delay} is too ".'short (<2 sec) to properly use this token');
|
||||
}
|
||||
|
||||
if ($delay > $this->max_expire_delay) {
|
||||
throw new TempUrlGeneratorException('The given delay is greater than the max delay authorized.');
|
||||
}
|
||||
|
||||
if ($submit_delay < 15) {
|
||||
throw new TempUrlGeneratorException("The submit delay of {$delay} is too ".'short (<15 sec) to properly use this token');
|
||||
}
|
||||
|
||||
if ($submit_delay > $this->max_submit_delay) {
|
||||
throw new TempUrlGeneratorException('The given submit delay is greater than the max submit delay authorized.');
|
||||
}
|
||||
|
||||
if ($max_file_count > $this->max_file_count) {
|
||||
throw new TempUrlGeneratorException('The number of files is greater than the authorized number of files');
|
||||
}
|
||||
|
||||
$expires = $this->clock->now()->add(new \DateInterval('PT'.(string) $delay.'S'));
|
||||
|
||||
$object_name = $this->generateObjectName();
|
||||
|
||||
$g = new SignedUrlPost(
|
||||
$url = $this->generateUrl($object_name),
|
||||
$expires,
|
||||
$this->max_post_file_size,
|
||||
$max_file_count,
|
||||
$submit_delay,
|
||||
'',
|
||||
$object_name,
|
||||
$this->generateSignaturePost($url, $expires)
|
||||
);
|
||||
|
||||
$this->event_dispatcher->dispatch(
|
||||
new TempUrlGenerateEvent($g),
|
||||
);
|
||||
|
||||
$this->logger->info(
|
||||
'generate signature for url',
|
||||
(array) $g
|
||||
);
|
||||
|
||||
return $g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an absolute public url for a GET request on the object.
|
||||
*/
|
||||
public function generate(string $method, string $object_name, int $expire_delay = null): SignedUrl
|
||||
{
|
||||
if ($expire_delay > $this->max_expire_delay) {
|
||||
throw new TempUrlGeneratorException(sprintf('The expire delay (%d) is greater than the max_expire_delay (%d)', $expire_delay, $this->max_expire_delay));
|
||||
}
|
||||
$url = $this->generateUrl($object_name);
|
||||
|
||||
$expires = $this->clock->now()
|
||||
->add(new \DateInterval(sprintf('PT%dS', $expire_delay ?? $this->max_expire_delay)));
|
||||
$args = [
|
||||
'temp_url_sig' => $this->generateSignature($method, $url, $expires),
|
||||
'temp_url_expires' => $expires->format('U'),
|
||||
];
|
||||
$url = $url.'?'.\http_build_query($args);
|
||||
|
||||
$signature = new SignedUrl(strtoupper($method), $url, $expires);
|
||||
|
||||
$this->event_dispatcher->dispatch(
|
||||
new TempUrlGenerateEvent($signature)
|
||||
);
|
||||
|
||||
return $signature;
|
||||
}
|
||||
|
||||
private function generateUrl($relative_path): string
|
||||
{
|
||||
return match (str_ends_with($this->base_url, '/')) {
|
||||
true => $this->base_url.$relative_path,
|
||||
false => $this->base_url.'/'.$relative_path
|
||||
};
|
||||
}
|
||||
|
||||
private function generateObjectName()
|
||||
{
|
||||
// inspiration from https://stackoverflow.com/a/4356295/1572236
|
||||
$charactersLength = strlen(self::CHARACTERS);
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < 21; ++$i) {
|
||||
$randomString .= self::CHARACTERS[random_int(0, $charactersLength - 1)];
|
||||
}
|
||||
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
private function generateSignaturePost($url, \DateTimeImmutable $expires)
|
||||
{
|
||||
$path = \parse_url((string) $url, PHP_URL_PATH);
|
||||
|
||||
$body = sprintf(
|
||||
"%s\n%s\n%s\n%s\n%s",
|
||||
$path,
|
||||
'', // redirect is empty
|
||||
(string) $this->max_post_file_size,
|
||||
(string) $this->max_file_count,
|
||||
$expires->format('U')
|
||||
)
|
||||
;
|
||||
|
||||
$this->logger->debug(
|
||||
'generate signature post',
|
||||
['url' => $body, 'method' => 'POST']
|
||||
);
|
||||
|
||||
return \hash_hmac('sha512', $body, $this->key, false);
|
||||
}
|
||||
|
||||
private function generateSignature($method, $url, \DateTimeImmutable $expires)
|
||||
{
|
||||
if ('POST' === $method) {
|
||||
return $this->generateSignaturePost($url, $expires);
|
||||
}
|
||||
|
||||
$path = \parse_url((string) $url, PHP_URL_PATH);
|
||||
|
||||
$body = sprintf(
|
||||
"%s\n%s\n%s",
|
||||
$method,
|
||||
$expires->format('U'),
|
||||
$path
|
||||
)
|
||||
;
|
||||
|
||||
$this->logger->debug(
|
||||
'generate signature GET',
|
||||
['url' => $body, 'method' => 'GET']
|
||||
);
|
||||
|
||||
return \hash_hmac('sha512', $body, $this->key, false);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?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\AsyncUpload\Event;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
|
||||
final class TempUrlGenerateEvent extends \Symfony\Contracts\EventDispatcher\Event
|
||||
{
|
||||
final public const NAME_GENERATE = 'async_uploader.generate_url';
|
||||
|
||||
public function __construct(private readonly SignedUrl $data) {}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->data->method;
|
||||
}
|
||||
|
||||
public function getData(): SignedUrl
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?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\AsyncUpload\Exception;
|
||||
|
||||
class BadCallToRemoteServer extends \LogicException
|
||||
{
|
||||
public function __construct(string $content, int $statusCode, int $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct("Bad call to remote server: {$statusCode}, {$content}", $code, $previous);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<?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\AsyncUpload\Exception;
|
||||
|
||||
class TempUrlGeneratorException extends \RuntimeException {}
|
@ -0,0 +1,20 @@
|
||||
<?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\AsyncUpload\Exception;
|
||||
|
||||
class TempUrlRemoteServerException extends \RuntimeException
|
||||
{
|
||||
public function __construct(int $statusCode, int $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct('Could not reach remote server: '.(string) $statusCode, $code, $previous);
|
||||
}
|
||||
}
|
38
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php
Normal file
38
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?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\AsyncUpload;
|
||||
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
readonly class SignedUrl
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $method,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $url,
|
||||
public \DateTimeImmutable $expires,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public function getExpires(): int
|
||||
{
|
||||
return $this->expires->getTimestamp();
|
||||
}
|
||||
}
|
54
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php
Normal file
54
src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?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\AsyncUpload;
|
||||
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
readonly class SignedUrlPost extends SignedUrl
|
||||
{
|
||||
public function __construct(
|
||||
string $url,
|
||||
\DateTimeImmutable $expires,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public int $max_file_size,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public int $max_file_count,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public int $submit_delay,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $redirect,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $prefix,
|
||||
|
||||
/**
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
public string $signature,
|
||||
) {
|
||||
parent::__construct('POST', $url, $expires);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?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\AsyncUpload;
|
||||
|
||||
interface TempUrlGeneratorInterface
|
||||
{
|
||||
public function generatePost(
|
||||
int $expire_delay = null,
|
||||
int $submit_delay = null,
|
||||
int $max_file_count = 1
|
||||
): SignedUrlPost;
|
||||
|
||||
public function generate(string $method, string $object_name, int $expire_delay = null): SignedUrl;
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
<?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\AsyncUpload\Templating;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
/**
|
||||
* This class extends the AbstractExtension class and provides Twig filter functions for generating URLs for asynchronous
|
||||
* file uploads.
|
||||
*/
|
||||
class AsyncUploadExtension extends AbstractExtension
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private readonly UrlGeneratorInterface $routingUrlGenerator
|
||||
) {}
|
||||
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new TwigFilter('file_url', $this->computeSignedUrl(...)),
|
||||
new TwigFilter('generate_url', $this->computeGenerateUrl(...)),
|
||||
];
|
||||
}
|
||||
|
||||
public function computeSignedUrl(StoredObject|string $file, string $method = 'GET', int $expiresDelay = null): string
|
||||
{
|
||||
if ($file instanceof StoredObject) {
|
||||
$object_name = $file->getFilename();
|
||||
} else {
|
||||
$object_name = $file;
|
||||
}
|
||||
|
||||
return $this->tempUrlGenerator->generate($method, $object_name, $expiresDelay)->url;
|
||||
}
|
||||
|
||||
public function computeGenerateUrl(StoredObject|string $file, string $method = 'GET', int $expiresDelay = null): string
|
||||
{
|
||||
if ($file instanceof StoredObject) {
|
||||
$object_name = $file->getFilename();
|
||||
} else {
|
||||
$object_name = $file;
|
||||
}
|
||||
|
||||
$args = [
|
||||
'method' => $method,
|
||||
'object_name' => $object_name,
|
||||
];
|
||||
|
||||
if (null !== $expiresDelay) {
|
||||
$args['expires_delay'] = $expiresDelay;
|
||||
}
|
||||
|
||||
return $this->routingUrlGenerator->generate('async_upload.generate_url', $args);
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
<?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\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlGeneratorException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AsyncUploadVoter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final readonly class AsyncUploadController
|
||||
{
|
||||
public function __construct(
|
||||
private TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private SerializerInterface $serializer,
|
||||
private Security $security,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @Route("/asyncupload/temp_url/generate/{method}",
|
||||
* name="async_upload.generate_url")
|
||||
*/
|
||||
public function getSignedUrl(string $method, Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
switch (strtolower($method)) {
|
||||
case 'post':
|
||||
$p = $this->tempUrlGenerator
|
||||
->generatePost(
|
||||
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null,
|
||||
$request->query->has('submit_delay') ? $request->query->getInt('submit_delay') : null
|
||||
)
|
||||
;
|
||||
break;
|
||||
case 'get':
|
||||
case 'head':
|
||||
$object_name = $request->query->get('object_name', null);
|
||||
|
||||
if (null === $object_name) {
|
||||
return (new JsonResponse((object) [
|
||||
'message' => 'the object_name is null',
|
||||
]))
|
||||
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
$p = $this->tempUrlGenerator->generate(
|
||||
$method,
|
||||
$object_name,
|
||||
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return (new JsonResponse((object) ['message' => 'the method '
|
||||
."{$method} is not valid"]))
|
||||
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
} catch (TempUrlGeneratorException $e) {
|
||||
$this->logger->warning('The client requested a temp url'
|
||||
.' which sparkle an error.', [
|
||||
'message' => $e->getMessage(),
|
||||
'expire_delay' => $request->query->getInt('expire_delay', 0),
|
||||
'file_count' => $request->query->getInt('file_count', 1),
|
||||
'method' => $method,
|
||||
]);
|
||||
|
||||
$p = new \stdClass();
|
||||
$p->message = $e->getMessage();
|
||||
$p->status = JsonResponse::HTTP_BAD_REQUEST;
|
||||
|
||||
return new JsonResponse($p, JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(AsyncUploadVoter::GENERATE_SIGNATURE, $p)) {
|
||||
throw new AccessDeniedHttpException('not allowed to generate this signature');
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
Response::HTTP_OK,
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
@ -32,9 +32,10 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
$configuration = new Configuration();
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
|
||||
$container->setParameter('chill_doc_store', $config);
|
||||
|
||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||
$loader->load('services.yaml');
|
||||
$loader->load('services/media.yaml');
|
||||
$loader->load('services/controller.yaml');
|
||||
$loader->load('services/menu.yaml');
|
||||
$loader->load('services/fixtures.yaml');
|
||||
@ -94,7 +95,6 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
|
||||
'routing' => [
|
||||
'resources' => [
|
||||
'@ChillDocStoreBundle/config/routes.yaml',
|
||||
'@ChampsLibresAsyncUploaderBundle/config/routes.yaml',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
|
||||
@ -24,11 +25,68 @@ class Configuration implements ConfigurationInterface
|
||||
public function getConfigTreeBuilder()
|
||||
{
|
||||
$treeBuilder = new TreeBuilder('chill_doc_store');
|
||||
/** @var ArrayNodeDefinition $rootNode */
|
||||
$rootNode = $treeBuilder->getRootNode();
|
||||
|
||||
// Here you should define the parameters that are allowed to
|
||||
// configure your bundle. See the documentation linked above for
|
||||
// more information on that topic.
|
||||
/* @phpstan-ignore-next-line As there are inconsistencies in return types, but the code works... */
|
||||
$rootNode->children()
|
||||
// openstack node
|
||||
->arrayNode('openstack')
|
||||
->info('parameters to authenticate and generate temp url against the openstack object storage service')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
// openstack.temp_url
|
||||
->arrayNode('temp_url')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
// openstack.temp_url.temp_url_key
|
||||
->scalarNode('temp_url_key')
|
||||
->isRequired()->cannotBeEmpty()
|
||||
->info('the temp url key')
|
||||
->end()
|
||||
|
||||
->scalarNode('temp_url_base_path')
|
||||
->isRequired()->cannotBeEmpty()
|
||||
->info('the base path to add **before** the path to media. Must contains the container')
|
||||
->end()
|
||||
|
||||
->scalarNode('container')
|
||||
->info('the container name')
|
||||
->isRequired()->cannotBeEmpty()
|
||||
->end()
|
||||
|
||||
->integerNode('max_post_file_size')
|
||||
->defaultValue(15_000_000)
|
||||
->info('Maximum size of the posted file, in bytes')
|
||||
->end()
|
||||
|
||||
->integerNode('max_post_file_count')
|
||||
->defaultValue(1)
|
||||
->info('Maximum number of files which may be posted at once using a POST operation using async upload')
|
||||
->end()
|
||||
|
||||
->integerNode('max_expires_delay')
|
||||
->defaultValue(180)
|
||||
->info('the maximum of seconds a cryptographic signature '
|
||||
.'will be valid for submitting a file. This should be '
|
||||
.'short, to avoid uploading multiple files')
|
||||
->end()
|
||||
|
||||
->integerNode('max_submit_delay')
|
||||
->defaultValue(3600)
|
||||
->info('the maximum of seconds between the upload of a file and '
|
||||
.'a the submission of the form. This delay will also prevent '
|
||||
.'the check of persistence of uploaded file. Should be long '
|
||||
.'enough for keeping user-friendly forms')
|
||||
->end()
|
||||
|
||||
->end() // end of children 's temp_url
|
||||
->end() // end array temp_url
|
||||
|
||||
->end() // end of children's openstack
|
||||
->end() // end of openstack
|
||||
->end() // end of root's children
|
||||
->end();
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
|
@ -11,8 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Entity;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
|
||||
use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists;
|
||||
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExists;
|
||||
use ChampsLibres\WopiLib\Contract\Entity\Document;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
@ -33,7 +32,7 @@ use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
* message="The file is not stored properly"
|
||||
* )
|
||||
*/
|
||||
class StoredObject implements AsyncFileInterface, Document, TrackCreationInterface
|
||||
class StoredObject implements Document, TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
final public const STATUS_READY = 'ready';
|
||||
|
@ -14,13 +14,10 @@ namespace Chill\DocStoreBundle\Form;
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Entity\Document;
|
||||
use Chill\DocStoreBundle\Entity\DocumentCategory;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
@ -29,33 +26,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AccompanyingCourseDocumentType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
/**
|
||||
* @var ObjectManager
|
||||
*/
|
||||
protected $om;
|
||||
|
||||
/**
|
||||
* @var TranslatableStringHelper
|
||||
*/
|
||||
protected $translatableStringHelper;
|
||||
|
||||
/**
|
||||
* the user running this form.
|
||||
*
|
||||
* @var User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
public function __construct(
|
||||
TranslatableStringHelper $translatableStringHelper
|
||||
) {
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
|
@ -20,23 +20,15 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class DocumentCategoryType extends AbstractType
|
||||
{
|
||||
private $chillBundlesFlipped;
|
||||
|
||||
public function __construct($kernelBundles)
|
||||
{
|
||||
// TODO faire un service dans CHillMain
|
||||
foreach ($kernelBundles as $key => $value) {
|
||||
if (str_starts_with((string) $key, 'Chill')) {
|
||||
$this->chillBundlesFlipped[$value] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$bundles = [
|
||||
'chill-doc-store' => 'chill-doc-store',
|
||||
];
|
||||
|
||||
$builder
|
||||
->add('bundleId', ChoiceType::class, [
|
||||
'choices' => $this->chillBundlesFlipped,
|
||||
'choices' => $bundles,
|
||||
'disabled' => false,
|
||||
])
|
||||
->add('idInsideBundle', null, [
|
||||
@ -44,7 +36,7 @@ class DocumentCategoryType extends AbstractType
|
||||
])
|
||||
->add('documentClass', null, [
|
||||
'disabled' => false,
|
||||
]) // cahcerh par default PersonDocument
|
||||
])
|
||||
->add('name', TranslatableStringFormType::class);
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Form;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Form\Type\AsyncUploaderType;
|
||||
use Chill\DocStoreBundle\Form\Type\AsyncUploaderType;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
@ -26,15 +26,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
class StoredObjectType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
public function __construct(private readonly EntityManagerInterface $em) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
|
@ -0,0 +1,77 @@
|
||||
<?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\Form\Type;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class AsyncUploaderType extends AbstractType
|
||||
{
|
||||
private readonly int $expires_delay;
|
||||
private readonly int $max_submit_delay;
|
||||
private readonly int $max_post_file_size;
|
||||
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $url_generator,
|
||||
ParameterBagInterface $parameters,
|
||||
) {
|
||||
$config = $parameters->get('chill_doc_store')['openstack']['temp_url'];
|
||||
|
||||
$this->expires_delay = $config['max_expires_delay'];
|
||||
$this->max_submit_delay = $config['max_submit_delay'];
|
||||
$this->max_post_file_size = $config['max_post_file_size'];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'expires_delay' => $this->expires_delay,
|
||||
'max_post_size' => $this->max_post_file_size,
|
||||
'submit_delay' => $this->max_submit_delay,
|
||||
'max_files' => 1,
|
||||
'error_bubbling' => false,
|
||||
]);
|
||||
|
||||
$resolver->setAllowedTypes('expires_delay', ['int']);
|
||||
$resolver->setAllowedTypes('max_post_size', ['int']);
|
||||
$resolver->setAllowedTypes('max_files', ['int']);
|
||||
$resolver->setAllowedTypes('submit_delay', ['int']);
|
||||
}
|
||||
|
||||
public function buildView(
|
||||
FormView $view,
|
||||
FormInterface $form,
|
||||
array $options
|
||||
) {
|
||||
$view->vars['attr']['data-async-file-upload'] = true;
|
||||
$view->vars['attr']['data-generate-temp-url-post'] = $this
|
||||
->url_generator->generate('async_upload.generate_url', [
|
||||
'expires_delay' => $options['expires_delay'],
|
||||
'method' => 'post',
|
||||
'submit_delay' => $options['submit_delay'],
|
||||
]);
|
||||
$view->vars['attr']['data-temp-url-get'] = $this->url_generator
|
||||
->generate('async_upload.generate_url', ['method' => 'GET']);
|
||||
$view->vars['attr']['data-max-files'] = $options['max_files'];
|
||||
$view->vars['attr']['data-max-post-size'] = $options['max_post_size'];
|
||||
}
|
||||
|
||||
public function getParent()
|
||||
{
|
||||
return HiddenType::class;
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
<?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\Object;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Form\AsyncFileTransformer\AsyncFileTransformerInterface;
|
||||
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class ObjectToAsyncFileTransformer implements AsyncFileTransformerInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function toAsyncFile($data)
|
||||
{
|
||||
if ($data instanceof StoredObject) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
public function toData(AsyncFileInterface $asyncFile)
|
||||
{
|
||||
$object = $this->em
|
||||
->getRepository(StoredObject::class)
|
||||
->findByFilename($asyncFile->getObjectName());
|
||||
|
||||
return $object ?? (new StoredObject())
|
||||
->setFilename($asyncFile->getObjectName());
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
<?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\Object;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\Persistence\PersistenceCheckerInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class PersistenceChecker implements PersistenceCheckerInterface
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function isPersisted($object_name): bool
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->select('COUNT(m)')
|
||||
->from(StoredObject::class, 'm')
|
||||
->where($qb->expr()->eq('m.filename', ':object_name'))
|
||||
->setParameter('object_name', $object_name);
|
||||
|
||||
return 1 === $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?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\Security\Authorization;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final class AsyncUploadVoter extends Voter
|
||||
{
|
||||
public const GENERATE_SIGNATURE = 'CHILL_DOC_GENERATE_ASYNC_SIGNATURE';
|
||||
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
{
|
||||
return self::GENERATE_SIGNATURE === $attribute && $subject instanceof SignedUrl;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
/** @var SignedUrl $subject */
|
||||
if (!in_array($subject->method, ['POST', 'GET', 'HEAD'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN');
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Base64Url\Base64Url;
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@ -27,7 +27,10 @@ final class StoredObjectManager implements StoredObjectManagerInterface
|
||||
|
||||
private array $inMemory = [];
|
||||
|
||||
public function __construct(private readonly HttpClientInterface $client, private readonly TempUrlGeneratorInterface $tempUrlGenerator) {}
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator
|
||||
) {}
|
||||
|
||||
public function getLastModified(StoredObject $document): \DateTimeInterface
|
||||
{
|
||||
|
@ -0,0 +1,65 @@
|
||||
<?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 AsyncUpload\Command;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Command\ConfigureOpenstackObjectStorageCommand;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ConfigureOpenstackObjectStorageCommandTest extends TestCase
|
||||
{
|
||||
public function testRun(): void
|
||||
{
|
||||
$client = new MockHttpClient(function ($method, $url, $options): MockResponse {
|
||||
self::assertSame('POST', $method);
|
||||
self::assertSame($url, 'https://object.store.example/v1/AUTH/container');
|
||||
|
||||
$headers = $options['headers'];
|
||||
|
||||
self::assertContains('X-Auth-Token: abc', $headers);
|
||||
self::assertContains('X-Container-Meta-Temp-URL-Key: 12345679801234567890', $headers);
|
||||
self::assertContains('X-Container-Meta-Access-Control-Allow-Origin: https://chill.domain.social https://chill2.domain.social', $headers);
|
||||
|
||||
return new MockResponse('', ['http_code' => 204]);
|
||||
});
|
||||
|
||||
$parameters = new ParameterBag([
|
||||
'chill_doc_store' => [
|
||||
'openstack' => [
|
||||
'temp_url' => [
|
||||
'temp_url_key' => '12345679801234567890',
|
||||
'temp_url_base_path' => 'https://object.store.example/v1/AUTH/container',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$command = new ConfigureOpenstackObjectStorageCommand($client, $parameters);
|
||||
|
||||
$tester = new CommandTester($command);
|
||||
|
||||
$status = $tester->execute([
|
||||
'--os_token' => 'abc',
|
||||
'--domain' => ['https://chill.domain.social', 'https://chill2.domain.social'],
|
||||
]);
|
||||
|
||||
self::assertSame(0, $status);
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
<?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 AsyncUpload\Driver\OpenstackObjectStore;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\TempUrlOpenstackGenerator;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class TempUrlOpenstackGeneratorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider dataProviderGenerate
|
||||
*/
|
||||
public function testGenerate(string $baseUrl, \DateTimeImmutable $now, string $key, string $method, string $objectName, int $expireDelay, SignedUrl $expected): void
|
||||
{
|
||||
$logger = new NullLogger();
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$clock = new MockClock($now);
|
||||
$parameters = new ParameterBag(
|
||||
[
|
||||
'chill_doc_store' => [
|
||||
'openstack' => [
|
||||
'temp_url' => [
|
||||
'temp_url_key' => $key,
|
||||
'temp_url_base_path' => $baseUrl,
|
||||
'max_post_file_size' => 150,
|
||||
'max_post_file_count' => 1,
|
||||
'max_expires_delay' => 1800,
|
||||
'max_submit_delay' => 1800,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$generator = new TempUrlOpenstackGenerator(
|
||||
$logger,
|
||||
$eventDispatcher,
|
||||
$clock,
|
||||
$parameters,
|
||||
);
|
||||
|
||||
$signedUrl = $generator->generate($method, $objectName, $expireDelay);
|
||||
|
||||
self::assertEquals($expected, $signedUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderGeneratePost
|
||||
*/
|
||||
public function testGeneratePost(string $baseUrl, \DateTimeImmutable $now, string $key, string $method, string $objectName, int $expireDelay, SignedUrl $expected): void
|
||||
{
|
||||
$logger = new NullLogger();
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$clock = new MockClock($now);
|
||||
$parameters = new ParameterBag(
|
||||
[
|
||||
'chill_doc_store' => [
|
||||
'openstack' => [
|
||||
'temp_url' => [
|
||||
'temp_url_key' => $key,
|
||||
'temp_url_base_path' => $baseUrl,
|
||||
'max_post_file_size' => 150,
|
||||
'max_post_file_count' => 1,
|
||||
'max_expires_delay' => 1800,
|
||||
'max_submit_delay' => 1800,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$generator = new TempUrlOpenstackGenerator(
|
||||
$logger,
|
||||
$eventDispatcher,
|
||||
$clock,
|
||||
$parameters,
|
||||
);
|
||||
|
||||
$signedUrl = $generator->generatePost();
|
||||
|
||||
self::assertEquals('POST', $signedUrl->method);
|
||||
self::assertEquals((int) $clock->now()->format('U') + 1800, $signedUrl->expires->getTimestamp());
|
||||
self::assertEquals(150, $signedUrl->max_file_size);
|
||||
self::assertEquals(1, $signedUrl->max_file_count);
|
||||
self::assertEquals(1800, $signedUrl->submit_delay);
|
||||
self::assertEquals('', $signedUrl->redirect);
|
||||
self::assertGreaterThanOrEqual(20, strlen($signedUrl->prefix));
|
||||
}
|
||||
|
||||
public function dataProviderGenerate(): iterable
|
||||
{
|
||||
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');
|
||||
$expireDelay = 1800;
|
||||
$baseUrls = [
|
||||
'https://objectstore.example/v1/my_account/container/',
|
||||
'https://objectstore.example/v1/my_account/container',
|
||||
];
|
||||
$objectName = 'object';
|
||||
$method = 'GET';
|
||||
$key = 'MYKEY';
|
||||
|
||||
$signedUrl = new SignedUrl(
|
||||
'GET',
|
||||
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
||||
\DateTimeImmutable::createFromFormat('U', '1702043543')
|
||||
);
|
||||
|
||||
foreach ($baseUrls as $baseUrl) {
|
||||
yield [
|
||||
$baseUrl,
|
||||
$now,
|
||||
$key,
|
||||
$method,
|
||||
$objectName,
|
||||
$expireDelay,
|
||||
$signedUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function dataProviderGeneratePost(): iterable
|
||||
{
|
||||
$now = \DateTimeImmutable::createFromFormat('U', '1702041743');
|
||||
$expireDelay = 1800;
|
||||
$baseUrls = [
|
||||
'https://objectstore.example/v1/my_account/container/',
|
||||
'https://objectstore.example/v1/my_account/container',
|
||||
];
|
||||
$objectName = 'object';
|
||||
$method = 'GET';
|
||||
$key = 'MYKEY';
|
||||
|
||||
$signedUrl = new SignedUrlPost(
|
||||
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
|
||||
\DateTimeImmutable::createFromFormat('U', '1702043543'),
|
||||
150,
|
||||
1,
|
||||
1800,
|
||||
'',
|
||||
'abc',
|
||||
'abc'
|
||||
);
|
||||
|
||||
foreach ($baseUrls as $baseUrl) {
|
||||
yield [
|
||||
$baseUrl,
|
||||
$now,
|
||||
$key,
|
||||
$method,
|
||||
$objectName,
|
||||
$expireDelay,
|
||||
$signedUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?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 AsyncUpload\Templating;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Templating\AsyncUploadExtension;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AsyncUploadExtensionTest extends KernelTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private AsyncUploadExtension $asyncUploadExtension;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
||||
$generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any())
|
||||
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
|
||||
|
||||
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
|
||||
$urlGenerator->generate('async_upload.generate_url', Argument::type('array'))
|
||||
->willReturn('url');
|
||||
|
||||
$this->asyncUploadExtension = new AsyncUploadExtension(
|
||||
$generator->reveal(),
|
||||
$urlGenerator->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderStoredObject
|
||||
*/
|
||||
public function testComputeSignedUrl(StoredObject|string $storedObject): void
|
||||
{
|
||||
$actual = $this->asyncUploadExtension->computeSignedUrl($storedObject);
|
||||
|
||||
self::assertStringContainsString('https://object.store.example/container', $actual);
|
||||
self::assertStringContainsString(is_string($storedObject) ? $storedObject : $storedObject->getFilename(), $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderStoredObject
|
||||
*/
|
||||
public function testComputeGenerateUrl(StoredObject|string $storedObject): void
|
||||
{
|
||||
$actual = $this->asyncUploadExtension->computeGenerateUrl($storedObject);
|
||||
|
||||
self::assertEquals('url', $actual);
|
||||
}
|
||||
|
||||
public function dataProviderStoredObject(): iterable
|
||||
{
|
||||
yield [(new StoredObject())->setFilename('blabla')];
|
||||
|
||||
yield ['blabla'];
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
<?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\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Controller\AsyncUploadController;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AsyncUploadVoter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AsyncUploadControllerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testGenerateWhenUserIsNotGranted(): void
|
||||
{
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
$controller = $this->buildAsyncUploadController(false);
|
||||
|
||||
$controller->getSignedUrl('POST', new Request());
|
||||
}
|
||||
|
||||
public function testGeneratePost(): void
|
||||
{
|
||||
$controller = $this->buildAsyncUploadController(true);
|
||||
|
||||
$actual = $controller->getSignedUrl('POST', new Request());
|
||||
$decodedActual = json_decode($actual->getContent(), true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertArrayHasKey('method', $decodedActual);
|
||||
self::assertEquals('POST', $decodedActual['method']);
|
||||
}
|
||||
|
||||
public function testGenerateGet(): void
|
||||
{
|
||||
$controller = $this->buildAsyncUploadController(true);
|
||||
|
||||
$actual = $controller->getSignedUrl('GET', new Request(['object_name' => 'abc']));
|
||||
$decodedActual = json_decode($actual->getContent(), true, JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertArrayHasKey('method', $decodedActual);
|
||||
self::assertEquals('GET', $decodedActual['method']);
|
||||
}
|
||||
|
||||
private function buildAsyncUploadController(
|
||||
bool $isGranted,
|
||||
): AsyncUploadController {
|
||||
$tempUrlGenerator = new class () implements TempUrlGeneratorInterface {
|
||||
public function generatePost(int $expire_delay = null, int $submit_delay = null, int $max_file_count = 1): SignedUrlPost
|
||||
{
|
||||
return new SignedUrlPost(
|
||||
'https://object.store.example',
|
||||
new \DateTimeImmutable('1 hour'),
|
||||
150,
|
||||
1,
|
||||
1800,
|
||||
'',
|
||||
'abc',
|
||||
'abc'
|
||||
);
|
||||
}
|
||||
|
||||
public function generate(string $method, string $object_name, int $expire_delay = null): SignedUrl
|
||||
{
|
||||
return new SignedUrl(
|
||||
$method,
|
||||
'https://object.store.example',
|
||||
new \DateTimeImmutable('1 hour')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$serializer = $this->prophesize(SerializerInterface::class);
|
||||
$serializer->serialize(Argument::type(SignedUrl::class), 'json', Argument::type('array'))
|
||||
->will(fn (array $args): string => json_encode(['method' => $args[0]->method], JSON_THROW_ON_ERROR, 3));
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted(AsyncUploadVoter::GENERATE_SIGNATURE, Argument::type(SignedUrl::class))
|
||||
->willReturn($isGranted);
|
||||
|
||||
return new AsyncUploadController(
|
||||
$tempUrlGenerator,
|
||||
$serializer->reveal(),
|
||||
$security->reveal(),
|
||||
new NullLogger()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?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\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class SignedUrlNormalizerTest extends KernelTestCase
|
||||
{
|
||||
public static NormalizerInterface $normalizer;
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
self::bootKernel();
|
||||
self::$normalizer = self::$container->get(NormalizerInterface::class);
|
||||
}
|
||||
|
||||
public function testNormalizerSignedUrl(): void
|
||||
{
|
||||
$signedUrl = new SignedUrl(
|
||||
'GET',
|
||||
'https://object.store.example/container/object',
|
||||
\DateTimeImmutable::createFromFormat('U', '1700000')
|
||||
);
|
||||
|
||||
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
||||
|
||||
self::assertEqualsCanonicalizing(
|
||||
[
|
||||
'method' => 'GET',
|
||||
'expires' => 1_700_000,
|
||||
'url' => 'https://object.store.example/container/object',
|
||||
],
|
||||
$actual
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
<?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\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class SignedUrlPostNormalizerTest extends KernelTestCase
|
||||
{
|
||||
public static NormalizerInterface $normalizer;
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
self::bootKernel();
|
||||
self::$normalizer = self::$container->get(NormalizerInterface::class);
|
||||
}
|
||||
|
||||
public function testNormalizerSignedUrl(): void
|
||||
{
|
||||
$signedUrl = new SignedUrlPost(
|
||||
'https://object.store.example/container/object',
|
||||
\DateTimeImmutable::createFromFormat('U', '1700000'),
|
||||
15000,
|
||||
1,
|
||||
180,
|
||||
'',
|
||||
'abc',
|
||||
'SiGnaTure'
|
||||
);
|
||||
|
||||
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
||||
|
||||
self::assertEqualsCanonicalizing(
|
||||
[
|
||||
'max_file_size' => 15000,
|
||||
'max_file_count' => 1,
|
||||
'submit_delay' => 180,
|
||||
'redirect' => '',
|
||||
'prefix' => 'abc',
|
||||
'signature' => 'SiGnaTure',
|
||||
'method' => 'POST',
|
||||
'expires' => 1_700_000,
|
||||
'url' => 'https://object.store.example/container/object',
|
||||
],
|
||||
$actual
|
||||
);
|
||||
}
|
||||
}
|
@ -11,7 +11,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManager;
|
||||
@ -163,8 +164,11 @@ final class StoredObjectManagerTest extends TestCase
|
||||
|
||||
private function getTempUrlGenerator(StoredObject $storedObject): TempUrlGeneratorInterface
|
||||
{
|
||||
$response = new \stdClass();
|
||||
$response->url = $storedObject->getFilename();
|
||||
$response = new SignedUrl(
|
||||
'PUT',
|
||||
'https://example.com/'.$storedObject->getFilename(),
|
||||
new \DateTimeImmutable('1 hours')
|
||||
);
|
||||
|
||||
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
||||
|
||||
|
@ -0,0 +1,67 @@
|
||||
<?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\Tests\Validator\Constraints;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExists;
|
||||
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExistsValidator;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AsyncFileExistsValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
protected function createValidator()
|
||||
{
|
||||
$client = new MockHttpClient(function ($method, $url, $options): MockResponse {
|
||||
if (str_contains((string) $url, '404')) {
|
||||
return new MockResponse('', ['http_code' => 404]);
|
||||
}
|
||||
|
||||
return new MockResponse('', ['http_code' => 200]);
|
||||
});
|
||||
|
||||
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
|
||||
$generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any())
|
||||
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
|
||||
|
||||
return new AsyncFileExistsValidator($generator->reveal(), $client);
|
||||
}
|
||||
|
||||
public function testWhenFileExistsIsValid(): void
|
||||
{
|
||||
$this->validator->validate((new StoredObject())->setFilename('present'), new AsyncFileExists());
|
||||
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testWhenFileIsNotPresent(): void
|
||||
{
|
||||
$this->validator->validate(
|
||||
(new StoredObject())->setFilename('is_404'),
|
||||
new AsyncFileExists(['message' => 'my_message'])
|
||||
);
|
||||
|
||||
$this->buildViolation('my_message')->setParameter('{{ filename }}', 'is_404')->assertRaised();
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?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\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
*/
|
||||
final class AsyncFileExists extends Constraint
|
||||
{
|
||||
public string $message = "The file '{{ filename }}' is not stored properly.";
|
||||
|
||||
public function validatedBy()
|
||||
{
|
||||
return AsyncFileExistsValidator::class;
|
||||
}
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return [Constraint::CLASS_CONSTRAINT, Constraint::PROPERTY_CONSTRAINT];
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?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\Validator\Constraints;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\BadCallToRemoteServer;
|
||||
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlRemoteServerException;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final class AsyncFileExistsValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
private readonly HttpClientInterface $client
|
||||
) {}
|
||||
|
||||
public function validate($value, Constraint $constraint): void
|
||||
{
|
||||
if ($value instanceof StoredObject) {
|
||||
$this->validateObject($value->getFilename(), $constraint);
|
||||
} elseif (is_string($value)) {
|
||||
$this->validateObject($value, $constraint);
|
||||
} else {
|
||||
throw new UnexpectedValueException($value, StoredObject::class.' or string');
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateObject(string $file, Constraint $constraint): void
|
||||
{
|
||||
if (!$constraint instanceof AsyncFileExists) {
|
||||
throw new UnexpectedTypeException($constraint, AsyncFileExists::class);
|
||||
}
|
||||
|
||||
$urlHead = $this->tempUrlGenerator->generate(
|
||||
'HEAD',
|
||||
$file,
|
||||
30
|
||||
);
|
||||
|
||||
try {
|
||||
$response = $this->client->request('HEAD', $urlHead->url);
|
||||
|
||||
if (404 === $status = $response->getStatusCode()) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ filename }}', $file)
|
||||
->addViolation();
|
||||
} elseif (500 <= $status) {
|
||||
throw new TempUrlRemoteServerException($response->getStatusCode());
|
||||
} elseif (400 <= $status) {
|
||||
throw new BadCallToRemoteServer($response->getContent(false), $response->getStatusCode());
|
||||
}
|
||||
} catch (HttpExceptionInterface $exception) {
|
||||
if (404 !== $exception->getResponse()->getStatusCode()) {
|
||||
throw $exception;
|
||||
}
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw new TempUrlRemoteServerException(0, previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +1,58 @@
|
||||
parameters:
|
||||
# cl_chill_person.example.class: Chill\PersonBundle\Example
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\DocStoreBundle\Repository\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: "../Repository/"
|
||||
|
||||
Chill\DocStoreBundle\Form\DocumentCategoryType:
|
||||
class: Chill\DocStoreBundle\Form\DocumentCategoryType
|
||||
arguments: [ "%kernel.bundles%" ]
|
||||
tags:
|
||||
- { name: form.type }
|
||||
- { name: doctrine.repository_service }
|
||||
|
||||
Chill\DocStoreBundle\Form\PersonDocumentType:
|
||||
class: Chill\DocStoreBundle\Form\PersonDocumentType
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
# arguments:
|
||||
# - "@chill.main.helper.translatable_string"
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
|
||||
Chill\DocStoreBundle\Security\Authorization\:
|
||||
resource: "./../Security/Authorization"
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: chill.role }
|
||||
|
||||
Chill\DocStoreBundle\Workflow\:
|
||||
resource: './../Workflow/'
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
Chill\DocStoreBundle\Serializer\Normalizer\:
|
||||
autowire: true
|
||||
resource: '../Serializer/Normalizer/'
|
||||
tags:
|
||||
- { name: 'serializer.normalizer', priority: 16 }
|
||||
- { name: serializer.normalizer, priority: 16 }
|
||||
|
||||
Chill\DocStoreBundle\Service\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Service/'
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Manager:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
arguments:
|
||||
$providersForAccompanyingPeriod: !tagged_iterator chill_doc_store.generic_doc_accompanying_period_provider
|
||||
$providersForPerson: !tagged_iterator chill_doc_store.generic_doc_person_provider
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension: ~
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtensionRuntime:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
arguments:
|
||||
$renderers: !tagged_iterator chill_doc_store.generic_doc_renderer
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Providers\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../GenericDoc/Providers/'
|
||||
|
||||
Chill\DocStoreBundle\GenericDoc\Renderer\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../GenericDoc/Renderer/'
|
||||
|
||||
Chill\DocStoreBundle\Validator\:
|
||||
resource: '../Validator'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Driver\:
|
||||
resource: '../AsyncUpload/Driver/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Templating\:
|
||||
resource: '../AsyncUpload/Templating/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\Command\:
|
||||
resource: '../AsyncUpload/Command/'
|
||||
|
||||
Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface:
|
||||
alias: Chill\DocStoreBundle\AsyncUpload\Driver\OpenstackObjectStore\TempUrlOpenstackGenerator
|
||||
|
@ -1,13 +1,15 @@
|
||||
services:
|
||||
Chill\DocStoreBundle\Form\StoredObjectType:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
tags:
|
||||
- { name: form.type }
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType:
|
||||
class: Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType
|
||||
arguments:
|
||||
- "@chill.main.helper.translatable_string"
|
||||
Chill\DocStoreBundle\Form\:
|
||||
resource: '../../Form'
|
||||
|
||||
Chill\DocStoreBundle\Form\PersonDocumentType:
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
|
||||
Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType:
|
||||
tags:
|
||||
- { name: form.type, alias: chill_docstorebundle_form_document }
|
||||
|
@ -1,9 +0,0 @@
|
||||
services:
|
||||
chill_doc_store.persistence_checker:
|
||||
class: Chill\DocStoreBundle\Object\PersistenceChecker
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
|
||||
Chill\DocStoreBundle\Object\ObjectToAsyncFileTransformer:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
||||
|
||||
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
|
||||
use ChampsLibres\WopiBundle\Contracts\AuthorizationManagerInterface;
|
||||
use ChampsLibres\WopiBundle\Contracts\UserManagerInterface;
|
||||
use ChampsLibres\WopiBundle\Service\Wopi as CLWopi;
|
||||
@ -60,8 +59,4 @@ return static function (ContainerConfigurator $container) {
|
||||
->set(UserManager::class);
|
||||
|
||||
$services->alias(UserManagerInterface::class, UserManager::class);
|
||||
|
||||
// TODO: Move this into the async bundle (low priority)
|
||||
$services
|
||||
->alias(TempUrlGeneratorInterface::class, 'async_uploader.temp_url_generator');
|
||||
};
|
||||
|
@ -10,7 +10,6 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
ChampsLibres\AsyncUploaderBundle\ChampsLibresAsyncUploaderBundle::class => ['all' => true],
|
||||
Chill\ActivityBundle\ChillActivityBundle::class => ['all' => true],
|
||||
Chill\AsideActivityBundle\ChillAsideActivityBundle::class => ['all' => true],
|
||||
Chill\CalendarBundle\ChillCalendarBundle::class => ['all' => true],
|
||||
|
@ -1,14 +0,0 @@
|
||||
champs_libres_async_uploader:
|
||||
openstack:
|
||||
os_username: '%env(resolve:OS_USERNAME)%' # Required
|
||||
os_password: '%env(resolve:OS_PASSWORD)%' # Required
|
||||
os_tenant_id: '%env(resolve:OS_TENANT_ID)%' # Required
|
||||
os_region_name: '%env(resolve:OS_REGION_NAME)%' # Required
|
||||
os_auth_url: '%env(resolve:OS_AUTH_URL)%' # Required
|
||||
temp_url:
|
||||
temp_url_key: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required
|
||||
container: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_CONTAINER)%' # Required
|
||||
temp_url_base_path: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_BASE_PATH)%' # Required. Do not forget a trailing slash
|
||||
max_post_file_size: 15000000 # 15Mo (bytes)
|
||||
max_expires_delay: 180
|
||||
max_submit_delay: 3600
|
6
tests/app/config/packages/chill_doc_store.yaml
Normal file
6
tests/app/config/packages/chill_doc_store.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
chill_doc_store:
|
||||
openstack:
|
||||
temp_url:
|
||||
temp_url_key: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_KEY)%' # Required
|
||||
container: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_CONTAINER)%' # Required
|
||||
temp_url_base_path: '%env(resolve:ASYNC_UPLOAD_TEMP_URL_BASE_PATH)%' # Required
|
Loading…
x
Reference in New Issue
Block a user