Compare commits

..

20 Commits

Author SHA1 Message Date
3fb04594eb add changie [ci-skip] 2023-12-12 15:55:14 +01:00
1065706e60 Re-configure test app 2023-12-12 15:55:14 +01:00
f131572344 Remove references to old async upload bundle 2023-12-12 15:55:14 +01:00
45e1ce034a Add ConfigureOpenstackObjectStorageCommand to ChillDocStoreBundle
Added new command file "ConfigureOpenstackObjectStorageCommand.php" under ChillDocStoreBundle that helps in setting up OpenStack container for document storage. Along with the command, added corresponding test file "ConfigureOpenstackObjectStorageCommandTest.php" to ensure the functionality is working as expected.
2023-12-12 15:55:14 +01:00
82ca321715 Add exceptions for async file handling failures
Introduced two new exceptions, `BadCallToRemoteServer` and `TempUrlRemoteServerException`, to improve error handling in asynchronous file operations. These exceptions are thrown when there are issues with the remote server during an async file operation such as HTTP status codes >= 400 and if the server is unreachable.
2023-12-12 15:55:13 +01:00
28d09a8206 Add form and types to handle async files 2023-12-12 15:55:13 +01:00
1195767eb3 Add Twig AsyncUploadExtension and its associated test
Created AsyncUploadExtension under Chill\DocStoreBundle\AsyncUpload\Templating to provide Twig filter functions for generating URLs for asynchronous file uploads. To ensure correctness, AsyncUploadExtensionTest was implemented in Bundle\ChillDocStoreBundle\Tests\AsyncUpload\Templating. Service definitions were also updated in services.yaml.
2023-12-12 15:55:13 +01:00
4fd5a37df3 Add serializer groups to SignedUrl and SignedUrlPost classes, include tests
Serializer groups have been added to the SignedUrl and SignedUrlPost classes to ensure correct serialization. Two new test files were also included: SignedUrlNormalizerTest and SignedUrlPostNormalizerTest which test the normalization of instances of the classes.
2023-12-12 15:55:13 +01:00
450e7c348b Fix and clean DI configuration for chill_doc_store for usage with AsyncUpload 2023-12-12 15:55:12 +01:00
a70572266f Refactor TempUrlOpenstackGenerator to use parameter bag and Bundle configuration
The TempUrlOpenstackGenerator now uses a ParameterBag for configuration instead of individual parameters. This simplifies the handling of configuration values and makes the code more maintainable. The parameter configuration has also been included in the chill_doc_store configuration array for a unified approach.
2023-12-12 15:55:12 +01:00
d688022825 Add Controller to get asyncUpload signatures
Added new files for handling asynchronous file uploads in ChillDocStoreBundle. The new files include a controller for generating temporary URLs (AsyncUploadController.php), a security authorization file (AsyncUploadVoter.php), and a corresponding test file (AsyncUploadControllerTest.php). These implementations permit asynchronous uploads via POST, GET, and HEAD methods while maintaining security protocols.
2023-12-12 11:36:14 +01:00
264fff5c36 Implement asynchronous upload feature in Openstack Object Store
Those features were previously stored in champs-libres/async-upload-bundle

This commit introduces several new classes within the ChillDocStore bundle for handling asynchronous uploads onto an Openstack Object Store. Specifically, the TempUrlOpenstackGenerator, SignedUrl, TempUrlGeneratorInterface, TempUrlGenerateEvent, and TempUrlGeneratorException classes have been created.

This implementation will allow for generating "temporary URLs", which assist in securely and temporarily uploading resources to the OpenStack Object Store. This feature enables the handling of file uploads in a more scalable and efficient manner in high-volume environments.

Additionally, corresponding unit tests have also been added to ensure the accuracy of this new feature.
2023-12-12 11:36:14 +01:00
91e6b035bd release 2.15.0 2023-12-11 17:23:47 +01:00
2adb6105eb Merge branch '246-do-not-show-confidential-in-list' into 'master'
Filter confidential courses from list of accompanying periods and related

Closes #246

See merge request Chill-Projet/chill-bundles!633
2023-12-11 16:22:38 +00:00
33d187f329 Merge branch '228-fix-export-activity-from-v1' into 'master'
Fix export for people created before 'createdAt' column introduction

Closes #228

