Merge branch 'signature-app/object-version' into 'signature-app-master'

Add versioning to stored objects

See merge request Chill-Projet/chill-bundles!710
This commit is contained in:
Julien Fastré 2024-09-04 12:46:43 +00:00
commit b2042bd1e4
369 changed files with 3687 additions and 1252 deletions

View File

@ -122,7 +122,7 @@ unit_tests:
- php tests/console chill:db:sync-views --env=test - php tests/console chill:db:sync-views --env=test
- php -d memory_limit=2G tests/console cache:clear --env=test - php -d memory_limit=2G tests/console cache:clear --env=test
- php -d memory_limit=3G tests/console doctrine:fixtures:load -n --env=test - php -d memory_limit=3G tests/console doctrine:fixtures:load -n --env=test
- php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive - php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive --exclude-group openstack-integration
artifacts: artifacts:
expire_in: 1 day expire_in: 1 day
paths: paths:

View File

@ -0,0 +1,125 @@
Enable CODE for development
===========================
For editing a document, there must be a way to communicate between the collabora server and the symfony server, in
both direction. The domain name should also be the same for collabora server and for the browser which access to the
online editor.
Using ngrok (or other http tunnel)
----------------------------------
One can configure a tunnel server to expose your local install to the web, and access to your local server using the
tunnel url.
Start ngrok
^^^^^^^^^^^
This can be achieve using `ngrok <https://ngrok.com/>`_.
.. note::
The configuration of ngrok is outside of the scope of this document. Refers to the ngrok's documentation.
.. code-block:: bash
# ensuring that your server is running through http and port 8000
ngrok http 8000
# then open the link given by the ngrok utility and you should reach your app
At this step, ensure that you can reach your local app using the ngrok url.
Configure Collabora
^^^^^^^^^^^^^^^^^^^
The collabora server must be executed online and configure to access to your ngrok installation. Ensure that the aliasgroup
exists for your ngrok application (`See the CODE documentation: <https://sdk.collaboraonline.com/docs/installation/Configuration.html#multihost-configuration>`_).
Configure your app
^^^^^^^^^^^^^^^^^^
Set the :code:`EDITOR_SERVER` variable to point to your collabora server, this should be done in your :code:`.env.local` file.
At this point, everything must be fine. In case of errors, watch the log from your collabora server, use the `profiler <https://symfony.com/doc/current/profiler.html>`_
to debug the requests.
.. note::
In case of error while validating proof (you'll see those message in the collabora's logs), you can temporarily disable
the proof validation adding this code snippet in `config/services.yaml`:
.. code-block:: yaml
when@dev:
# add only in dev environment, to avoid security problems
services:
ChampsLibres\WopiLib\Contract\Service\ProofValidatorInterface:
# this class will always validate proof
alias: Chill\WopiBundle\Service\Wopi\NullProofValidator
With a local CODE image
-----------------------
.. warning::
This configuration is not sure, and must be refined. The documentation does not seems to be entirely valid.
Use a local domain name and https for your app
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Use the proxy feature from embedded symfony server to run your app. `See the dedicated doc <https://symfony.com/doc/current/setup/symfony_server.html#local-domain-names>`
Configure also the `https certificate <https://symfony.com/doc/current/setup/symfony_server.html#enabling-tls>`_
In this example, your local domain name will be :code:`my-domain` and the url will be :code:`https://my-domain.wip`.
Ensure that the proxy is running.
Create a certificate database for collabora
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Collabora must validate your certificate generated by symfony console. For that, you need `to create a NSS database <https://sdk.collaboraonline.com/docs/installation/Configuration.html#validating-digital-signatures>`
and configure collabora to use it.
At first, export the certificate for symfony development. Use the graphical interface from your browser to get the
certificate as a PEM file.
.. code-block:: bash
# create your database in a custom directory
mkdir /path/to/your/directory
certutil -N -d /path/to/your/directory
cat /path/to/your/ca.crt | certutil -d . -A symfony -t -t C,P,C,u,w -a
Launch CODE properly configured
.. code-block:: yaml
collabora:
image: collabora/code:latest
environment:
- SLEEPFORDEBUGGER=0
- DONT_GEN_SSL_CERT="True"
# add path to the database
- extra_params=--o:ssl.enable=false --o:ssl.termination=false --o:logging.level=7 -o:certificates.database_path=/etc/custom-certificates/nss-database
- username=admin
- password=admin
- dictionaries=en_US
- aliasgroup1=https://my-domain.wip
ports:
- "127.0.0.1:9980:9980"
volumes:
- "/path/to/your/directory/nss-database:/etc/custom-certificates/nss-database"
extra_hosts:
- "my-domain.wip:host-gateway"
Configure your app
^^^^^^^^^^^^^^^^^^
Into your :code:`.env.local` file:
.. code-block:: env
EDITOR_SERVER=http://${COLLABORA_HOST}:${COLLABORA_PORT}
At this step, you should be able to edit a document through collabora.

View File

@ -54,7 +54,6 @@
"masonry-layout": "^4.2.2", "masonry-layout": "^4.2.2",
"mime": "^4.0.0", "mime": "^4.0.0",
"pdfjs-dist": "^4.3.136", "pdfjs-dist": "^4.3.136",
"swagger-ui": "^4.15.5",
"vis-network": "^9.1.0", "vis-network": "^9.1.0",
"vue": "^3.2.37", "vue": "^3.2.37",
"vue-i18n": "^9.1.6", "vue-i18n": "^9.1.6",

View File

@ -68,7 +68,7 @@ final class ActivityController extends AbstractController
private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory, private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly PaginatorFactory $paginatorFactory, private readonly PaginatorFactory $paginatorFactory,
private readonly ChillSecurity $security private readonly ChillSecurity $security,
) {} ) {}
/** /**

View File

@ -28,7 +28,7 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali
public function __construct( public function __construct(
protected ActivityReasonCategoryRepository $activityReasonCategoryRepository, protected ActivityReasonCategoryRepository $activityReasonCategoryRepository,
protected ActivityReasonRepository $activityReasonRepository, protected ActivityReasonRepository $activityReasonRepository,
protected TranslatableStringHelper $translatableStringHelper protected TranslatableStringHelper $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -26,7 +26,7 @@ class ActivityUsersJobAggregator implements AggregatorInterface
public function __construct( public function __construct(
private readonly UserJobRepositoryInterface $userJobRepository, private readonly UserJobRepositoryInterface $userJobRepository,
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -26,7 +26,7 @@ class ActivityUsersScopeAggregator implements AggregatorInterface
public function __construct( public function __construct(
private readonly ScopeRepositoryInterface $scopeRepository, private readonly ScopeRepositoryInterface $scopeRepository,
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -26,7 +26,7 @@ class CreatorJobAggregator implements AggregatorInterface
public function __construct( public function __construct(
private readonly UserJobRepositoryInterface $userJobRepository, private readonly UserJobRepositoryInterface $userJobRepository,
private readonly TranslatableStringHelper $translatableStringHelper private readonly TranslatableStringHelper $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -26,7 +26,7 @@ class CreatorScopeAggregator implements AggregatorInterface
public function __construct( public function __construct(
private readonly ScopeRepository $scopeRepository, private readonly ScopeRepository $scopeRepository,
private readonly TranslatableStringHelper $translatableStringHelper private readonly TranslatableStringHelper $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -42,7 +42,7 @@ class StatActivityDuration implements ExportInterface, GroupedExportInterface
/** /**
* The action for this report. * The action for this report.
*/ */
protected string $action = 'sum' protected string $action = 'sum',
) { ) {
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center']; $this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
} }

View File

@ -39,7 +39,7 @@ class ListActivityHelper
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,
private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly TranslatableStringExportLabelHelper $translatableStringLabelHelper, private readonly TranslatableStringExportLabelHelper $translatableStringLabelHelper,
private readonly UserHelper $userHelper private readonly UserHelper $userHelper,
) {} ) {}
public function addSelect(QueryBuilder $qb): void public function addSelect(QueryBuilder $qb): void

View File