See merge request Chill-Projet/chill-bundles!634
2023-12-11 16:22:12 +00:00
8cc93a8b07 add changie [ci-skip] 2023-12-07 23:25:38 +01:00
f7184ca7bb Fix export for people created before 'createdAt' column introduction
This commit introduces a migration to fix the export for individuals who were created before the introduction of the 'createdAt' column. The 'startdate' column is now updated to match the individual's first activity date. This migration is irreversible. It's been added as a response to Issue #228
2023-12-07 23:22:48 +01:00
f0605c6b08 Use dedicated FilterListAccompanyingPeriodHelper to filter confidential course in different lists 2023-12-07 16:18:37 +01:00
54606403b4 Refactor confidential filtering on list to let it usable by other lists 2023-12-07 15:13:54 +01:00
47d829d72d Filter confidential courses from list of accompanying periods 2023-12-06 22:46:50 +01:00
63 changed files with 2100 additions and 457 deletions

View File

@@ -1,5 +0,0 @@
kind: Feature
body: Add export "number of household associate with an exchange"
time: 2023-12-04T16:29:23.341485328+01:00
custom:
Issue: "191"

View File

@@ -1,5 +0,0 @@
kind: Feature
body: 'Export: add dates on the filter "filter course by activity type"'
time: 2023-12-04T18:11:56.906524311+01:00
custom:
Issue: "235"

View 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"

View File

@@ -1,5 +0,0 @@
kind: Fixed
body: Fix error when posting an empty comment on an accompanying period.
time: 2023-11-29T11:34:58.986983057+01:00
custom:
Issue: "214"

View File

@@ -1,5 +0,0 @@
kind: Fixed
body: Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
time: 2023-12-01T09:45:10.744382859+01:00
custom:
Issue: "233"

View File

@@ -1,6 +0,0 @@
kind: Fixed
body: |
Fix "filter aside activity by date"
time: 2023-12-04T17:17:16.553597209+01:00
custom:
Issue: "234"

11
.changes/v2.15.0.md Normal file
View File

@@ -0,0 +1,11 @@
## v2.15.0 - 2023-12-11
### Feature
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
### Fixed
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
* ([#234](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/234)) Fix "filter aside activity by date"
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them

View File

@@ -6,6 +6,24 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v2.15.0 - 2023-12-11
### Feature
* ([#191](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/191)) Add export "number of household associate with an exchange"
* ([#235](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/235)) Export: add dates on the filter "filter course by activity type"
### Fixed
* ([#214](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/214)) Fix error when posting an empty comment on an accompanying period.
* ([#233](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/233)) Fix "filter evaluation by evaluation type" (and add select2 to the list of evaluation types to pick)
* ([#234](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/234)) Fix "filter aside activity by date"
* ([#228](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/228)) Fix export of activity for people created before the introduction of the createdAt column on person (during v1)
* ([#246](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/246)) Do not show activities, evaluations and social work when associated to a confidential accompanying period, except for the users which are allowed to see them
## v2.14.1 - 2023-11-29
### Fixed
* Export: fix list person with custom fields
* ([#100](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/100)) Add a paginator to budget elements (resource and charge types) in the admin
* Fix error in ListEvaluation when "handling agents" are alone
## v2.14.0 - 2023-11-24
### Feature
* ([#161](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/161)) Export: in filter "Filter accompanying period work (social action) by type, goal and result", order the items alphabetically or with the defined order

View File

@@ -12,7 +12,6 @@
"ext-json": "*",
"ext-openssl": "*",
"ext-redis": "*",
"champs-libres/async-uploader-bundle": "dev-sf4#d57134aee8e504a83c902ff0cf9f8d36ac418290",
"champs-libres/wopi-bundle": "dev-master@dev",
"champs-libres/wopi-lib": "dev-master@dev",
"doctrine/doctrine-bundle": "^2.1",

View File

@@ -20,23 +20,18 @@ use Chill\MainBundle\Export\AccompanyingCourseExportHelper;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Export\Helper\TranslatableStringExportLabelHelper;
use Chill\MainBundle\Export\ListInterface;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
class ListActivity implements ListInterface, GroupedExportInterface
final readonly class ListActivity implements ListInterface, GroupedExportInterface
{
private readonly bool $filterStatsByCenters;
public function __construct(
private readonly ListActivityHelper $helper,
private readonly EntityManagerInterface $entityManager,
private readonly TranslatableStringExportLabelHelper $translatableStringExportLabelHelper,
ParameterBagInterface $parameterBag,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
private ListActivityHelper $helper,
private EntityManagerInterface $entityManager,
private TranslatableStringExportLabelHelper $translatableStringExportLabelHelper,
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
) {}
public function buildForm(FormBuilderInterface $builder)
{
@@ -119,19 +114,7 @@ class ListActivity implements ListInterface, GroupedExportInterface
->leftJoin('acppart.person', 'person')
->andWhere('acppart.startDate != acppart.endDate OR acppart.endDate IS NULL');
if ($this->filterStatsByCenters) {
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1
FROM '.PersonCenterHistory::class.' acl_count_person_history
WHERE acl_count_person_history.person = person
AND acl_count_person_history.center IN (:authorized_centers)
'
)
)
->setParameter('authorized_centers', $centers);
}
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
$qb
// some grouping are necessary

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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 {}

View File

@@ -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);
}
}

View 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();
}
}

View 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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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
);
}
}

View File

@@ -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',
],
],
]);

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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)
{

View File

@@ -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);
}

View File

@@ -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)
{

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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');
}
}

View File

@@ -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
{

View File

@@ -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);
}
}

View File

@@ -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,
];
}
}
}

View File

@@ -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'];
}
}

View File

@@ -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()
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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];
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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"
tags:
- { name: form.type, alias: chill_docstorebundle_form_document }
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 }

View File

@@ -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'

View File

@@ -19,28 +19,22 @@ use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
{
private bool $filterStatsByCenters;
public function __construct(
private EntityManagerInterface $entityManager,
private RollingDateConverterInterface $rollingDateConverter,
private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper,
ParameterBagInterface $parameterBag,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
) {}
public function buildForm(FormBuilderInterface $builder)
{
@@ -101,8 +95,6 @@ final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExp
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$centers = array_map(static fn ($el) => $el['center'], $acl);
$qb = $this->entityManager->createQueryBuilder();
$qb
@@ -110,18 +102,7 @@ final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExp
->andWhere('acp.step != :list_acp_step')
->setParameter('list_acp_step', AccompanyingPeriod::STEP_DRAFT);
if ($this->filterStatsByCenters) {
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM '.AccompanyingPeriodParticipation::class.' acl_count_part
JOIN '.PersonCenterHistory::class.' acl_count_person_history WITH IDENTITY(acl_count_person_history.person) = IDENTITY(acl_count_part.person)
WHERE acl_count_part.accompanyingPeriod = acp.id AND acl_count_person_history.center IN (:authorized_centers)
'
)
)
->setParameter('authorized_centers', $centers);
}
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
$this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date']));

View File

@@ -28,11 +28,11 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkGoal;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkReferrerHistory;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Chill\PersonBundle\Entity\SocialWork\Goal;
use Chill\PersonBundle\Entity\SocialWork\Result;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
@@ -44,10 +44,9 @@ use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod implements ListInterface, GroupedExportInterface
final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod implements ListInterface, GroupedExportInterface
{
private const FIELDS = [
'id',
@@ -79,8 +78,6 @@ class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod implements L
'updatedBy',
];
private readonly bool $filterStatsByCenters;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly DateTimeHelper $dateTimeHelper,
@@ -94,10 +91,8 @@ class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod implements L
private readonly RollingDateConverterInterface $rollingDateConverter,
private readonly AggregateStringHelper $aggregateStringHelper,
private readonly SocialActionRepository $socialActionRepository,
ParameterBagInterface $parameterBag,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
private readonly FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
) {}
public function buildForm(FormBuilderInterface $builder)
{
@@ -223,17 +218,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod implements L
->andWhere('acppart.startDate <= :calc_date AND (acppart.endDate > :calc_date OR acppart.endDate IS NULL)')
->setParameter('calc_date', $this->rollingDateConverter->convert($calcDate));
if ($this->filterStatsByCenters) {
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person
AND acl_count_person_history.center IN (:authorized_centers)
'
)
)
->setParameter('authorized_centers', $centers);
}
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb);

View File