@ -25,7 +25,7 @@ final readonly class ActivityPresenceFilter implements FilterInterface
{ {
public function __construct( public function __construct(
private TranslatableStringHelperInterface $translatableStringHelper, private TranslatableStringHelperInterface $translatableStringHelper,
private TranslatorInterface $translator private TranslatorInterface $translator,
) {} ) {}
public function getTitle() public function getTitle()

View File

@ -26,7 +26,7 @@ class ActivityTypeFilter implements ExportElementValidatedInterface, FilterInter
{ {
public function __construct( public function __construct(
protected TranslatableStringHelperInterface $translatableStringHelper, protected TranslatableStringHelperInterface $translatableStringHelper,
protected ActivityTypeRepositoryInterface $activityTypeRepository protected ActivityTypeRepositoryInterface $activityTypeRepository,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -29,7 +29,7 @@ class UsersJobFilter implements FilterInterface
public function __construct( public function __construct(
private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly UserJobRepositoryInterface $userJobRepository private readonly UserJobRepositoryInterface $userJobRepository,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -29,7 +29,7 @@ class UsersScopeFilter implements FilterInterface
public function __construct( public function __construct(
private readonly ScopeRepositoryInterface $scopeRepository, private readonly ScopeRepositoryInterface $scopeRepository,
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -59,7 +59,7 @@ class ActivityType extends AbstractType
protected TranslatableStringHelper $translatableStringHelper, protected TranslatableStringHelper $translatableStringHelper,
protected array $timeChoices, protected array $timeChoices,
protected SocialIssueRender $socialIssueRender, protected SocialIssueRender $socialIssueRender,
protected SocialActionRender $socialActionRender protected SocialActionRender $socialActionRender,
) { ) {
if (!$tokenStorage->getToken()->getUser() instanceof User) { if (!$tokenStorage->getToken()->getUser() instanceof User) {
throw new \RuntimeException('you should have a valid user'); throw new \RuntimeException('you should have a valid user');

View File

@ -27,7 +27,7 @@ class PickActivityReasonType extends AbstractType
public function __construct( public function __construct(
private readonly ActivityReasonRepository $activityReasonRepository, private readonly ActivityReasonRepository $activityReasonRepository,
private readonly ActivityReasonRender $reasonRender, private readonly ActivityReasonRender $reasonRender,
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
) {} ) {}
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver)

View File

@ -32,7 +32,7 @@ final readonly class ActivityDocumentACLAwareRepository implements ActivityDocum
private EntityManagerInterface $em, private EntityManagerInterface $em,
private CenterResolverManagerInterface $centerResolverManager, private CenterResolverManagerInterface $centerResolverManager,
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser, private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
private Security $security private Security $security,
) {} ) {}
public function buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface public function buildFetchQueryActivityDocumentLinkedToPersonFromPersonContext(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQueryInterface

View File

@ -25,7 +25,7 @@ class ActivityReasonRepository extends ServiceEntityRepository
{ {
public function __construct( public function __construct(
ManagerRegistry $registry, ManagerRegistry $registry,
private readonly RequestStack $requestStack private readonly RequestStack $requestStack,
) { ) {
parent::__construct($registry, ActivityReason::class); parent::__construct($registry, ActivityReason::class);
} }

View File

@ -24,7 +24,7 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
public function __construct( public function __construct(
private readonly ActivityRepository $repository, private readonly ActivityRepository $repository,
Security $security, Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService WorkflowStoredObjectPermissionHelper $workflowDocumentService,
) { ) {
parent::__construct($security, $workflowDocumentService); parent::__construct($security, $workflowDocumentService);
} }

View File

@ -75,7 +75,7 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
public function __construct( public function __construct(
protected Security $security, protected Security $security,
VoterHelperFactoryInterface $voterHelperFactory VoterHelperFactoryInterface $voterHelperFactory,
) { ) {
$this->voterHelper = $voterHelperFactory->generate(self::class) $this->voterHelper = $voterHelperFactory->generate(self::class)
->addCheckFor(Person::class, [self::SEE, self::CREATE]) ->addCheckFor(Person::class, [self::SEE, self::CREATE])

View File

@ -50,7 +50,7 @@ class ActivityContext implements
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,
private readonly BaseContextData $baseContextData, private readonly BaseContextData $baseContextData,
private readonly ThirdPartyRender $thirdPartyRender, private readonly ThirdPartyRender $thirdPartyRender,
private readonly ThirdPartyRepository $thirdPartyRepository private readonly ThirdPartyRepository $thirdPartyRepository,
) {} ) {}
public function adminFormReverseTransform(array $data): array public function adminFormReverseTransform(array $data): array

View File

@ -56,7 +56,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
private readonly SocialIssueRepository $socialIssueRepository, private readonly SocialIssueRepository $socialIssueRepository,
private readonly ThirdPartyRepository $thirdPartyRepository, private readonly ThirdPartyRepository $thirdPartyRepository,
private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly UserRepository $userRepository private readonly UserRepository $userRepository,
) {} ) {}
public function adminFormReverseTransform(array $data): array public function adminFormReverseTransform(array $data): array

View File

@ -76,7 +76,7 @@ final class TranslatableActivityReasonTest extends TypeTestCase
*/ */
protected function getTranslatableStringHelper( protected function getTranslatableStringHelper(
$locale = 'en', $locale = 'en',
$fallbackLocale = 'en' $fallbackLocale = 'en',
) { ) {
$prophet = new \Prophecy\Prophet(); $prophet = new \Prophecy\Prophet();
$requestStack = $prophet->prophesize(); $requestStack = $prophet->prophesize();

View File

@ -138,7 +138,7 @@ final class ActivityVoterTest extends KernelTestCase
Scope $scope, Scope $scope,
Center $center, Center $center,
$attribute, $attribute,
$message $message,
) { ) {
$token = $this->prepareToken($user); $token = $this->prepareToken($user);
$activity = $this->prepareActivity($scope, $this->preparePerson($center)); $activity = $this->prepareActivity($scope, $this->preparePerson($center));

View File

@ -32,7 +32,7 @@ class TimelineActivityProvider implements TimelineProviderInterface
protected EntityManagerInterface $em, protected EntityManagerInterface $em,
protected AuthorizationHelperInterface $helper, protected AuthorizationHelperInterface $helper,
TokenStorageInterface $storage, TokenStorageInterface $storage,
protected ActivityACLAwareRepository $aclAwareRepository protected ActivityACLAwareRepository $aclAwareRepository,
) { ) {
if (!$storage->getToken()->getUser() instanceof User) { if (!$storage->getToken()->getUser() instanceof User) {
throw new \RuntimeException('A user should be authenticated !'); throw new \RuntimeException('A user should be authenticated !');

View File

@ -25,7 +25,7 @@ final class AsideActivityController extends CRUDController
{ {
public function __construct( public function __construct(
private readonly AsideActivityCategoryRepository $categoryRepository, private readonly AsideActivityCategoryRepository $categoryRepository,
private readonly Security $security private readonly Security $security,
) {} ) {}
public function createEntity(string $action, Request $request): object public function createEntity(string $action, Request $request): object
@ -76,7 +76,7 @@ final class AsideActivityController extends CRUDController
string $action, string $action,
$query, $query,
Request $request, Request $request,
PaginatorInterface $paginator PaginatorInterface $paginator,
) { ) {
if ('index' === $action) { if ('index' === $action) {
return $query->orderBy('e.date', 'DESC'); return $query->orderBy('e.date', 'DESC');

View File

@ -22,7 +22,7 @@ class ByActivityTypeAggregator implements AggregatorInterface
{ {
public function __construct( public function __construct(
private readonly AsideActivityCategoryRepository $asideActivityCategoryRepository, private readonly AsideActivityCategoryRepository $asideActivityCategoryRepository,
private readonly TranslatableStringHelper $translatableStringHelper private readonly TranslatableStringHelper $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -26,7 +26,7 @@ class ByUserJobAggregator implements AggregatorInterface
public function __construct( public function __construct(
private readonly UserJobRepositoryInterface $userJobRepository, private readonly UserJobRepositoryInterface $userJobRepository,
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -26,7 +26,7 @@ class ByUserScopeAggregator implements AggregatorInterface
public function __construct( public function __construct(
private readonly ScopeRepositoryInterface $scopeRepository, private readonly ScopeRepositoryInterface $scopeRepository,
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -41,7 +41,7 @@ final readonly class ListAsideActivity implements ListInterface, GroupedExportIn
private AsideActivityCategoryRepository $asideActivityCategoryRepository, private AsideActivityCategoryRepository $asideActivityCategoryRepository,
private CategoryRender $categoryRender, private CategoryRender $categoryRender,
private LocationRepository $locationRepository, private LocationRepository $locationRepository,
private TranslatableStringHelperInterface $translatableStringHelper private TranslatableStringHelperInterface $translatableStringHelper,
) {} ) {}
public function buildForm(FormBuilderInterface $builder) {} public function buildForm(FormBuilderInterface $builder) {}

View File

@ -27,7 +27,7 @@ class ByActivityTypeFilter implements FilterInterface
public function __construct( public function __construct(
private readonly CategoryRender $categoryRender, private readonly CategoryRender $categoryRender,
private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly AsideActivityCategoryRepository $asideActivityTypeRepository private readonly AsideActivityCategoryRepository $asideActivityTypeRepository,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -24,7 +24,7 @@ use Symfony\Component\Security\Core\Security;
final readonly class ByLocationFilter implements FilterInterface final readonly class ByLocationFilter implements FilterInterface
{ {
public function __construct( public function __construct(
private Security $security private Security $security,
) {} ) {}
public function getTitle(): string public function getTitle(): string

View File

@ -29,7 +29,7 @@ class ByUserJobFilter implements FilterInterface
public function __construct( public function __construct(
private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatableStringHelperInterface $translatableStringHelper,
private readonly UserJobRepositoryInterface $userJobRepository private readonly UserJobRepositoryInterface $userJobRepository,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -29,7 +29,7 @@ class ByUserScopeFilter implements FilterInterface
public function __construct( public function __construct(
private readonly ScopeRepositoryInterface $scopeRepository, private readonly ScopeRepositoryInterface $scopeRepository,
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -44,7 +44,7 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
CountNotificationTask $counter, CountNotificationTask $counter,
TokenStorageInterface $tokenStorage, TokenStorageInterface $tokenStorage,
TranslatorInterface $translator, TranslatorInterface $translator,
AuthorizationCheckerInterface $authorizationChecker AuthorizationCheckerInterface $authorizationChecker,
) { ) {
$this->counter = $counter; $this->counter = $counter;
$this->tokenStorage = $tokenStorage; $this->tokenStorage = $tokenStorage;

View File

@ -26,7 +26,7 @@ class AsideActivityVoter extends AbstractChillVoter implements ProvideRoleHierar
private readonly VoterHelperInterface $voterHelper; private readonly VoterHelperInterface $voterHelper;
public function __construct( public function __construct(
VoterHelperFactoryInterface $voterHelperFactory VoterHelperFactoryInterface $voterHelperFactory,
) { ) {
$this->voterHelper = $voterHelperFactory $this->voterHelper = $voterHelperFactory
->generate(self::class) ->generate(self::class)

View File

@ -47,7 +47,7 @@ class SendTestShortMessageOnCalendarCommand extends Command
private readonly PhoneNumberHelperInterface $phoneNumberHelper, private readonly PhoneNumberHelperInterface $phoneNumberHelper,
private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder, private readonly ShortMessageForCalendarBuilderInterface $messageForCalendarBuilder,
private readonly ShortMessageTransporterInterface $transporter, private readonly ShortMessageTransporterInterface $transporter,
private readonly UserRepositoryInterface $userRepository private readonly UserRepositoryInterface $userRepository,
) { ) {
parent::__construct('chill:calendar:test-send-short-message'); parent::__construct('chill:calendar:test-send-short-message');
} }

View File

@ -59,7 +59,7 @@ class CalendarController extends AbstractController
private readonly AccompanyingPeriodRepository $accompanyingPeriodRepository, private readonly AccompanyingPeriodRepository $accompanyingPeriodRepository,
private readonly UserRepositoryInterface $userRepository, private readonly UserRepositoryInterface $userRepository,
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
) {} ) {}
/** /**

View File

@ -47,7 +47,7 @@ class CalendarDoc implements TrackCreationInterface, TrackUpdateInterface
Calendar $calendar, Calendar $calendar,
#[ORM\ManyToOne(targetEntity: StoredObject::class, cascade: ['persist'])] #[ORM\ManyToOne(targetEntity: StoredObject::class, cascade: ['persist'])]
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false)]
private ?StoredObject $storedObject private ?StoredObject $storedObject,
) { ) {
$this->setCalendar($calendar); $this->setCalendar($calendar);
$this->datetimeVersion = $calendar->getDateTimeVersion(); $this->datetimeVersion = $calendar->getDateTimeVersion();

View File

@ -26,7 +26,7 @@ final readonly class JobAggregator implements AggregatorInterface
public function __construct( public function __construct(
private UserJobRepository $jobRepository, private UserJobRepository $jobRepository,
private TranslatableStringHelper $translatableStringHelper private TranslatableStringHelper $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -26,7 +26,7 @@ final readonly class ScopeAggregator implements AggregatorInterface
public function __construct( public function __construct(
private ScopeRepository $scopeRepository, private ScopeRepository $scopeRepository,
private TranslatableStringHelper $translatableStringHelper private TranslatableStringHelper $translatableStringHelper,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -28,7 +28,7 @@ final readonly class JobFilter implements FilterInterface
public function __construct( public function __construct(
private TranslatableStringHelper $translatableStringHelper, private TranslatableStringHelper $translatableStringHelper,
private UserJobRepositoryInterface $userJobRepository private UserJobRepositoryInterface $userJobRepository,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -30,7 +30,7 @@ class ScopeFilter implements FilterInterface
public function __construct( public function __construct(
protected TranslatorInterface $translator, protected TranslatorInterface $translator,
private readonly TranslatableStringHelper $translatableStringHelper, private readonly TranslatableStringHelper $translatableStringHelper,
private readonly ScopeRepositoryInterface $scopeRepository private readonly ScopeRepositoryInterface $scopeRepository,
) {} ) {}
public function addRole(): ?string public function addRole(): ?string

View File

@ -37,7 +37,7 @@ class CalendarType extends AbstractType
private readonly IdToUsersDataTransformer $idToUsersDataTransformer, private readonly IdToUsersDataTransformer $idToUsersDataTransformer,
private readonly IdToLocationDataTransformer $idToLocationDataTransformer, private readonly IdToLocationDataTransformer $idToLocationDataTransformer,
private readonly ThirdPartiesToIdDataTransformer $partiesToIdDataTransformer, private readonly ThirdPartiesToIdDataTransformer $partiesToIdDataTransformer,
private readonly IdToCalendarRangeDataTransformer $calendarRangeDataTransformer private readonly IdToCalendarRangeDataTransformer $calendarRangeDataTransformer,
) {} ) {}
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)

View File

@ -46,7 +46,7 @@ class CalendarMessage
public function __construct( public function __construct(
Calendar $calendar, Calendar $calendar,
private readonly string $action, private readonly string $action,
User $byUser User $byUser,
) { ) {
$this->calendarId = $calendar->getId(); $this->calendarId = $calendar->getId();
$this->byUserId = $byUser->getId(); $this->byUserId = $byUser->getId();

View File

@ -59,7 +59,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface
'alwaysEnabled' => true, 'alwaysEnabled' => true,
'scheduled' => RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledStartDateTime']['dateTime']) < $this->clock->now() 'scheduled' => RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledStartDateTime']['dateTime']) < $this->clock->now()
&& RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledEndDateTime']['dateTime']) > $this->clock->now(), && RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledEndDateTime']['dateTime']) > $this->clock->now(),
default => throw new UserAbsenceSyncException('this status is not documented by Microsoft') default => throw new UserAbsenceSyncException('this status is not documented by Microsoft'),
}; };
} }
} }

View File

@ -177,7 +177,7 @@ class MapCalendarToUser
User $user, User $user,
int $expiration, int $expiration,
?string $id = null, ?string $id = null,
?string $secret = null ?string $secret = null,
): void { ): void {
$user->setAttributeByDomain(self::METADATA_KEY, self::EXPIRATION_SUBSCRIPTION_EVENT, $expiration); $user->setAttributeByDomain(self::METADATA_KEY, self::EXPIRATION_SUBSCRIPTION_EVENT, $expiration);

View File

@ -57,7 +57,7 @@ class RemoteEventConverter
private readonly LocationConverter $locationConverter, private readonly LocationConverter $locationConverter,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly PersonRenderInterface $personRender, private readonly PersonRenderInterface $personRender,
private readonly TranslatorInterface $translator private readonly TranslatorInterface $translator,
) { ) {
$this->defaultDateTimeZone = (new \DateTimeImmutable())->getTimezone(); $this->defaultDateTimeZone = (new \DateTimeImmutable())->getTimezone();
$this->remoteDateTimeZone = self::getRemoteTimeZone(); $this->remoteDateTimeZone = self::getRemoteTimeZone();

View File

@ -351,7 +351,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
[ [
'id' => $id, 'id' => $id,
'lastModifiedDateTime' => $lastModified, 'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey 'changeKey' => $changeKey,
] = $this->createOnRemote($eventData, $calendar->getMainUser(), 'calendar_'.$calendar->getId()); ] = $this->createOnRemote($eventData, $calendar->getMainUser(), 'calendar_'.$calendar->getId());
if (null === $id) { if (null === $id) {
@ -427,7 +427,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
[ [
'id' => $id, 'id' => $id,
'lastModifiedDateTime' => $lastModified, 'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey 'changeKey' => $changeKey,
] = $this->createOnRemote( ] = $this->createOnRemote(
$eventData, $eventData,
$calendarRange->getUser(), $calendarRange->getUser(),
@ -564,7 +564,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
[ [
'id' => $id, 'id' => $id,
'lastModifiedDateTime' => $lastModified, 'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey 'changeKey' => $changeKey,
] = $this->patchOnRemote( ] = $this->patchOnRemote(
$calendar->getRemoteId(), $calendar->getRemoteId(),
$eventData, $eventData,

View File

@ -33,6 +33,6 @@ class RemoteEvent
#[Serializer\Groups(['read'])] #[Serializer\Groups(['read'])]
public \DateTimeImmutable $endDate, public \DateTimeImmutable $endDate,
#[Serializer\Groups(['read'])] #[Serializer\Groups(['read'])]
public bool $isAllDay = false public bool $isAllDay = false,
) {} ) {}
} }

View File

@ -65,7 +65,7 @@ class CalendarRangeRepository implements ObjectRepository
\DateTimeImmutable $from, \DateTimeImmutable $from,
\DateTimeImmutable $to, \DateTimeImmutable $to,
?int $limit = null, ?int $limit = null,
?int $offset = null ?int $offset = null,
): array { ): array {
$qb = $this->buildQueryAvailableRangesForUser($user, $from, $to); $qb = $this->buildQueryAvailableRangesForUser($user, $from, $to);

View File

@ -40,7 +40,7 @@ final readonly class CalendarContext implements CalendarContextInterface
private PersonRepository $personRepository, private PersonRepository $personRepository,
private ThirdPartyRender $thirdPartyRender, private ThirdPartyRender $thirdPartyRender,
private ThirdPartyRepository $thirdPartyRepository, private ThirdPartyRepository $thirdPartyRepository,
private TranslatableStringHelperInterface $translatableStringHelper private TranslatableStringHelperInterface $translatableStringHelper,
) {} ) {}
public function adminFormReverseTransform(array $data): array public function adminFormReverseTransform(array $data): array

View File

@ -37,7 +37,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen
public function __construct( public function __construct(
private Security $security, private Security $security,
private EntityManagerInterface $em private EntityManagerInterface $em,
) {} ) {}
/** /**

View File

@ -36,7 +36,7 @@ final readonly class PersonCalendarGenericDocProvider implements GenericDocForPe
public function __construct( public function __construct(
private Security $security, private Security $security,
private EntityManagerInterface $em private EntityManagerInterface $em,
) {} ) {}
private function addWhereClausesToQuery(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery private function addWhereClausesToQuery(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery

View File

@ -156,7 +156,7 @@ final class CalendarTypeTest extends TypeTestCase
private function buildMultiToIdDataTransformer( private function buildMultiToIdDataTransformer(
string $classTransformer, string $classTransformer,
string $objClass string $objClass,
) { ) {
$transformer = $this->prophesize($classTransformer); $transformer = $this->prophesize($classTransformer);
$transformer->transform(Argument::type('array')) $transformer->transform(Argument::type('array'))
@ -195,7 +195,7 @@ final class CalendarTypeTest extends TypeTestCase
private function buildSingleToIdDataTransformer( private function buildSingleToIdDataTransformer(
string $classTransformer, string $classTransformer,
string $class string $class,
) { ) {
$transformer = $this->prophesize($classTransformer); $transformer = $this->prophesize($classTransformer);
$transformer->transform(Argument::type('object')) $transformer->transform(Argument::type('object'))

View File

@ -203,7 +203,7 @@ final class CalendarContextTest extends TestCase
private function buildCalendarContext( private function buildCalendarContext(
?EntityManagerInterface $entityManager = null, ?EntityManagerInterface $entityManager = null,
?NormalizerInterface $normalizer = null ?NormalizerInterface $normalizer = null,
): CalendarContext { ): CalendarContext {
$baseContext = $this->prophesize(BaseContextData::class); $baseContext = $this->prophesize(BaseContextData::class);
$baseContext->getData(null)->willReturn(['base_context' => 'data']); $baseContext->getData(null)->willReturn(['base_context' => 'data']);

View File

@ -44,7 +44,7 @@ class CreateFieldsOnGroupCommand extends Command
private readonly EntityManager $entityManager, private readonly EntityManager $entityManager,
private readonly ValidatorInterface $validator, private readonly ValidatorInterface $validator,
private $availableLanguages, private $availableLanguages,
private $customizablesEntities private $customizablesEntities,
) { ) {
parent::__construct(); parent::__construct();
} }

View File

@ -39,7 +39,7 @@ class CustomFieldsGroupController extends AbstractController
public function __construct( public function __construct(
private readonly CustomFieldProvider $customFieldProvider, private readonly CustomFieldProvider $customFieldProvider,
private readonly TranslatorInterface $translator, private readonly TranslatorInterface $translator,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
) {} ) {}
/** /**

View File

@ -42,7 +42,7 @@ class CustomFieldChoice extends AbstractCustomField
/** /**
* @var TranslatableStringHelper Helper that find the string in current locale from an array of translation * @var TranslatableStringHelper Helper that find the string in current locale from an array of translation
*/ */
private readonly TranslatableStringHelper $translatableStringHelper private readonly TranslatableStringHelper $translatableStringHelper,
) {} ) {}
public function allowOtherChoice(CustomField $cf) public function allowOtherChoice(CustomField $cf)

View File

@ -44,7 +44,7 @@ class CustomFieldDate extends AbstractCustomField
public function __construct( public function __construct(
private readonly Environment $templating, private readonly Environment $templating,
private readonly TranslatableStringHelper $translatableStringHelper private readonly TranslatableStringHelper $translatableStringHelper,
) {} ) {}
public function buildForm(FormBuilderInterface $builder, CustomField $customField) public function buildForm(FormBuilderInterface $builder, CustomField $customField)

View File

@ -41,7 +41,7 @@ class CustomFieldNumber extends AbstractCustomField
public function __construct( public function __construct(
private readonly Environment $templating, private readonly Environment $templating,
private readonly TranslatableStringHelper $translatableStringHelper private readonly TranslatableStringHelper $translatableStringHelper,
) {} ) {}
public function buildForm(FormBuilderInterface $builder, CustomField $customField) public function buildForm(FormBuilderInterface $builder, CustomField $customField)

View File

@ -28,7 +28,7 @@ class CustomFieldText extends AbstractCustomField
public function __construct( public function __construct(
private readonly Environment $templating, private readonly Environment $templating,
private readonly TranslatableStringHelper $translatableStringHelper private readonly TranslatableStringHelper $translatableStringHelper,
) {} ) {}
/** /**

View File

@ -31,7 +31,7 @@ class CustomFieldTitle extends AbstractCustomField
/** /**
* @var TranslatableStringHelper Helper that find the string in current locale from an array of translation * @var TranslatableStringHelper Helper that find the string in current locale from an array of translation
*/ */
private readonly TranslatableStringHelper $translatableStringHelper private readonly TranslatableStringHelper $translatableStringHelper,
) {} ) {}
public function buildForm(FormBuilderInterface $builder, CustomField $customField) public function buildForm(FormBuilderInterface $builder, CustomField $customField)

View File

@ -26,7 +26,7 @@ class CustomFieldsGroupType extends AbstractType
public function __construct( public function __construct(
private readonly array $customizableEntities, private readonly array $customizableEntities,
// TODO : add comment about this variable // TODO : add comment about this variable
private readonly TranslatorInterface $translator private readonly TranslatorInterface $translator,
) {} ) {}
// TODO : details about the function // TODO : details about the function

View File

@ -48,7 +48,7 @@ final class DocGeneratorTemplateController extends AbstractController
private readonly PaginatorFactory $paginatorFactory, private readonly PaginatorFactory $paginatorFactory,
private readonly EntityManagerInterface $entityManager, private readonly EntityManagerInterface $entityManager,
private readonly ClockInterface $clock, private readonly ClockInterface $clock,
private readonly ChillSecurity $security private readonly ChillSecurity $security,
) {} ) {}
#[Route(path: '{_locale}/admin/doc/gen/generate/test/from/{template}/for/{entityClassName}/{entityId}', name: 'chill_docgenerator_test_generate_from_template')] #[Route(path: '{_locale}/admin/doc/gen/generate/test/from/{template}/for/{entityClassName}/{entityId}', name: 'chill_docgenerator_test_generate_from_template')]
@ -56,7 +56,7 @@ final class DocGeneratorTemplateController extends AbstractController
DocGeneratorTemplate $template, DocGeneratorTemplate $template,
string $entityClassName, string $entityClassName,
int $entityId, int $entityId,
Request $request Request $request,
): Response { ): Response {
return $this->generateDocFromTemplate( return $this->generateDocFromTemplate(
$template, $template,
@ -71,7 +71,7 @@ final class DocGeneratorTemplateController extends AbstractController
DocGeneratorTemplate $template, DocGeneratorTemplate $template,
string $entityClassName, string $entityClassName,
int $entityId, int $entityId,
Request $request Request $request,
): Response { ): Response {
return $this->generateDocFromTemplate( return $this->generateDocFromTemplate(
$template, $template,
@ -137,7 +137,7 @@ final class DocGeneratorTemplateController extends AbstractController
DocGeneratorTemplate $template, DocGeneratorTemplate $template,
int $entityId, int $entityId,
Request $request, Request $request,
bool $isTest bool $isTest,
): Response { ): Response {
try { try {
$context = $this->contextManager->getContextByDocGeneratorTemplate($template); $context = $this->contextManager->getContextByDocGeneratorTemplate($template);

View File

@ -54,12 +54,15 @@ class LoadDocGeneratorTemplate extends AbstractFixture
]; ];
foreach ($templates as $template) { foreach ($templates as $template) {
$newStoredObj = (new StoredObject()) $newStoredObj = (new StoredObject());
->setFilename($template['file']['filename'])
->setKeyInfos(json_decode($template['file']['key'], true)) $newStoredObj
->setIv(json_decode($template['file']['iv'], true))
->setCreatedAt(new \DateTime('today')) ->setCreatedAt(new \DateTime('today'))
->setType($template['file']['type']); ->registerVersion(
json_decode($template['file']['key'], true),
json_decode($template['file']['iv'], true),
$template['file']['type'],
);
$manager->persist($newStoredObj); $manager->persist($newStoredObj);

View File

@ -28,7 +28,7 @@ final readonly class RelatorioDriver implements DriverInterface
public function __construct( public function __construct(
private HttpClientInterface $client, private HttpClientInterface $client,
ParameterBagInterface $parameterBag, ParameterBagInterface $parameterBag,
private LoggerInterface $logger private LoggerInterface $logger,
) { ) {
$this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url']; $this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url'];
} }

View File

@ -35,7 +35,7 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
public function __construct( public function __construct(
private readonly ClassMetadataFactoryInterface $classMetadataFactory, private readonly ClassMetadataFactoryInterface $classMetadataFactory,
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
) { ) {
$this->propertyAccess = PropertyAccess::createPropertyAccessor(); $this->propertyAccess = PropertyAccess::createPropertyAccessor();
} }

View File

@ -33,7 +33,7 @@ class Generator implements GeneratorInterface
private readonly DriverInterface $driver, private readonly DriverInterface $driver,
private readonly ManagerRegistry $objectManagerRegistry, private readonly ManagerRegistry $objectManagerRegistry,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly StoredObjectManagerInterface $storedObjectManager private readonly StoredObjectManagerInterface $storedObjectManager,
) {} ) {}
public function generateDataDump( public function generateDataDump(
@ -134,13 +134,11 @@ class Generator implements GeneratorInterface
$content = Yaml::dump($data, 6); $content = Yaml::dump($data, 6);
/* @var StoredObject $destinationStoredObject */ /* @var StoredObject $destinationStoredObject */
$destinationStoredObject $destinationStoredObject
->setType('application/yaml')
->setFilename(sprintf('%s_yaml', uniqid('doc_', true)))
->setStatus(StoredObject::STATUS_READY) ->setStatus(StoredObject::STATUS_READY)
; ;
try { try {
$this->storedObjectManager->write($destinationStoredObject, $content); $this->storedObjectManager->write($destinationStoredObject, $content, 'application/yaml');
} catch (StoredObjectManagerException $e) { } catch (StoredObjectManagerException $e) {
$destinationStoredObject->addGenerationErrors($e->getMessage()); $destinationStoredObject->addGenerationErrors($e->getMessage());
@ -174,13 +172,11 @@ class Generator implements GeneratorInterface
/* @var StoredObject $destinationStoredObject */ /* @var StoredObject $destinationStoredObject */
$destinationStoredObject $destinationStoredObject
->setType($template->getFile()->getType())
->setFilename(sprintf('%s_odt', uniqid('doc_', true)))
->setStatus(StoredObject::STATUS_READY) ->setStatus(StoredObject::STATUS_READY)
; ;
try { try {
$this->storedObjectManager->write($destinationStoredObject, $generatedResource); $this->storedObjectManager->write($destinationStoredObject, $generatedResource, $template->getFile()->getType());
} catch (StoredObjectManagerException $e) { } catch (StoredObjectManagerException $e) {
$destinationStoredObject->addGenerationErrors($e->getMessage()); $destinationStoredObject->addGenerationErrors($e->getMessage());

View File

@ -39,7 +39,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface
private MailerInterface $mailer, private MailerInterface $mailer,
private StoredObjectRepositoryInterface $storedObjectRepository, private StoredObjectRepositoryInterface $storedObjectRepository,
private TranslatorInterface $translator, private TranslatorInterface $translator,
private UserRepositoryInterface $userRepository private UserRepositoryInterface $userRepository,
) {} ) {}
public static function getSubscribedEvents() public static function getSubscribedEvents()

View File

@ -56,7 +56,7 @@ final class BaseContextDataTest extends KernelTestCase
} }
private function buildBaseContext( private function buildBaseContext(
?NormalizerInterface $normalizer = null ?NormalizerInterface $normalizer = null,
): BaseContextData { ): BaseContextData {
return new BaseContextData( return new BaseContextData(
$normalizer ?? self::getContainer()->get(NormalizerInterface::class) $normalizer ?? self::getContainer()->get(NormalizerInterface::class)

View File

@ -58,6 +58,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
?int $expire_delay = null, ?int $expire_delay = null,
?int $submit_delay = null, ?int $submit_delay = null,
int $max_file_count = 1, int $max_file_count = 1,
?string $object_name = null,
): SignedUrlPost { ): SignedUrlPost {
$delay = $expire_delay ?? $this->max_expire_delay; $delay = $expire_delay ?? $this->max_expire_delay;
$submit_delay ??= $this->max_submit_delay; $submit_delay ??= $this->max_submit_delay;
@ -84,7 +85,9 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
$expires = $this->clock->now()->add(new \DateInterval('PT'.(string) $delay.'S')); $expires = $this->clock->now()->add(new \DateInterval('PT'.(string) $delay.'S'));
$object_name = $this->generateObjectName(); if (null === $object_name) {
$object_name = $this->generateObjectName();
}
$g = new SignedUrlPost( $g = new SignedUrlPost(
$url = $this->generateUrl($object_name), $url = $this->generateUrl($object_name),
@ -141,7 +144,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
{ {
return match (str_ends_with($this->base_url, '/')) { return match (str_ends_with($this->base_url, '/')) {
true => $this->base_url.$relative_path, true => $this->base_url.$relative_path,
false => $this->base_url.'/'.$relative_path false => $this->base_url.'/'.$relative_path,
}; };
} }
@ -179,21 +182,19 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
return \hash_hmac('sha512', $body, $this->key, false); return \hash_hmac('sha512', $body, $this->key, false);
} }
private function generateSignature($method, $url, \DateTimeImmutable $expires) private function generateSignature(string $method, $url, \DateTimeImmutable $expires)
{ {
if ('POST' === $method) { if ('POST' === $method) {
return $this->generateSignaturePost($url, $expires); return $this->generateSignaturePost($url, $expires);
} }
$path = \parse_url((string) $url, PHP_URL_PATH); $path = \parse_url((string) $url, PHP_URL_PATH);
$body = sprintf( $body = sprintf(
"%s\n%s\n%s", "%s\n%s\n%s",
$method, strtoupper($method),
$expires->format('U'), $expires->format('U'),
$path $path
) );
;
$this->logger->debug( $this->logger->debug(
'generate signature GET', 'generate signature GET',

View File

@ -16,7 +16,8 @@ interface TempUrlGeneratorInterface
public function generatePost( public function generatePost(
?int $expire_delay = null, ?int $expire_delay = null,
?int $submit_delay = null, ?int $submit_delay = null,
int $max_file_count = 1 int $max_file_count = 1,
?string $object_name = null,
): SignedUrlPost; ): SignedUrlPost;
public function generate(string $method, string $object_name, ?int $expire_delay = null): SignedUrl; public function generate(string $method, string $object_name, ?int $expire_delay = null): SignedUrl;

View File

@ -25,7 +25,7 @@ class AsyncUploadExtension extends AbstractExtension
{ {
public function __construct( public function __construct(
private readonly TempUrlGeneratorInterface $tempUrlGenerator, private readonly TempUrlGeneratorInterface $tempUrlGenerator,
private readonly UrlGeneratorInterface $routingUrlGenerator private readonly UrlGeneratorInterface $routingUrlGenerator,
) {} ) {}
public function getFilters() public function getFilters()

View File

@ -11,9 +11,11 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Controller; namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlGeneratorException;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface; use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Security\Authorization\AsyncUploadVoter; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\MainBundle\Entity\User;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -30,62 +32,84 @@ final readonly class AsyncUploadController
private TempUrlGeneratorInterface $tempUrlGenerator, private TempUrlGeneratorInterface $tempUrlGenerator,
private SerializerInterface $serializer, private SerializerInterface $serializer,
private Security $security, private Security $security,
private LoggerInterface $logger, private LoggerInterface $chillLogger,
) {} ) {}
#[Route(path: '/asyncupload/temp_url/generate/{method}', name: 'async_upload.generate_url')] #[Route(path: '/api/1.0/doc-store/async-upload/temp_url/{uuid}/generate/post', name: 'chill_docstore_asyncupload_getsignedurlpost')]
public function getSignedUrl(string $method, Request $request): JsonResponse public function getSignedUrlPost(Request $request, StoredObject $storedObject): JsonResponse
{ {
try { if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
switch (strtolower($method)) { throw new AccessDeniedHttpException('not able to edit the given stored object');
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) { // we create a dummy version, to generate a filename
return (new JsonResponse((object) [ $version = $storedObject->registerVersion();
'message' => 'the object_name is null',
])) $p = $this->tempUrlGenerator
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST); ->generatePost(
} $request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null,
$p = $this->tempUrlGenerator->generate( $request->query->has('submit_delay') ? $request->query->getInt('submit_delay') : null,
$method, object_name: $version->getFilename()
$object_name, );
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null
); $this->chillLogger->notice('[Privacy Event] a request to upload a document has been generated', [
break; 'doc_uuid' => $storedObject->getUuid(),
default: ]);
return (new JsonResponse((object) ['message' => 'the method '
."{$method} is not valid"])) return new JsonResponse(
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST); $this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]),
Response::HTTP_OK,
[],
true
);
}
#[Route(path: '/api/1.0/doc-store/async-upload/temp_url/{uuid}/generate/{method}', name: 'chill_docstore_asyncupload_getsignedurlget', requirements: ['method' => 'get|head'])]
public function getSignedUrlGet(Request $request, StoredObject $storedObject, string $method): JsonResponse
{
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
throw new AccessDeniedHttpException('not able to read the given stored object');
}
// we really want to be sure that there are no other method than get or head:
if (!in_array($method, ['get', 'head'], true)) {
throw new AccessDeniedHttpException('Only methods get and head are allowed');
}
if ($request->query->has('version')) {
$filename = $request->query->get('version');
$storedObjectVersion = $storedObject->getVersions()->findFirst(fn (int $index, StoredObjectVersion $version): bool => $version->getFilename() === $filename);
if (null === $storedObjectVersion) {
// we are here in the case where the version is not stored into the database
// as the version is prefixed by the stored object prefix, we just have to check that this prefix
// is the same. It means that the user had previously the permission to "SEE_AND_EDIT" this stored
// object with same prefix that we checked before
if (!str_starts_with($filename, $storedObject->getPrefix())) {
throw new AccessDeniedHttpException('not able to match the version with the same filename');
}
} }
} catch (TempUrlGeneratorException $e) { } else {
$this->logger->warning('The client requested a temp url' $filename = $storedObject->getCurrentVersion()->getFilename();
.' 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)) { $p = $this->tempUrlGenerator->generate(
throw new AccessDeniedHttpException('not allowed to generate this signature'); $method,
} $filename,
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null
);
$user = $this->security->getUser();
$userId = match ($user instanceof User) {
true => $user->getId(),
false => $user->getUserIdentifier(),
};
$this->chillLogger->notice('[Privacy Event] a request to see a document has been granted', [
'doc_uuid' => $storedObject->getUuid()->toString(),
'user_id' => $userId,
]);
return new JsonResponse( return new JsonResponse(
$this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]), $this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]),

View File

@ -35,7 +35,7 @@ class DocumentAccompanyingCourseController extends AbstractController
protected TranslatorInterface $translator, protected TranslatorInterface $translator,
protected EventDispatcherInterface $eventDispatcher, protected EventDispatcherInterface $eventDispatcher,
protected AuthorizationHelper $authorizationHelper, protected AuthorizationHelper $authorizationHelper,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
) {} ) {}
#[Route(path: '/{id}/delete', name: 'chill_docstore_accompanying_course_document_delete')] #[Route(path: '/{id}/delete', name: 'chill_docstore_accompanying_course_document_delete')]

View File

@ -44,7 +44,7 @@ class DocumentPersonController extends AbstractController
protected AuthorizationHelper $authorizationHelper, protected AuthorizationHelper $authorizationHelper,
protected PDFSignatureZoneParser $PDFSignatureZoneParser, protected PDFSignatureZoneParser $PDFSignatureZoneParser,
protected StoredObjectManagerInterface $storedObjectManagerInterface, protected StoredObjectManagerInterface $storedObjectManagerInterface,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
) {} ) {}
#[Route(path: '/{id}/delete', name: 'chill_docstore_person_document_delete')] #[Route(path: '/{id}/delete', name: 'chill_docstore_person_document_delete')]

View File

@ -11,6 +11,46 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Controller; namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
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;
class StoredObjectApiController extends ApiController {} class StoredObjectApiController extends ApiController
{
public function __construct(
private readonly Security $security,
private readonly SerializerInterface $serializer,
private readonly EntityManagerInterface $entityManager,
) {}
/**
* Creates a new stored object.
*
* @return JsonResponse the response containing the serialized object in JSON format
*
* @throws AccessDeniedHttpException if the user does not have the necessary role to create a stored object
*/
#[Route('/api/1.0/doc-store/stored-object/create', methods: ['POST'])]
public function createStoredObject(): JsonResponse
{
if (!($this->security->isGranted('ROLE_ADMIN') || $this->security->isGranted('ROLE_USER'))) {
throw new AccessDeniedHttpException('Must be user or admin to create a stored object');
}
$object = new StoredObject();
$this->entityManager->persist($object);
$this->entityManager->flush();
return new JsonResponse(
$this->serializer->serialize($object, 'json', [AbstractNormalizer::GROUPS => ['read']]),
json: true
);
}
}

View File

@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Dav\Response\DavResponse;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@ -42,6 +43,7 @@ final readonly class WebdavController
private \Twig\Environment $engine, private \Twig\Environment $engine,
private StoredObjectManagerInterface $storedObjectManager, private StoredObjectManagerInterface $storedObjectManager,
private Security $security, private Security $security,
private EntityManagerInterface $entityManager,
) { ) {
$this->requestAnalyzer = new PropfindRequestAnalyzer(); $this->requestAnalyzer = new PropfindRequestAnalyzer();
} }
@ -201,6 +203,8 @@ final readonly class WebdavController
$this->storedObjectManager->write($storedObject, $request->getContent()); $this->storedObjectManager->write($storedObject, $request->getContent());
$this->entityManager->flush();
return new DavResponse('', Response::HTTP_NO_CONTENT); return new DavResponse('', Response::HTTP_NO_CONTENT);
} }

View File

@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\DependencyInjection; namespace Chill\DocStoreBundle\DependencyInjection;
use Chill\DocStoreBundle\Controller\StoredObjectApiController;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
@ -19,7 +18,6 @@ use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/** /**
@ -53,29 +51,6 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
$this->prependRoute($container); $this->prependRoute($container);
$this->prependAuthorization($container); $this->prependAuthorization($container);
$this->prependTwig($container); $this->prependTwig($container);
$this->prependApis($container);
}
protected function prependApis(ContainerBuilder $container)
{
$container->prependExtensionConfig('chill_main', [
'apis' => [
[
'class' => \Chill\DocStoreBundle\Entity\StoredObject::class,
'controller' => StoredObjectApiController::class,
'name' => 'stored_object',
'base_path' => '/api/1.0/docstore/stored-object',
'base_role' => 'ROLE_USER',
'actions' => [
'_entity' => [
'methods' => [
Request::METHOD_POST => true,
],
],
],
],
],
]);
} }
protected function prependAuthorization(ContainerBuilder $container) protected function prependAuthorization(ContainerBuilder $container)

View File

@ -42,7 +42,7 @@ class DocumentCategory
*/ */
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, name: 'id_inside_bundle')] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, name: 'id_inside_bundle')]
private $idInsideBundle private $idInsideBundle,
) {} ) {}
public function getBundleId() // ::class BundleClass (FQDN) public function getBundleId() // ::class BundleClass (FQDN)

View File

@ -16,10 +16,14 @@ use ChampsLibres\WopiLib\Contract\Entity\Document;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface; use Ramsey\Uuid\UuidInterface;
use Random\RandomException;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
/** /**
* Represent a document stored in an object store. * Represent a document stored in an object store.
@ -28,13 +32,16 @@ use Symfony\Component\Serializer\Annotation as Serializer;
* *
* The property `$deleteAt` allow a deletion of the document after the given date. But this property should * The property `$deleteAt` allow a deletion of the document after the given date. But this property should
* be set before the document is actually written by the StoredObjectManager. * be set before the document is actually written by the StoredObjectManager.
*
* Each version is stored within a @see{StoredObjectVersion}, associated with this current's object. The creation
* of each new version should be done using the method @see{self::registerVersion}.
*/ */
#[ORM\Entity] #[ORM\Entity]
#[ORM\Table('chill_doc.stored_object')] #[ORM\Table('stored_object', schema: 'chill_doc')]
#[AsyncFileExists(message: 'The file is not stored properly')]
class StoredObject implements Document, TrackCreationInterface class StoredObject implements Document, TrackCreationInterface
{ {
use TrackCreationTrait; use TrackCreationTrait;
final public const STATUS_EMPTY = 'empty';
final public const STATUS_READY = 'ready'; final public const STATUS_READY = 'ready';
final public const STATUS_PENDING = 'pending'; final public const STATUS_PENDING = 'pending';
final public const STATUS_FAILURE = 'failure'; final public const STATUS_FAILURE = 'failure';
@ -43,9 +50,11 @@ class StoredObject implements Document, TrackCreationInterface
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'datas')] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'datas')]
private array $datas = []; private array $datas = [];
#[Serializer\Groups(['write'])] /**
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)] * the prefix of each version.
private string $filename = ''; */
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
private string $prefix = '';
#[Serializer\Groups(['write'])] #[Serializer\Groups(['write'])]
#[ORM\Id] #[ORM\Id]
@ -53,25 +62,10 @@ class StoredObject implements Document, TrackCreationInterface
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null; private ?int $id = null;
/**
* @var int[]
*/
#[Serializer\Groups(['write'])] #[Serializer\Groups(['write'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'iv')] #[ORM\Column(name: 'title', type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
private array $iv = [];
#[Serializer\Groups(['write'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'key')]
private array $keyInfos = [];
#[Serializer\Groups(['write'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'title')]
private string $title = ''; private string $title = '';
#[Serializer\Groups(['write'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'type', options: ['default' => ''])]
private string $type = '';
#[Serializer\Groups(['write'])] #[Serializer\Groups(['write'])]
#[ORM\Column(type: 'uuid', unique: true)] #[ORM\Column(type: 'uuid', unique: true)]
private UuidInterface $uuid; private UuidInterface $uuid;
@ -94,14 +88,22 @@ class StoredObject implements Document, TrackCreationInterface
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
private string $generationErrors = ''; private string $generationErrors = '';
/**
* @var Collection<int, StoredObjectVersion>
*/
#[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)]
private Collection $versions;
/** /**
* @param StoredObject::STATUS_* $status * @param StoredObject::STATUS_* $status
*/ */
public function __construct( public function __construct(
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => 'ready'])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => 'ready'])]
private string $status = 'ready' private string $status = 'empty',
) { ) {
$this->uuid = Uuid::uuid4(); $this->uuid = Uuid::uuid4();
$this->versions = new ArrayCollection();
$this->prefix = self::generatePrefix();
} }
public function addGenerationTrial(): self public function addGenerationTrial(): self
@ -125,14 +127,34 @@ class StoredObject implements Document, TrackCreationInterface
return \DateTime::createFromImmutable($this->createdAt); return \DateTime::createFromImmutable($this->createdAt);
} }
#[AsyncFileExists(message: 'The file is not stored properly')]
#[Assert\NotNull(message: 'The store object version must be present')]
public function getCurrentVersion(): ?StoredObjectVersion
{
$maxVersion = null;
foreach ($this->versions as $v) {
if ($v->getVersion() > ($maxVersion?->getVersion() ?? -1)) {
$maxVersion = $v;
}
}
return $maxVersion;
}
public function getDatas(): array public function getDatas(): array
{ {
return $this->datas; return $this->datas;
} }
public function getPrefix(): string
{
return $this->prefix;
}
public function getFilename(): string public function getFilename(): string
{ {
return $this->filename; return $this->getCurrentVersion()?->getFilename() ?? '';
} }
public function getGenerationTrialsCounter(): int public function getGenerationTrialsCounter(): int
@ -145,14 +167,17 @@ class StoredObject implements Document, TrackCreationInterface
return $this->id; return $this->id;
} }
/**
* @return list<int>
*/
public function getIv(): array public function getIv(): array
{ {
return $this->iv; return $this->getCurrentVersion()?->getIv() ?? [];
} }
public function getKeyInfos(): array public function getKeyInfos(): array
{ {
return $this->keyInfos; return $this->getCurrentVersion()?->getKeyInfos() ?? [];
} }
/** /**
@ -171,14 +196,14 @@ class StoredObject implements Document, TrackCreationInterface
return $this->status; return $this->status;
} }
public function getTitle() public function getTitle(): string
{ {
return $this->title; return $this->title;
} }
public function getType() public function getType(): string
{ {
return $this->type; return $this->getCurrentVersion()?->getType() ?? '';
} }
public function getUuid(): UuidInterface public function getUuid(): UuidInterface
@ -209,27 +234,6 @@ class StoredObject implements Document, TrackCreationInterface
return $this; return $this;
} }
public function setFilename(?string $filename): self
{
$this->filename = (string) $filename;
return $this;
}
public function setIv(?array $iv): self
{
$this->iv = (array) $iv;
return $this;
}
public function setKeyInfos(?array $keyInfos): self
{
$this->keyInfos = (array) $keyInfos;
return $this;
}
/** /**
* @param StoredObject::STATUS_* $status * @param StoredObject::STATUS_* $status
*/ */
@ -247,18 +251,16 @@ class StoredObject implements Document, TrackCreationInterface
return $this; return $this;
} }
public function setType(?string $type): self
{
$this->type = (string) $type;
return $this;
}
public function getTemplate(): ?DocGeneratorTemplate public function getTemplate(): ?DocGeneratorTemplate
{ {
return $this->template; return $this->template;
} }
public function getVersions(): Collection
{
return $this->versions;
}
public function hasTemplate(): bool public function hasTemplate(): bool
{ {
return null !== $this->template; return null !== $this->template;
@ -314,18 +316,65 @@ class StoredObject implements Document, TrackCreationInterface
return $this; return $this;
} }
public function saveHistory(): void public function registerVersion(
{ array $iv = [],
if ('' === $this->getFilename()) { array $keyInfos = [],
return; string $type = '',
?string $filename = null,
): StoredObjectVersion {
$version = new StoredObjectVersion(
$this,
null === $this->getCurrentVersion() ? 0 : $this->getCurrentVersion()->getVersion() + 1,
$iv,
$keyInfos,
$type,
$filename
);
$this->versions->add($version);
if ('empty' === $this->status) {
$this->status = self::STATUS_READY;
} }
$this->datas['history'][] = [ return $version;
'filename' => $this->getFilename(), }
'iv' => $this->getIv(),
'key_infos' => $this->getKeyInfos(), public function removeVersion(StoredObjectVersion $storedObjectVersion): void
'type' => $this->getType(), {
'before' => (new \DateTimeImmutable('now'))->getTimestamp(), if (!$this->versions->contains($storedObjectVersion)) {
]; throw new \UnexpectedValueException('This stored object does not contains this version');
}
$this->versions->removeElement($storedObjectVersion);
}
/**
* @deprecated
*/
public function saveHistory(): void {}
public static function generatePrefix(): string
{
try {
return base_convert(bin2hex(random_bytes(32)), 16, 36);
} catch (RandomException) {
return uniqid(more_entropy: true);
}
}
/**
* Checks if a stored object can be deleted.
*
* Currently, return true if the deletedAt date is below the current date, and the object
* does not contains any version (which must be removed first).
*
* @param \DateTimeImmutable $now the current date and time
* @param StoredObject $storedObject the stored object to check
*
* @return bool returns true if the stored object can be deleted, false otherwise
*/
public static function canBeDeleted(\DateTimeImmutable $now, StoredObject $storedObject): bool
{
return $storedObject->getDeleteAt() < $now && $storedObject->getVersions()->isEmpty();
} }
} }

View File

@ -0,0 +1,127 @@
<?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\Entity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Doctrine\ORM\Mapping as ORM;
use Random\RandomException;
/**
* Store each version of StoredObject's.
*
* A version should not be created manually: use the method @see{StoredObject::registerVersion} instead.
*/
#[ORM\Entity]
#[ORM\Table('chill_doc.stored_object_version')]
#[ORM\UniqueConstraint(name: 'chill_doc_stored_object_version_unique_by_object', columns: ['stored_object_id', 'version'])]
class StoredObjectVersion implements TrackCreationInterface
{
use TrackCreationTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
/**
* filename of the version in the stored object.
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
private string $filename = '';
public function __construct(
/**
* The stored object associated with this version.
*/
#[ORM\ManyToOne(targetEntity: StoredObject::class, inversedBy: 'versions')]
#[ORM\JoinColumn(name: 'stored_object_id', nullable: false)]
private StoredObject $storedObject,
/**
* The incremental version.
*/
#[ORM\Column(name: 'version', type: \Doctrine\DBAL\Types\Types::INTEGER, options: ['default' => 0])]
private int $version = 0,
/**
* vector for encryption.
*
* @var int[]
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'iv')]
private array $iv = [],
/**
* Key infos for document encryption.
*
* @var array
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'key')]
private array $keyInfos = [],
/**
* type of the document.
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'type', options: ['default' => ''])]
private string $type = '',
?string $filename = null,
) {
$this->filename = $filename ?? self::generateFilename($this);
}
public static function generateFilename(StoredObjectVersion $storedObjectVersion): string
{
try {
$suffix = base_convert(bin2hex(random_bytes(8)), 16, 36);
} catch (RandomException) {
$suffix = uniqid(more_entropy: true);
}
return $storedObjectVersion->getStoredObject()->getPrefix().'/'.$suffix;
}
public function getFilename(): string
{
return $this->filename;
}
public function getId(): ?int
{
return $this->id;
}
public function getIv(): array
{
return $this->iv;
}
public function getKeyInfos(): array
{
return $this->keyInfos;
}
public function getStoredObject(): StoredObject
{
return $this->storedObject;
}
public function getType(): string
{
return $this->type;
}
public function getVersion(): int
{
return $this->version;
}
}

View File

@ -27,7 +27,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
final class AccompanyingCourseDocumentType extends AbstractType final class AccompanyingCourseDocumentType extends AbstractType
{ {
public function __construct( public function __construct(
private readonly TranslatableStringHelperInterface $translatableStringHelper private readonly TranslatableStringHelperInterface $translatableStringHelper,
) {} ) {}
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)

View File

@ -55,16 +55,8 @@ class StoredObjectDataMapper implements DataMapperInterface
return; return;
} }
/** @var StoredObject $viewData */ /* @var StoredObject $viewData */
if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) { $viewData = $forms['stored_object']->getData();
// we want to keep the previous history
$viewData->saveHistory();
}
$viewData->setFilename($forms['stored_object']->getData()['filename']);
$viewData->setIv($forms['stored_object']->getData()['iv']);
$viewData->setKeyInfos($forms['stored_object']->getData()['keyInfos']);
$viewData->setType($forms['stored_object']->getData()['type']);
if (array_key_exists('title', $forms)) { if (array_key_exists('title', $forms)) {
$viewData->setTitle($forms['title']->getData()); $viewData->setTitle($forms['title']->getData());

View File

@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Form\DataTransformer; namespace Chill\DocStoreBundle\Form\DataTransformer;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
@ -20,7 +19,7 @@ use Symfony\Component\Serializer\SerializerInterface;
class StoredObjectDataTransformer implements DataTransformerInterface class StoredObjectDataTransformer implements DataTransformerInterface
{ {
public function __construct( public function __construct(
private readonly SerializerInterface $serializer private readonly SerializerInterface $serializer,
) {} ) {}
public function transform(mixed $value): mixed public function transform(mixed $value): mixed
@ -30,11 +29,7 @@ class StoredObjectDataTransformer implements DataTransformerInterface
} }
if ($value instanceof StoredObject) { if ($value instanceof StoredObject) {
return $this->serializer->serialize($value, 'json', [ return $this->serializer->serialize($value, 'json');
'groups' => [
StoredObjectNormalizer::ADD_DAV_EDIT_LINK_CONTEXT,
],
]);
} }
throw new UnexpectedTypeException($value, StoredObject::class); throw new UnexpectedTypeException($value, StoredObject::class);
@ -46,6 +41,6 @@ class StoredObjectDataTransformer implements DataTransformerInterface
return null; return null;
} }
return json_decode((string) $value, true, 10, JSON_THROW_ON_ERROR); return $this->serializer->deserialize($value, StoredObject::class, 'json');
} }
} }