@@ -28,11 +28,11 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkGoal;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkReferrerHistory;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Chill\PersonBundle\Entity\SocialWork\Goal;
use Chill\PersonBundle\Entity\SocialWork\Result;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
@@ -44,10 +44,9 @@ use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
class ListAccompanyingPeriodWorkAssociatePersonOnWork implements ListInterface, GroupedExportInterface
final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements ListInterface, GroupedExportInterface
{
private const FIELDS = [
'id',
@@ -79,8 +78,6 @@ class ListAccompanyingPeriodWorkAssociatePersonOnWork implements ListInterface,
'updatedBy',
];
private readonly bool $filterStatsByCenters;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly DateTimeHelper $dateTimeHelper,
@@ -94,10 +91,8 @@ class ListAccompanyingPeriodWorkAssociatePersonOnWork implements ListInterface,
private readonly RollingDateConverterInterface $rollingDateConverter,
private readonly AggregateStringHelper $aggregateStringHelper,
private readonly SocialActionRepository $socialActionRepository,
ParameterBagInterface $parameterBag,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
) {}
public function buildForm(FormBuilderInterface $builder)
{
@@ -218,17 +213,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnWork implements ListInterface,
->join('acpw.persons', 'person')
;
if ($this->filterStatsByCenters) {
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person
AND acl_count_person_history.center IN (:authorized_centers)
'
)
)
->setParameter('authorized_centers', $centers);
}
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb);

View File

@@ -26,8 +26,8 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
@@ -37,10 +37,9 @@ use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
class ListEvaluation implements ListInterface, GroupedExportInterface
final readonly class ListEvaluation implements ListInterface, GroupedExportInterface
{
private const FIELDS = [
'id',
@@ -69,8 +68,6 @@ class ListEvaluation implements ListInterface, GroupedExportInterface
'updatedBy',
];
private readonly bool $filterStatsByCenters;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly SocialIssueRender $socialIssueRender,
@@ -83,10 +80,8 @@ class ListEvaluation implements ListInterface, GroupedExportInterface
private readonly TranslatableStringExportLabelHelper $translatableStringExportLabelHelper,
private readonly AggregateStringHelper $aggregateStringHelper,
private readonly RollingDateConverterInterface $rollingDateConverter,
ParameterBagInterface $parameterBag,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
) {}
public function buildForm(FormBuilderInterface $builder)
{
@@ -211,17 +206,7 @@ class ListEvaluation implements ListInterface, GroupedExportInterface
->andWhere('acppart.startDate <= :calc_date AND (acppart.endDate > :calc_date OR acppart.endDate IS NULL)')
->setParameter('calc_date', $this->rollingDateConverter->convert($calcDate));
if ($this->filterStatsByCenters) {
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person
AND acl_count_person_history.center IN (:authorized_centers)
'
)
)
->setParameter('authorized_centers', $centers);
}
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb);

View File

@@ -20,15 +20,14 @@ use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
use Chill\PersonBundle\Export\Helper\ListPersonHelper;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use DateTimeImmutable;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
/**
@@ -36,17 +35,13 @@ use Symfony\Component\Form\FormBuilderInterface;
*/
final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInterface, GroupedExportInterface
{
private bool $filterStatsByCenters;
public function __construct(
private ListPersonHelper $listPersonHelper,
private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper,
private EntityManagerInterface $entityManager,
private RollingDateConverterInterface $rollingDateConverter,
ParameterBagInterface $parameterBag,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
private FilterListAccompanyingPeriodHelperInterface $filterListAccompanyingPeriodHelper,
) {}
public function buildForm(FormBuilderInterface $builder)
{
@@ -122,14 +117,7 @@ final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInte
->join('acppart.accompanyingPeriod', 'acp')
->andWhere($qb->expr()->neq('acp.step', "'".AccompanyingPeriod::STEP_DRAFT."'"));
if ($this->filterStatsByCenters) {
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM '.PersonCenterHistory::class.' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)'
)
)->setParameter('authorized_centers', $centers);
}
$this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data);
$this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date']));
$this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date']));

View File