View File

@ -55,7 +55,7 @@ class AsyncUploaderType extends AbstractType
public function buildView( public function buildView(
FormView $view, FormView $view,
FormInterface $form, FormInterface $form,
array $options array $options,
) { ) {
$view->vars['attr']['data-async-file-upload'] = true; $view->vars['attr']['data-async-file-upload'] = true;
$view->vars['attr']['data-generate-temp-url-post'] = $this $view->vars['attr']['data-generate-temp-url-post'] = $this

View File

@ -20,7 +20,7 @@ interface GenericDocForAccompanyingPeriodProviderInterface
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null, ?\DateTimeImmutable $endDate = null,
?string $content = null, ?string $content = null,
?string $origin = null ?string $origin = null,
): FetchQueryInterface; ): FetchQueryInterface;
/** /**

View File

@ -20,7 +20,7 @@ interface GenericDocForPersonProviderInterface
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null, ?\DateTimeImmutable $endDate = null,
?string $content = null, ?string $content = null,
?string $origin = null ?string $origin = null,
): FetchQueryInterface; ): FetchQueryInterface;
/** /**

View File

@ -46,7 +46,7 @@ final readonly class Manager
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null, ?\DateTimeImmutable $endDate = null,
?string $content = null, ?string $content = null,
array $places = [] array $places = [],
): int { ): int {
['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod, $startDate, $endDate, $content, $places); ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod, $startDate, $endDate, $content, $places);
@ -76,7 +76,7 @@ final readonly class Manager
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null, ?\DateTimeImmutable $endDate = null,
?string $content = null, ?string $content = null,
array $places = [] array $places = [],
): int { ): int {
['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person, $startDate, $endDate, $content, $places); ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person, $startDate, $endDate, $content, $places);
@ -97,7 +97,7 @@ final readonly class Manager
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null, ?\DateTimeImmutable $endDate = null,
?string $content = null, ?string $content = null,
array $places = [] array $places = [],
): iterable { ): iterable {
['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod, $startDate, $endDate, $content, $places); ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($accompanyingPeriod, $startDate, $endDate, $content, $places);
@ -140,7 +140,7 @@ final readonly class Manager
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null, ?\DateTimeImmutable $endDate = null,
?string $content = null, ?string $content = null,
array $places = [] array $places = [],
): iterable { ): iterable {
['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person, $startDate, $endDate, $content, $places); ['sql' => $sql, 'params' => $params, 'types' => $types] = $this->buildUnionQuery($person, $startDate, $endDate, $content, $places);

View File

@ -34,7 +34,7 @@ final readonly class PersonDocumentGenericDocProvider implements GenericDocForPe
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null, ?\DateTimeImmutable $endDate = null,
?string $content = null, ?string $content = null,
?string $origin = null ?string $origin = null,
): FetchQueryInterface { ): FetchQueryInterface {
return $this->personDocumentACLAwareRepository->buildFetchQueryForPerson( return $this->personDocumentACLAwareRepository->buildFetchQueryForPerson(
$person, $person,

View File

@ -31,13 +31,13 @@ interface PersonDocumentACLAwareRepositoryInterface
Person $person, Person $person,
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null, ?\DateTimeImmutable $endDate = null,
?string $content = null ?string $content = null,
): FetchQueryInterface; ): FetchQueryInterface;
public function buildFetchQueryForAccompanyingPeriod( public function buildFetchQueryForAccompanyingPeriod(
AccompanyingPeriod $period, AccompanyingPeriod $period,
?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null, ?\DateTimeImmutable $endDate = null,
?string $content = null ?string $content = null,
): FetchQueryInterface; ): FetchQueryInterface;
} }

View File

@ -25,7 +25,7 @@ readonly class PersonDocumentRepository implements ObjectRepository, AssociatedE
private EntityRepository $repository; private EntityRepository $repository;
public function __construct( public function __construct(
private EntityManagerInterface $entityManager private EntityManagerInterface $entityManager,
) { ) {
$this->repository = $this->entityManager->getRepository($this->getClassName()); $this->repository = $this->entityManager->getRepository($this->getClassName());
} }

View File

@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface
{ {
@ -53,6 +54,21 @@ final readonly class StoredObjectRepository implements StoredObjectRepositoryInt
return $this->repository->findOneBy($criteria); return $this->repository->findOneBy($criteria);
} }
public function findByExpired(\DateTimeImmutable $expiredAtDate): iterable
{
$qb = $this->repository->createQueryBuilder('stored_object');
$qb
->where('stored_object.deleteAt <= :expiredAt')
->setParameter('expiredAt', $expiredAtDate);
return $qb->getQuery()->toIterable(hydrationMode: Query::HYDRATE_OBJECT);
}
public function findOneByUUID(string $uuid): ?StoredObject
{
return $this->repository->findOneBy(['uuid' => $uuid]);
}
public function getClassName(): string public function getClassName(): string
{ {
return StoredObject::class; return StoredObject::class;

View File

@ -17,4 +17,12 @@ use Doctrine\Persistence\ObjectRepository;
/** /**
* @extends ObjectRepository<StoredObject> * @extends ObjectRepository<StoredObject>
*/ */
interface StoredObjectRepositoryInterface extends ObjectRepository {} interface StoredObjectRepositoryInterface extends ObjectRepository
{
/**
* @return iterable<StoredObject>
*/
public function findByExpired(\DateTimeImmutable $expiredAtDate): iterable;
public function findOneByUUID(string $uuid): ?StoredObject;
}

View File

@ -0,0 +1,92 @@
<?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\Repository;
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
/**
* @implements ObjectRepository<StoredObjectVersion>
*/
class StoredObjectVersionRepository implements ObjectRepository
{
private readonly EntityRepository $repository;
private readonly Connection $connection;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(StoredObjectVersion::class);
$this->connection = $entityManager->getConnection();
}
public function find($id): ?StoredObjectVersion
{
return $this->repository->find($id);
}
public function findAll(): array
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?StoredObjectVersion
{
return $this->repository->findOneBy($criteria);
}
/**
* Finds the IDs of versions older than a given date and that are not the last version.
*
* Those version are good candidates for a deletion.
*
* @param \DateTimeImmutable $beforeDate the date to compare versions against
*
* @return iterable returns an iterable with the IDs of the versions
*/
public function findIdsByVersionsOlderThanDateAndNotLastVersion(\DateTimeImmutable $beforeDate): iterable
{
$results = $this->connection->executeQuery(
self::QUERY_FIND_IDS_BY_VERSIONS_OLDER_THAN_DATE_AND_NOT_LAST_VERSION,
[$beforeDate],
[Types::DATETIME_IMMUTABLE]
);
foreach ($results->iterateAssociative() as $row) {
yield $row['sov_id'];
}
}
private const QUERY_FIND_IDS_BY_VERSIONS_OLDER_THAN_DATE_AND_NOT_LAST_VERSION = <<<'SQL'
SELECT
sov.id AS sov_id
FROM chill_doc.stored_object_version sov
WHERE
sov.createdat < ?::timestamp
AND
sov.version < (SELECT MAX(sub_sov.version) FROM chill_doc.stored_object_version sub_sov WHERE sub_sov.stored_object_id = sov.stored_object_id)
SQL;
public function getClassName(): string
{
return StoredObjectVersion::class;
}
}