@@ -0,0 +1,105 @@
<?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\PersonBundle\Export\Helper;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Security;
/**
* Filter accompanying period list and related, removing confidential ones
* based on ACL rules.
*/
final readonly class FilterListAccompanyingPeriodHelper implements FilterListAccompanyingPeriodHelperInterface
{
private bool $filterStatsByCenters;
public function __construct(
private Security $security,
private CenterRepositoryInterface $centerRepository,
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
ParameterBagInterface $parameterBag,
) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
}
public function addFilterAccompanyingPeriods(QueryBuilder &$qb, array $requiredModifiers, array $acl, array $data = []): void
{
$centers = match ($this->filterStatsByCenters) {
true => array_map(static fn ($el) => $el['center'], $acl),
false => $this->centerRepository->findAll(),
};
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new \RuntimeException('only a regular user can run this export');
}
// add filtering on confidential accompanying period. The confidential is applyed on the current status of
// the accompanying period (we do not use the 'calc_date' here
$aclConditionsOrX = $qb->expr()->orX(
// either the current user is the refferer for the course
'acp.user = :list_acp_current_user',
);
$qb->setParameter('list_acp_current_user', $user);
$i = 0;
foreach ($centers as $center) {
$scopes = $this->authorizationHelperForCurrentUser->getReachableScopes(AccompanyingPeriodVoter::SEE_DETAILS, $center);
$scopesConfidential =
$this->authorizationHelperForCurrentUser->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center);
$orScopes = $qb->expr()->orX();
foreach ($scopes as $scope) {
$scopeCondition = match (in_array($scope, $scopesConfidential, true)) {
true => ":scope_{$i} MEMBER OF acp.scopes",
false => $qb->expr()->andX(
'acp.confidential = FALSE',
":scope_{$i} MEMBER OF acp.scopes",
),
};
$orScopes->add($scopeCondition);
$qb->setParameter("scope_{$i}", $scope);
++$i;
}
if ($this->filterStatsByCenters) {
$andX = $qb->expr()->andX(
$qb->expr()->exists(
'SELECT 1 FROM '.AccompanyingPeriodParticipation::class." acl_count_part_{$i}
JOIN ".PersonCenterHistory::class." acl_count_person_history_{$i} WITH IDENTITY(acl_count_person_history_{$i}.person) = IDENTITY(acl_count_part_{$i}.person)
WHERE acl_count_part_{$i}.accompanyingPeriod = acp.id AND acl_count_person_history_{$i}.center IN (:authorized_center_{$i})
AND acl_count_person_history_{$i}.startDate <= CURRENT_DATE() AND (acl_count_person_history_{$i}.endDate IS NULL or acl_count_person_history_{$i}.endDate > CURRENT_DATE())
"
),
$orScopes,
);
$qb->setParameter('authorized_center_'.$i, $center);
$aclConditionsOrX->add($andX);
} else {
$aclConditionsOrX->add($orScopes);
}
++$i;
}
$qb->andWhere($aclConditionsOrX);
}
}

View File

@@ -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\PersonBundle\Export\Helper;
use Doctrine\ORM\QueryBuilder;
/**
* Filter accompanying period list and related, removing confidential ones
* based on ACL rules.
*/
interface FilterListAccompanyingPeriodHelperInterface
{
public function addFilterAccompanyingPeriods(QueryBuilder &$qb, array $requiredModifiers, array $acl, array $data = []): void;
}

View File

@@ -11,14 +11,23 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Tests\Export\Export;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Test\Export\AbstractExportTest;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Export\ListAccompanyingPeriod;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelper;
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Security\Core\Security;
/**
* @internal
@@ -27,6 +36,8 @@ use Doctrine\ORM\EntityManagerInterface;
*/
class ListAccompanyingPeriodTest extends AbstractExportTest
{
use ProphecyTrait;
private readonly ListAccompanyingPeriod $listAccompanyingPeriod;
private readonly CenterRepositoryInterface $centerRepository;
@@ -39,12 +50,54 @@ class ListAccompanyingPeriodTest extends AbstractExportTest
public function getExport()
{
/** @var EntityManagerInterface::class $em */
$em = self::$container->get(EntityManagerInterface::class);
$rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
$listAccompanyingPeriodHelper = self::$container->get(ListAccompanyingPeriodHelper::class);
$centerRepository = self::$container->get(CenterRepositoryInterface::class);
$scopeRepository = self::$container->get(ScopeRepositoryInterface::class);
yield new ListAccompanyingPeriod($em, $rollingDateConverter, $listAccompanyingPeriodHelper, $this->getParameters(true));
yield new ListAccompanyingPeriod($em, $rollingDateConverter, $listAccompanyingPeriodHelper, $this->getParameters(false));
// mock security
$user = $em->createQuery('SELECT u FROM '.User::class.' u')
->setMaxResults(1)->getSingleResult();
if (null === $user) {
throw new \RuntimeException('no user found');
}
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user);
// mock authorization helper
$scopes = $scopeRepository->findAll();
$scopesConfidentials = [] !== $scopes ? [$scopes[0]] : [];
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
$authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_DETAILS, Argument::type(Center::class))
->willReturn($scopes);
$authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, Argument::type(Center::class))
->willReturn($scopesConfidentials);
yield new ListAccompanyingPeriod(
$em,
$rollingDateConverter,
$listAccompanyingPeriodHelper,
new FilterListAccompanyingPeriodHelper(
$security->reveal(),
$centerRepository,
$authorizationHelper->reveal(),
$this->getParameters(true)
)
);
yield new ListAccompanyingPeriod(
$em,
$rollingDateConverter,
$listAccompanyingPeriodHelper,
new FilterListAccompanyingPeriodHelper(
$security->reveal(),
$centerRepository,
$authorizationHelper->reveal(),
$this->getParameters(false)
)
);
}
public function getFormData()