View File

@ -1,5 +1,5 @@
import {makeFetch} from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; import {makeFetch} from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import {PostStoreObjectSignature} from "../../types"; import {PostStoreObjectSignature, StoredObject} from "../../types";
const algo = 'AES-CBC'; const algo = 'AES-CBC';
@ -21,11 +21,22 @@ const createFilename = (): string => {
return text; return text;
}; };
export const uploadFile = async (uploadFile: ArrayBuffer): Promise<string> => { /**
* Fetches a new stored object from the server.
*
* @async
* @function fetchNewStoredObject
* @returns {Promise<StoredObject>} A Promise that resolves to the newly created StoredObject.
*/
export const fetchNewStoredObject = async (): Promise<StoredObject> => {
return makeFetch("POST", '/api/1.0/doc-store/stored-object/create', null);
}
export const uploadVersion = async (uploadFile: ArrayBuffer, storedObject: StoredObject): Promise<string> => {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('expires_delay', "180"); params.append('expires_delay', "180");
params.append('submit_delay', "180"); params.append('submit_delay', "180");
const asyncData: PostStoreObjectSignature = await makeFetch("GET", URL_POST + "?" + params.toString()); const asyncData: PostStoreObjectSignature = await makeFetch("GET", `/api/1.0/doc-store/async-upload/temp_url/${storedObject.uuid}/generate/post` + "?" + params.toString());
const suffix = createFilename(); const suffix = createFilename();
const filename = asyncData.prefix + suffix; const filename = asyncData.prefix + suffix;
const formData = new FormData(); const formData = new FormData();
@ -50,7 +61,6 @@ export const uploadFile = async (uploadFile: ArrayBuffer): Promise<string> => {
} }
export const encryptFile = async (originalFile: ArrayBuffer): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => { export const encryptFile = async (originalFile: ArrayBuffer): Promise<[ArrayBuffer, Uint8Array, JsonWebKey]> => {
console.log('encrypt', originalFile);
const iv = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(16));
const key = await window.crypto.subtle.generateKey(keyDefinition, true, [ "encrypt", "decrypt" ]); const key = await window.crypto.subtle.generateKey(keyDefinition, true, [ "encrypt", "decrypt" ]);
const exportedKey = await window.crypto.subtle.exportKey('jwk', key); const exportedKey = await window.crypto.subtle.exportKey('jwk', key);

View File

@ -1,7 +1,7 @@
import {CollectionEventPayload} from "../../../../../ChillMainBundle/Resources/public/module/collection"; import {CollectionEventPayload} from "../../../../../ChillMainBundle/Resources/public/module/collection";
import {createApp} from "vue"; import {createApp} from "vue";
import DropFileWidget from "../../vuejs/DropFileWidget/DropFileWidget.vue" import DropFileWidget from "../../vuejs/DropFileWidget/DropFileWidget.vue"
import {StoredObject, StoredObjectCreated} from "../../types"; import {StoredObject, StoredObjectVersion} from "../../types";
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n"; import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
const i18n = _createI18n({}); const i18n = _createI18n({});
@ -30,15 +30,17 @@ const startApp = (divElement: HTMLDivElement, collectionEntry: null|HTMLLIElemen
DropFileWidget, DropFileWidget,
}, },
methods: { methods: {
addDocument: function(object: StoredObjectCreated): void { addDocument: function({stored_object, stored_object_version}: {stored_object: StoredObject, stored_object_version: StoredObjectVersion}): void {
console.log('object added', object); console.log('object added', stored_object);
this.$data.existingDoc = object; console.log('version added', stored_object_version);
input_stored_object.value = JSON.stringify(object); this.$data.existingDoc = stored_object;
this.$data.existingDoc.currentVersion = stored_object_version;
input_stored_object.value = JSON.stringify(this.$data.existingDoc);
}, },
removeDocument: function(object: StoredObject): void { removeDocument: function(object: StoredObject): void {
console.log('catch remove document', object); console.log('catch remove document', object);
input_stored_object.value = ""; input_stored_object.value = "";
this.$data.existingDoc = null; this.$data.existingDoc = undefined;
console.log('collectionEntry', collectionEntry); console.log('collectionEntry', collectionEntry);
if (null !== collectionEntry) { if (null !== collectionEntry) {

View File

@ -1,36 +1,51 @@
import {DateTime} from "../../../ChillMainBundle/Resources/public/types"; import {DateTime, User} from "../../../ChillMainBundle/Resources/public/types";
export type StoredObjectStatus = "ready"|"failure"|"pending"; export type StoredObjectStatus = "empty"|"ready"|"failure"|"pending";
export interface StoredObject { export interface StoredObject {
id: number, id: number,
title: string|null,
/** uuid: string,
* filename of the object in the object storage prefix: string,
*/ status: StoredObjectStatus,
filename: string, currentVersion: null|StoredObjectVersionCreated|StoredObjectVersionPersisted,
creationDate: DateTime, totalVersions: number,
datas: object, datas: object,
iv: number[], /** @deprecated */
keyInfos: object, creationDate: DateTime,
title: string, createdAt: DateTime|null,
type: string, createdBy: User|null,
uuid: string, _permissions: {
status: StoredObjectStatus, canEdit: boolean,
canSee: boolean,
},
_links?: { _links?: {
dav_link?: { dav_link?: {
href: string href: string
expiration: number expiration: number
}, },
} },
} }
export interface StoredObjectCreated { export interface StoredObjectVersion {
status: "stored_object_created", /**
filename: string, * filename of the object in the object storage
iv: Uint8Array, */
keyInfos: object, filename: string,
type: string, iv: number[],
keyInfos: JsonWebKey,
type: string,
}
export interface StoredObjectVersionCreated extends StoredObjectVersion {
persisted: false,
}
export interface StoredObjectVersionPersisted extends StoredObjectVersionCreated {
version: number,
id: number,
createdAt: DateTime|null,
createdBy: User|null,
} }
export interface StoredObjectStatusChange { export interface StoredObjectStatusChange {
@ -82,4 +97,4 @@ export interface Signature {
zones: SignatureZone[], zones: SignatureZone[],
} }
export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error'; export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error';

Some files were not shown because too many files have changed in this diff Show More