View File

@@ -20,6 +20,7 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Test\Export\AbstractExportTest;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Export\ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
@@ -27,6 +28,7 @@ use Chill\PersonBundle\Templating\Entity\SocialActionRender;
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
@@ -35,6 +37,8 @@ use Doctrine\ORM\EntityManagerInterface;
*/
class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriodTest extends AbstractExportTest
{
use ProphecyTrait;
protected function setUp(): void
{
parent::setUp();
@@ -55,6 +59,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriodTest extends
$rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
$aggregateStringHelper = self::$container->get(AggregateStringHelper::class);
$socialActionRepository = self::$container->get(SocialActionRepository::class);
$filterListAccompanyingPeriodHelper = $this->prophesize(FilterListAccompanyingPeriodHelperInterface::class);
yield new ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod(
$entityManager,
@@ -69,23 +74,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriodTest extends
$rollingDateConverter,
$aggregateStringHelper,
$socialActionRepository,
$this->getParameters(true),
);
yield new ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeriod(
$entityManager,
$dateTimeHelper,
$userHelper,
$personHelper,
$thirdPartyHelper,
$translatableStringExportLabelHelper,
$socialIssueRender,
$socialIssueRepository,
$socialActionRender,
$rollingDateConverter,
$aggregateStringHelper,
$socialActionRepository,
$this->getParameters(false),
$filterListAccompanyingPeriodHelper->reveal(),
);
}

View File

@@ -20,6 +20,7 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Test\Export\AbstractExportTest;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Export\ListAccompanyingPeriodWorkAssociatePersonOnWork;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
@@ -27,6 +28,7 @@ use Chill\PersonBundle\Templating\Entity\SocialActionRender;
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
use Chill\ThirdPartyBundle\Export\Helper\LabelThirdPartyHelper;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
@@ -35,6 +37,8 @@ use Doctrine\ORM\EntityManagerInterface;
*/
class ListAccompanyingPeriodWorkAssociatePersonOnWorkTest extends AbstractExportTest
{
use ProphecyTrait;
protected function setUp(): void
{
parent::setUp();
@@ -55,6 +59,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnWorkTest extends AbstractExport
$rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
$aggregateStringHelper = self::$container->get(AggregateStringHelper::class);
$socialActionRepository = self::$container->get(SocialActionRepository::class);
$filterHelper = $this->prophesize(FilterListAccompanyingPeriodHelperInterface::class);
yield new ListAccompanyingPeriodWorkAssociatePersonOnWork(
$entityManager,
@@ -69,23 +74,7 @@ class ListAccompanyingPeriodWorkAssociatePersonOnWorkTest extends AbstractExport
$rollingDateConverter,
$aggregateStringHelper,
$socialActionRepository,
$this->getParameters(true),
);
yield new ListAccompanyingPeriodWorkAssociatePersonOnWork(
$entityManager,
$dateTimeHelper,
$userHelper,
$personHelper,
$thirdPartyHelper,
$translatableStringExportLabelHelper,
$socialIssueRender,
$socialIssueRepository,
$socialActionRender,
$rollingDateConverter,
$aggregateStringHelper,
$socialActionRepository,
$this->getParameters(false),
$filterHelper->reveal(),
);
}

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Tests\Export\Export;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Export\Helper\AggregateStringHelper;
use Chill\MainBundle\Export\Helper\DateTimeHelper;
use Chill\MainBundle\Export\Helper\TranslatableStringExportLabelHelper;
@@ -22,13 +21,14 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Test\Export\AbstractExportTest;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Export\ListEvaluation;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
use Chill\PersonBundle\Export\Helper\LabelPersonHelper;
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
use Chill\PersonBundle\Templating\Entity\SocialActionRender;
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
@@ -37,7 +37,7 @@ use Doctrine\ORM\EntityManagerInterface;
*/
class ListEvaluationTest extends AbstractExportTest
{
private ListEvaluation $listEvaluation;
use ProphecyTrait;
private CenterRepositoryInterface $centerRepository;
@@ -46,7 +46,6 @@ class ListEvaluationTest extends AbstractExportTest
parent::setUp();
self::bootKernel();
$this->listEvaluation = self::$container->get(ListEvaluation::class);
$this->centerRepository = self::$container->get(CenterRepositoryInterface::class);
}
@@ -63,6 +62,7 @@ class ListEvaluationTest extends AbstractExportTest
$rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
$aggregateStringHelper = self::$container->get(AggregateStringHelper::class);
$socialActionRepository = self::$container->get(SocialActionRepository::class);
$filterListHelper = $this->prophesize(FilterListAccompanyingPeriodHelperInterface::class);
yield new ListEvaluation(
$entityManager,
@@ -76,22 +76,7 @@ class ListEvaluationTest extends AbstractExportTest
$translatableStringExportLabelHelper,
$aggregateStringHelper,
$rollingDateConverter,
$this->getParameters(true),
);
yield new ListEvaluation(
$entityManager,
$socialIssueRender,
$socialIssueRepository,
$socialActionRender,
$socialActionRepository,
$userHelper,
$personHelper,
$dateTimeHelper,
$translatableStringExportLabelHelper,
$aggregateStringHelper,
$rollingDateConverter,
$this->getParameters(false),
$filterListHelper->reveal(),
);
}
@@ -104,14 +89,4 @@ class ListEvaluationTest extends AbstractExportTest
{
return [[Declarations::ACP_TYPE]];
}
public function testQuery(): void
{
$centers = $this->centerRepository->findAll();
$query = $this->listEvaluation->initiateQuery([], array_map(fn (Center $c) => ['center' => $c], $centers), ['calc_date' => new RollingDate(RollingDate::T_TODAY)]);
$query->setMaxResults(1);
self::assertIsArray($query->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY));
}
}

View File

@@ -16,9 +16,11 @@ use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Test\Export\AbstractExportTest;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriodDetails;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelperInterface;
use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper;
use Chill\PersonBundle\Export\Helper\ListPersonHelper;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
@@ -27,6 +29,8 @@ use Doctrine\ORM\EntityManagerInterface;
*/
class ListPersonWithAccompanyingPeriodDetailsTest extends AbstractExportTest
{
use ProphecyTrait;
protected function setUp(): void
{
parent::setUp();
@@ -39,21 +43,14 @@ class ListPersonWithAccompanyingPeriodDetailsTest extends AbstractExportTest
$listAccompanyingPeriodHelper = self::$container->get(ListAccompanyingPeriodHelper::class);
$entityManager = self::$container->get(EntityManagerInterface::class);
$rollingDateConverter = self::$container->get(RollingDateConverterInterface::class);
$filterHelper = $this->prophesize(FilterListAccompanyingPeriodHelperInterface::class);
yield new ListPersonWithAccompanyingPeriodDetails(
$listPersonHelper,
$listAccompanyingPeriodHelper,
$entityManager,
$rollingDateConverter,
$this->getParameters(true),
);
yield new ListPersonWithAccompanyingPeriodDetails(
$listPersonHelper,
$listAccompanyingPeriodHelper,
$entityManager,
$rollingDateConverter,
$this->getParameters(false),
$filterHelper->reveal()
);
}

View File

@@ -0,0 +1,137 @@
<?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\PersonBundle\Tests\Export\Helper;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Helper\FilterListAccompanyingPeriodHelper;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Security;
/**
* @internal
*
* @coversNothing
*/
final class FilterListAccompanyingPeriodHelperTest extends KernelTestCase
{
use ProphecyTrait;
private CenterRepositoryInterface $centerRepository;
private ScopeRepositoryInterface $scopeRepository;
private EntityManagerInterface $entityManager;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->centerRepository = self::$container->get(CenterRepositoryInterface::class);
$this->scopeRepository = self::$container->get(ScopeRepositoryInterface::class);
$this->entityManager = self::$container->get(EntityManagerInterface::class);
}
/**
* @dataProvider dataProviderTestAddFilterAccompanyingPeriod
*/
public function testAddFilterAccompanyingPeriod(QueryBuilder $qb, ParameterBagInterface $parameterBag): void
{
// mock security
$user = $this->entityManager->createQuery('SELECT u FROM '.User::class.' u')
->setMaxResults(1)->getSingleResult();
if (null === $user) {
throw new \RuntimeException('no user found');
}
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user);
// mock authorization helper
$scopes = $this->scopeRepository->findAll();
$scopesConfidentials = [] !== $scopes ? [$scopes[0]] : [];
$authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
$authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_DETAILS, Argument::type(Center::class))
->willReturn($scopes);
$authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, Argument::type(Center::class))
->willReturn($scopesConfidentials);
$filter = new FilterListAccompanyingPeriodHelper(
$security->reveal(),
$this->centerRepository,
$authorizationHelper->reveal(),
$parameterBag
);
$filter->addFilterAccompanyingPeriods($qb, [], $this->getACL(), []);
$qb->setMaxResults(1);
$result = $qb->getQuery()->getResult();
self::assertIsArray($result);
}
public function dataProviderTestAddFilterAccompanyingPeriod(): iterable
{
self::setUp();
$qb = $this->entityManager->createQueryBuilder();
$qb
->select('acp.id')
->from(AccompanyingPeriod::class, 'acp');
yield [
$qb,
new ParameterBag(['chill_main' => ['acl' => ['filter_stats_by_center' => true]]]),
];
yield [
$qb,
new ParameterBag(['chill_main' => ['acl' => ['filter_stats_by_center' => false]]]),
];
}
/**
* @return list<array{center: Center, circles: list<Scope>}> the ACL, structured as an array
*
* @throws \RuntimeException when no center or circle is found
*/
private function getACL(): array
{
$centers = $this->centerRepository->findAll();
$circles = $this->scopeRepository->findAll();
if (0 === \count($centers)) {
throw new \RuntimeException('No center found. Did you forget to run `doctrine:fixtures:load` command before ?');
}
if (0 === \count($circles)) {
throw new \RuntimeException('No circle found. Did you forget to run `doctrine:fixtures:load` command before ?');
}
return [[
'center' => $centers[0],
'circles' => [
$circles,
], ]];
}
}

View File

@@ -0,0 +1,58 @@
<?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\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Fix lines in chill_person_person_center_history for people created before the introduction of the createdAt column.
*
* This class represents a migration for fixing lines in the 'chill_person_person_center_history' table.
*
* It updates the 'startdate' column for people created before the introduction
* of the 'createdAt' column, and set it at the first activity date. This migration is irreversible.
*/
final class Version20231207221700 extends AbstractMigration
{
public function getDescription(): string
{
return 'Fix lines in chill_person_person_center_history for people created before the introduction of the createdAt column';
}
public function up(Schema $schema): void
{
$this->addSql('WITH first_history_line AS (SELECT *
FROM (SELECT id,
person_id,
startdate,
rank() OVER (PARTITION BY person_id ORDER BY startdate ASC, id ASC) AS r
FROM chill_person_person_center_history) AS sk
WHERE sk.r = 1),
first_activity AS (SELECT *
FROM (SELECT id, date, person_id, rank() OVER (PARTITION BY person_id ORDER BY date ASC, id ASC) AS r
FROM activity
WHERE person_id IS NOT NULL) sq
WHERE sq.r = 1)
UPDATE chill_person_person_center_history cppch SET startdate=first_activity.date
FROM first_history_line, first_activity
WHERE
first_history_line.id = cppch.id
AND first_activity.person_id = cppch.person_id
AND first_activity.date < first_history_line.startDate');
}
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException();
}
}

View File

@@ -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');
};

View File

@@ -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],

View File

@@ -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

View 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