Compare commits

..

85 Commits

Author SHA1 Message Date
fad7bdf235 Add event subscriber to convert docs to PDF before signature
Introduce ConvertToPdfBeforeSignatureStepEventSubscriber to convert documents to PDF when reaching a signature step in the workflow. Includes tests to ensure the conversion process only triggers when necessary.
2024-09-10 14:29:20 +02:00
8521cea46c Add StoredObjectToPdfConverter service and unit tests
Introduced the StoredObjectToPdfConverter service to handle conversion of stored objects to PDF format. Added unit tests to ensure proper functionality, including versioning and exception handling.
2024-09-10 14:29:20 +02:00
4ead7ba761 Add signer type differentiation for workflows
Added a method to determine if the signer is a 'person' or 'user'. Updated the signature template to handle both types accordingly, ensuring the correct entity type is displayed in workflow signatures.
2024-09-10 14:29:20 +02:00
9721b166eb Enhance object version removal to exclude point-in-time versions
Add a check to exclude versions associated with points in time before deleting old object versions. This ensures that such versions are not mistakenly removed, providing greater data integrity. Updated tests and repository methods accordingly.
2024-09-10 14:29:20 +02:00
1b21cd6c33 Add StoredObjectPointInTime entity and related functionality
Implemented a new StoredObjectPointInTime entity to manage snapshots of stored objects. This includes related migrations, enum for reasons, repository, and integration with StoredObjectVersion.
2024-09-10 14:29:19 +02:00
97860a9487 Add WopiConverter service and update Collabora integration tests
Introduce the WopiConverter service to handle document-to-PDF conversion using Collabora Online. Extend and update related tests in WopiConvertToPdfTest and ConvertControllerTest for better coverage and reliability. Enhance the GitLab CI configuration to exclude new test category "collabora-integration".
2024-09-10 10:44:45 +02:00
2fb46c65c2 Refactor CancelStaleWorkflowCronJobTest to simplify setup
Replaced KernelTestCase with TestCase to simplify test setup and removed dependency on the database connection. Added NullLogger to replace mocked LoggerInterface during testing. Updated method call in tests to correctly reference CancelStaleWorkflowMessage class.
2024-09-09 15:16:10 +02:00
f4356ac249 Add test for detecting stale workflows and enhance handler
Added a new test to check if workflows are stale in EntityWorkflowTest. Enhanced CancelStaleWorkflowHandler to handle stale workflows more accurately, including checking if workflows have transitioned recently. Updated EntityWorkflow entity to cascade remove workflow steps.

Refactor tests for handler, to avoid using $kernel during tests
2024-09-09 15:08:39 +02:00
d152efe084 Refactor imports and remove redundant type strings
This commit refactors the usage of \DateTimeImmutable to ensure consistent namespacing and removes unnecessary string type declarations from constants in CancelStaleWorkflowCronJob. These changes improve code readability and maintainability.
2024-09-09 14:58:19 +02:00
ee9530d03f More conditions to find staled workflows 2024-09-09 10:46:35 +02:00
b97eabf0d2 Get workflowComponent directly from registry 2024-09-09 10:46:35 +02:00
2e69d2df90 Adjust test to work with actual workflow + minor fix of handler logic 2024-09-09 10:46:33 +02:00
cb446edd18 Suffix message class with 'Message' and add check on workflow to assert no transitions were applied since message placed in queue 2024-09-09 10:40:42 +02:00
5d84e997c1 Php cs fixes 2024-09-09 10:40:42 +02:00
35199b6993 Create test for CancelStaleWorkflowHandler: wip state 2024-09-09 10:40:42 +02:00
dab68fb409 Add CancelStaleWorkflowCronJobTest 2024-09-09 10:40:41 +02:00
6001bb6447 Add logger messages for possible debugging purposes 2024-09-09 10:40:41 +02:00
29fec50515 Add cronjob and repository method to find and cancel stale workflows every other day 2024-09-09 10:40:41 +02:00
34edb02cd0 Create message and handler for canceling stale workflows 2024-09-09 10:40:41 +02:00
860ae5cedf Create CancelStaleWorkflow message and handler 2024-09-09 10:40:40 +02:00
bf056046ab Fix cs 2024-09-06 14:40:25 +02:00
4d73f9b81a Rename Convert to ConvertController for clarity
Renamed Convert class and relevant references to ConvertController to improve clarity and maintain consistency. Updated corresponding test files and route configurations to reflect the new name.
2024-09-06 14:07:51 +02:00
dd159f4379 Merge branch '297-workflow-en-attente' into 'signature-app-master'
Allow user to put workflow on hold

See merge request Chill-Projet/chill-bundles!718
2024-09-06 12:02:40 +00:00
49ad25b4c8 Refactor WorkflowOnHoldController and improve tests
Refactored WorkflowOnHoldController to remove dependencies and improve redirect handling. Updated test cases to use PHPUnit instead of KernelTestCase and mock proper dependencies. Added relationship handling in EntityWorkflowStep to manage EntityWorkflowStepHold instances accurately.
2024-09-06 14:02:11 +02:00
ad94310981 Refactor workflow hold functionality to avoid relying on database to perform some checks on "user holds entityworkflow"
Simplify the logic in handling workflow on hold status by moving related checks and operations to `EntityWorkflowStep` and `EntityWorkflow` entities. This includes implementing new methods to check if a step or workflow is held by a specific user and refactoring the controller actions to use these methods.
2024-09-05 20:46:29 +02:00
e8f09b507f Fix cs after php-cs-fixer upgrade, fix phpstan and rector errors 2024-09-05 18:00:37 +02:00
e29e1db6ed Take into account 'destUserByAccessKey' in the list of workflows associated to a user
Refactor the query to include checks for user membership via both 'destUser' and 'destUserByAccessKey'. This ensures that workflows correctly account for user access by multiple criteria.
2024-09-05 17:38:18 +02:00
8c4f342ca1 Correct instantiation of controller within test 2024-09-05 17:19:28 +02:00
745a29f742 Adjust logic for removing the hold on a workflow only by user who owns the hold and when a transition is applied on the workflow 2024-09-05 17:19:27 +02:00
41ffc470a0 Adjust test to check creation of onHold if user is allowed to apply a transition 2024-09-05 17:19:27 +02:00
46b31ae1ea Adjust logic to only allow on hold if user is allowed to apply a transition 2024-09-05 17:19:26 +02:00
8c5e94e295 Work on test for workflowOnHold controller 2024-09-05 17:19:25 +02:00
9c8a84cdbd Add test for WorkflowOnHoldControllerTest 2024-09-05 17:19:24 +02:00
a82b99aecc Add template-extends on EntityWorkflowStepHoldRepository 2024-09-05 17:19:23 +02:00
deb4bda16e Add unique constraint as attribute on EntityWorkflowStepHold class 2024-09-05 17:19:23 +02:00
c1e5f4a57e Move onHold methods to a separate WorkflowOnHold controller 2024-09-05 17:19:20 +02:00
6fc5a10dc4 Add unique constraint to migration 2024-09-05 17:18:43 +02:00
18abc84e68 Rector fixes 2024-09-05 17:18:43 +02:00
e85c31826f php cs fixes 2024-09-05 17:18:42 +02:00
d119ba49f7 Add on_hold label to vue components 2024-09-05 17:18:42 +02:00
c21de777fd Place on_hold label next to breadcrumb in twig template 2024-09-05 17:18:42 +02:00
15eaf648df Add label to workflow listing and history to indicate whether it's on hold 2024-09-05 17:18:42 +02:00
42471269db Implement logic to put workflow on hold and resume it 2024-09-05 17:16:18 +02:00
9475a708c3 Add EntityWorkflowStepHold entity to allow workflow to be put on hold by user
Entity created, migration, and repository.
2024-09-05 17:13:20 +02:00
bf1af1aaad Merge branch '301-has-signed-statement' into 'signature-app-master'
Adjust behavior when signature in signed state

See merge request Chill-Projet/chill-bundles!717
2024-09-05 14:50:05 +00:00
8ea87053f0 fix some tests 2024-09-05 16:47:45 +02:00
3c9ee41b3b fix some tests 2024-09-04 18:30:18 +02:00
d0031e82e8 Add hover effect and slim class to dev assets and apply in signature list
Introduced a new SCSS file to handle hover effects on rows and added the ability to remove bottom margins with a "slim" class. Updated various twig templates to utilize these new styles for better visual feedback and alignment.
2024-09-04 17:55:01 +02:00
20f2bc6c35 Enhance signature display with detailed person information
Updated the signature view template to include person details using the '_insert_vue_onthefly.html.twig' template. This change adds more contextual information about the signer, such as their name and status, improving the user experience.
2024-09-04 17:26:46 +02:00
71d3aa3969 Refactor signature rendering logic
Reorganized the signature rendering loop for better readability. Moved the row alignment inside the loop and added text alignment for signed statements. Simplified the conditional checks within the loop to enhance code maintainability.
2024-09-04 17:24:11 +02:00
ce781a5b58 Translate datetime object with icu: split date and time
Translate datetime within icu format
2024-09-04 17:14:43 +02:00
2dd275a074 Merge branch '296-signature-menu-tab' into 'signature-app-master'
Add person menu entry for signature  list

See merge request Chill-Projet/chill-bundles!719
2024-09-04 15:13:14 +00:00
5f5d4b8f06 Add person menu entry for signature list 2024-09-04 15:13:14 +00:00
cc8214d52c Add flash message in case signature was already applied 2024-09-04 17:08:38 +02:00
0c797c2997 Redirect to workflow show page if document already signed
Verify the state of the signature. If isSigned is true, redirection to
workflow show page.
2024-09-04 17:08:37 +02:00
ee6edba206 Create isSigned method in EntityWorkflowStepSignature.php
Refactorization and ease of use in template and workflow controller
2024-09-04 17:08:35 +02:00
3e6d764b9b Replace sign button with signed statement if person/user has signed 2024-09-04 17:06:45 +02:00
3e5a558cdf Add exists method to RemoveOldVersionMessageHandlerTest
Introduce the exists method to handle checks for StoredObject or StoredObjectVersion in test cases. This modification ensures consistency and proper exception handling in the testing framework.
2024-09-04 15:18:07 +02:00
0e6b7d76a4 Add exists method to WebdavControllerTest
Included a new method `exists` to handle existence checks for StoredObject or StoredObjectVersion within WebdavControllerTest. This method currently always returns true, ensuring initial compatibility and providing a foundation for future logic refinements.
2024-09-04 15:10:18 +02:00
b2042bd1e4 Merge branch 'signature-app/object-version' into 'signature-app-master'
Add versioning to stored objects

See merge request Chill-Projet/chill-bundles!710
2024-09-04 12:46:43 +00:00
6e9f111fd9 Fix syntax errors in RemoveExpiredStoredObjectCronJob class
Correct syntax errors involving constant definitions in the RemoveExpiredStoredObjectCronJob class. This change ensures compliance with PHP's constant declaration standards.
2024-09-04 14:45:17 +02:00
313fb9ffdf PHP CS Fixer updated (3.63.1 -> v3.64.0) 2024-09-04 14:38:56 +02:00
063bc2857f Remove deprecated constants from StoredObjectNormalizer
Deleted the deprecated constants `ADD_DAV_SEE_LINK_CONTEXT` and `ADD_DAV_EDIT_LINK_CONTEXT` from the `StoredObjectNormalizer` class. These constants are no longer in use and their removal cleans up the codebase.
2024-09-04 14:34:49 +02:00
615629d1b4 Simplify entity relationship and enforce foreign key constraints
Removed the resetStoredObject() method call from StoredObject, adjusted the storedObjectId column in the stored_object_version table to be NOT NULL, and moved the association setup of the StoredObject entity directly into the constructor in StoredObjectVersion. This ensures that every StoredObjectVersion has a valid StoredObject and maintains database integrity.
2024-09-04 14:28:13 +02:00
667e144681 Add PDF signature zone availability checks
Introduce `PDFSignatureZoneAvailable` service to check available PDF signature zones. Updated `WorkflowAddSignatureController` to use the new service. Added unit tests to verify the correctness of the functionality.
2024-09-04 14:09:53 +02:00
e17203ca3a Add PDF signature zone availability checks
Introduce `PDFSignatureZoneAvailable` service to check available PDF signature zones. Updated `WorkflowAddSignatureController` to use the new service. Added unit tests to verify the correctness of the functionality.
2024-09-04 13:55:08 +02:00
c6a6d76790 Add signatureZoneIndex to PdfSignedMessage and related classes
Included signatureZoneIndex for PdfSignedMessage in class definitions, handlers, and serializers to support signature zones. Updated test cases to reflect this new property, ensuring robust validation for handling and serialization.
2024-09-03 15:44:25 +02:00
3d49c959e0 Update DropFile to handle object versioning 2024-09-03 15:42:04 +02:00
b6edbb3eed Refactor StoredObject normalization handling
Deprecate and remove specific context constants from StoredObjectNormalizer. Update object properties for better clarity and add permissions handling. Introduce related tests and adjust other files relying on the old context constants.
2024-08-28 23:19:24 +02:00
00cc3b7806 Refactor backend for getting signed url 2024-08-28 18:00:20 +02:00
7ab52ff09e Add stored object creation endpoint
Introduced a new API endpoint to create stored objects with access control for roles 'ROLE_ADMIN' and 'ROLE_USER'. Updated corresponding routes, removed unused dependencies, and added unit tests to ensure functionality.
2024-08-28 15:34:42 +02:00
2d82c1e105 rector fixes after rector's upgrade 2024-08-28 15:34:13 +02:00
e477a49c92 Integrate SwaggerUI with direct download and cleanup package
Added a customized SwaggerUI HTML template under Dev directory. Removed outdated swagger-ui dependency from package.json to streamline dependency management.
2024-08-28 15:26:19 +02:00
0db2652f08 Add cron job for removing expired stored objects
Introduced `RemoveExpiredStoredObjectCronJob` to automate the deletion of expired stored objects every 7 days. Enhanced associated tests and updated relevant interfaces and classes to support the new cron job functionality.
2024-08-28 14:06:11 +02:00
c38f7c1179 Add functionality to delete old versions of documents
This commit introduces a feature that automatically deletes old versions of StoredObjects in the Chill application. A cron job, "RemoveOldVersionCronJob", has been implemented to delete versions older than 90 days. A message handler, "RemoveOldVersionMessageHandler", has been added to handle deletion requests. Furthermore, unit tests for the new functionality have been provided.
2024-08-28 14:06:10 +02:00
67d24cb951 Use "createdAt" from database to compute the last modified datetime in StoredObjectManager
The code has been updated to use 'createdAt' from StoredObjectVersion entity in StoredObjectManager. Specifically, if a 'createdAt' datetime is set, we return that datetime. This change also includes corresponding test cases to validate the functionality. The situation helps deal with files created before July 2024.
2024-08-28 14:06:09 +02:00
cb90261309 Update random_bytes length in filename and prefix generation
The size of the random byte string used in the generateFilename method of StoredObjectVersion has been reduced from 16 to 8. Conversely, the size of the random byte string used in the generatePrefix method of StoredObject has been increased from 8 to 32.

The naming generation fit better with the usage, as 16bytes are generated for each file (more version), and less for the version.
2024-08-28 14:06:09 +02:00
2feea24c41 Add Collabora configuration guide and NullProofValidator class
A new document detailing the configuration steps for enabling Collabora in development has been added. This includes setting up ngrok and configuring both the Collabora and Symfony servers. Furthermore, a new class, NullProofValidator, has been created for validation during the development process. This class always returns true, making it useful for debugging purposes.
2024-08-28 14:06:08 +02:00
1b16d4fe3b Flush entities when storing document using webdav / put operation
The WebdavController has been updated to flush the EntityManager after writing a document, while its tests have been adjusted correspondingly. A new test for the document PUT operation has also been added, which ensures the EntityManager flushes and the StoredObjectManager writes to this document.
2024-08-28 14:06:08 +02:00
ce5659219a Fix test 2024-08-28 14:06:07 +02:00
5fefe09a39 Fix test 2024-08-28 14:06:07 +02:00
e21db73b84 Refactor and simplify document management functionality to adapt to StoredObject versioning
This commit includes several updates to the document management functionality within ChillWopiBundle and ChillDocGeneratorBundle, refactoring for simplicity and improved readability.
2024-08-28 14:06:07 +02:00
3978ea9a47 Update StoredObjectManager to handle versioned StoredObjects
The StoredObjectManager and related test cases have been updated to handle versioned StoredObjects, allowing the same methods to work with either a StoredObject or its versions. The changes also involve return information for the write method and enhancements to the write test procedure. This provides more functionality and flexibility for handling StoredObjects in different versions.
2024-08-28 14:06:06 +02:00
4fbb7811ac Refactor StoredObjectDataMapper to use registerVersion method
Removed saveHistory and set methods in StoredObjectDataMapper and replaced them with one call to registerVersion.
2024-08-28 14:06:06 +02:00
2b7ea4178b Add versioning to stored objects
This update introduces a versioning system to the stored objects in the ChillDocStoreBundle. The 'StoredObject' entity now includes several new methods, and maintains a collection of 'StoredObjectVersion' instances. Each time a 'StoredObject' is modified, a new version instance is created and added to the collection, ensuring a history of changes. Migration file for the addition of new database column included. Corresponding tests are also updated.
2024-08-28 14:06:06 +02:00
427 changed files with 6148 additions and 1695 deletions

View File

@@ -122,7 +122,7 @@ unit_tests:
- php tests/console chill:db:sync-views --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=4G bin/phpunit --colors=never --exclude-group dbIntensive
- php -d memory_limit=4G bin/phpunit --colors=never --exclude-group dbIntensive,openstack-integration,collabora-integration
artifacts:
expire_in: 1 day
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",
"mime": "^4.0.0",
"pdfjs-dist": "^4.3.136",
"swagger-ui": "^4.15.5",
"vis-network": "^9.1.0",
"vue": "^3.2.37",
"vue-i18n": "^9.1.6",

View File

@@ -68,7 +68,7 @@ final class ActivityController extends AbstractController
private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
private readonly TranslatableStringHelperInterface $translatableStringHelper,
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(
protected ActivityReasonCategoryRepository $activityReasonCategoryRepository,
protected ActivityReasonRepository $activityReasonRepository,
protected TranslatableStringHelper $translatableStringHelper
protected TranslatableStringHelper $translatableStringHelper,
) {}
public function addRole(): ?string

View File

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

View File

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

View File

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

View File

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

View File

@@ -42,7 +42,7 @@ class StatActivityDuration implements ExportInterface, GroupedExportInterface
/**
* The action for this report.
*/
protected string $action = 'sum'
protected string $action = 'sum',
) {
$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 TranslatableStringHelperInterface $translatableStringHelper,
private readonly TranslatableStringExportLabelHelper $translatableStringLabelHelper,
private readonly UserHelper $userHelper
private readonly UserHelper $userHelper,
) {}
public function addSelect(QueryBuilder $qb): void

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ final readonly class ActivityDocumentACLAwareRepository implements ActivityDocum
private EntityManagerInterface $em,
private CenterResolverManagerInterface $centerResolverManager,
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
private Security $security
private Security $security,
) {}
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(
ManagerRegistry $registry,
private readonly RequestStack $requestStack
private readonly RequestStack $requestStack,
) {
parent::__construct($registry, ActivityReason::class);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ class CalendarController extends AbstractController
private readonly AccompanyingPeriodRepository $accompanyingPeriodRepository,
private readonly UserRepositoryInterface $userRepository,
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,
#[ORM\ManyToOne(targetEntity: StoredObject::class, cascade: ['persist'])]
#[ORM\JoinColumn(nullable: false)]
private ?StoredObject $storedObject
private ?StoredObject $storedObject,
) {
$this->setCalendar($calendar);
$this->datetimeVersion = $calendar->getDateTimeVersion();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface
'alwaysEnabled' => true,
'scheduled' => RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledStartDateTime']['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,
int $expiration,
?string $id = null,
?string $secret = null
?string $secret = null,
): void {
$user->setAttributeByDomain(self::METADATA_KEY, self::EXPIRATION_SUBSCRIPTION_EVENT, $expiration);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ final readonly class PersonCalendarGenericDocProvider implements GenericDocForPe
public function __construct(
private Security $security,
private EntityManagerInterface $em
private EntityManagerInterface $em,
) {}
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(
string $classTransformer,
string $objClass
string $objClass,
) {
$transformer = $this->prophesize($classTransformer);
$transformer->transform(Argument::type('array'))
@@ -195,7 +195,7 @@ final class CalendarTypeTest extends TypeTestCase
private function buildSingleToIdDataTransformer(
string $classTransformer,
string $class
string $class,
) {
$transformer = $this->prophesize($classTransformer);
$transformer->transform(Argument::type('object'))

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ class CustomFieldsGroupController extends AbstractController
public function __construct(
private readonly CustomFieldProvider $customFieldProvider,
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
*/
private readonly TranslatableStringHelper $translatableStringHelper
private readonly TranslatableStringHelper $translatableStringHelper,
) {}
public function allowOtherChoice(CustomField $cf)

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ class CustomFieldText extends AbstractCustomField
public function __construct(
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
*/
private readonly TranslatableStringHelper $translatableStringHelper
private readonly TranslatableStringHelper $translatableStringHelper,
) {}
public function buildForm(FormBuilderInterface $builder, CustomField $customField)

View File

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

View File

@@ -48,7 +48,7 @@ final class DocGeneratorTemplateController extends AbstractController
private readonly PaginatorFactory $paginatorFactory,
private readonly EntityManagerInterface $entityManager,
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')]
@@ -56,7 +56,7 @@ final class DocGeneratorTemplateController extends AbstractController
DocGeneratorTemplate $template,
string $entityClassName,
int $entityId,
Request $request
Request $request,
): Response {
return $this->generateDocFromTemplate(
$template,
@@ -71,7 +71,7 @@ final class DocGeneratorTemplateController extends AbstractController
DocGeneratorTemplate $template,
string $entityClassName,
int $entityId,
Request $request
Request $request,
): Response {
return $this->generateDocFromTemplate(
$template,
@@ -137,7 +137,7 @@ final class DocGeneratorTemplateController extends AbstractController
DocGeneratorTemplate $template,
int $entityId,
Request $request,
bool $isTest
bool $isTest,
): Response {
try {
$context = $this->contextManager->getContextByDocGeneratorTemplate($template);

View File

@@ -54,12 +54,15 @@ class LoadDocGeneratorTemplate extends AbstractFixture
];
foreach ($templates as $template) {
$newStoredObj = (new StoredObject())
->setFilename($template['file']['filename'])
->setKeyInfos(json_decode($template['file']['key'], true))
->setIv(json_decode($template['file']['iv'], true))
$newStoredObj = (new StoredObject());
$newStoredObj
->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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ use Chill\DocGeneratorBundle\Service\Generator\Generator;
use Chill\DocGeneratorBundle\Service\Generator\ObjectReadyException;
use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
@@ -39,11 +40,11 @@ class GeneratorTest extends TestCase
public function testSuccessfulGeneration(): void
{
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
->setType('application/test'));
$templateStoredObject = new StoredObject();
$templateStoredObject->registerVersion(type: 'application/test');
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject);
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING);
$reflection = new \ReflectionClass($destinationStoredObject);
$reflection->getProperty('id')->setAccessible(true);
$reflection->getProperty('id')->setValue($destinationStoredObject, 1);
$entity = new class () {};
$data = [];
@@ -76,7 +77,14 @@ class GeneratorTest extends TestCase
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
$storedObjectManager->read($templateStoredObject)->willReturn('template');
$storedObjectManager->write($destinationStoredObject, 'generated')->shouldBeCalled();
$storedObjectManager->write($destinationStoredObject, 'generated', 'application/test')
->will(function ($args): StoredObjectVersion {
/** @var StoredObject $storedObject */
$storedObject = $args[0];
return $storedObject->registerVersion(type: $args[2]);
})
->shouldBeCalled();
$generator = new Generator(
$contextManagerInterface->reveal(),
@@ -107,8 +115,9 @@ class GeneratorTest extends TestCase
$this->prophesize(StoredObjectManagerInterface::class)->reveal()
);
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
->setType('application/test'));
$templateStoredObject = new StoredObject();
$templateStoredObject->registerVersion(type: 'application/test');
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject);
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_READY);
$generator->generateDocFromTemplate(
@@ -124,11 +133,11 @@ class GeneratorTest extends TestCase
{
$this->expectException(RelatedEntityNotFoundException::class);
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject())
->setType('application/test'));
$templateStoredObject = new StoredObject();
$templateStoredObject->registerVersion(type: 'application/test');
$template = (new DocGeneratorTemplate())->setFile($templateStoredObject);
$destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING);
$reflection = new \ReflectionClass($destinationStoredObject);
$reflection->getProperty('id')->setAccessible(true);
$reflection->getProperty('id')->setValue($destinationStoredObject, 1);
$context = $this->prophesize(DocGeneratorContextInterface::class);

View File

@@ -58,6 +58,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
?int $expire_delay = null,
?int $submit_delay = null,
int $max_file_count = 1,
?string $object_name = null,
): SignedUrlPost {
$delay = $expire_delay ?? $this->max_expire_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'));
$object_name = $this->generateObjectName();
if (null === $object_name) {
$object_name = $this->generateObjectName();
}
$g = new SignedUrlPost(
$url = $this->generateUrl($object_name),
@@ -141,7 +144,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
{
return match (str_ends_with($this->base_url, '/')) {
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);
}
private function generateSignature($method, $url, \DateTimeImmutable $expires)
private function generateSignature(string $method, $url, \DateTimeImmutable $expires)
{
if ('POST' === $method) {
return $this->generateSignaturePost($url, $expires);
}
$path = \parse_url((string) $url, PHP_URL_PATH);
$body = sprintf(
"%s\n%s\n%s",
$method,
strtoupper($method),
$expires->format('U'),
$path
)
;
);
$this->logger->debug(
'generate signature GET',

View File

@@ -16,7 +16,8 @@ interface TempUrlGeneratorInterface
public function generatePost(
?int $expire_delay = null,
?int $submit_delay = null,
int $max_file_count = 1
int $max_file_count = 1,
?string $object_name = null,
): SignedUrlPost;
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(
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
private readonly UrlGeneratorInterface $routingUrlGenerator
private readonly UrlGeneratorInterface $routingUrlGenerator,
) {}
public function getFilters()

View File

@@ -11,9 +11,11 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\AsyncUpload\Exception\TempUrlGeneratorException;
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 Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -30,62 +32,84 @@ final readonly class AsyncUploadController
private TempUrlGeneratorInterface $tempUrlGenerator,
private SerializerInterface $serializer,
private Security $security,
private LoggerInterface $logger,
private LoggerInterface $chillLogger,
) {}
#[Route(path: '/asyncupload/temp_url/generate/{method}', name: 'async_upload.generate_url')]
public function getSignedUrl(string $method, Request $request): JsonResponse
#[Route(path: '/api/1.0/doc-store/async-upload/temp_url/{uuid}/generate/post', name: 'chill_docstore_asyncupload_getsignedurlpost')]
public function getSignedUrlPost(Request $request, StoredObject $storedObject): JsonResponse
{
try {
switch (strtolower($method)) {
case 'post':
$p = $this->tempUrlGenerator
->generatePost(
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null,
$request->query->has('submit_delay') ? $request->query->getInt('submit_delay') : null
)
;
break;
case 'get':
case 'head':
$object_name = $request->query->get('object_name', null);
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
throw new AccessDeniedHttpException('not able to edit the given stored object');
}
if (null === $object_name) {
return (new JsonResponse((object) [
'message' => 'the object_name is null',
]))
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
}
$p = $this->tempUrlGenerator->generate(
$method,
$object_name,
$request->query->has('expires_delay') ? $request->query->getInt('expires_delay') : null
);
break;
default:
return (new JsonResponse((object) ['message' => 'the method '
."{$method} is not valid"]))
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
// we create a dummy version, to generate a filename
$version = $storedObject->registerVersion();
$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,
object_name: $version->getFilename()
);
$this->chillLogger->notice('[Privacy Event] a request to upload a document has been generated', [
'doc_uuid' => $storedObject->getUuid(),
]);
return new JsonResponse(
$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) {
$this->logger->warning('The client requested a temp url'
.' which sparkle an error.', [
'message' => $e->getMessage(),
'expire_delay' => $request->query->getInt('expire_delay', 0),
'file_count' => $request->query->getInt('file_count', 1),
'method' => $method,
]);
$p = new \stdClass();
$p->message = $e->getMessage();
$p->status = JsonResponse::HTTP_BAD_REQUEST;
return new JsonResponse($p, JsonResponse::HTTP_BAD_REQUEST);
} else {
$filename = $storedObject->getCurrentVersion()->getFilename();
}
if (!$this->security->isGranted(AsyncUploadVoter::GENERATE_SIGNATURE, $p)) {
throw new AccessDeniedHttpException('not allowed to generate this signature');
}
$p = $this->tempUrlGenerator->generate(
$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(
$this->serializer->serialize($p, 'json', [AbstractNormalizer::GROUPS => ['read']]),

View File

@@ -35,7 +35,7 @@ class DocumentAccompanyingCourseController extends AbstractController
protected TranslatorInterface $translator,
protected EventDispatcherInterface $eventDispatcher,
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')]

View File

@@ -44,7 +44,7 @@ class DocumentPersonController extends AbstractController
protected AuthorizationHelper $authorizationHelper,
protected PDFSignatureZoneParser $PDFSignatureZoneParser,
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')]

View File

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

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\DependencyInjection;
use Chill\DocStoreBundle\Controller\StoredObjectApiController;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
@@ -19,7 +18,6 @@ use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
@@ -53,29 +51,6 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
$this->prependRoute($container);
$this->prependAuthorization($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)

View File

@@ -42,7 +42,7 @@ class DocumentCategory
*/
#[ORM\Id]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, name: 'id_inside_bundle')]
private $idInsideBundle
private $idInsideBundle,
) {}
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\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Random\RandomException;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
/**
* 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
* 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\Table('chill_doc.stored_object')]
#[AsyncFileExists(message: 'The file is not stored properly')]
#[ORM\Table('stored_object', schema: 'chill_doc')]
class StoredObject implements Document, TrackCreationInterface
{
use TrackCreationTrait;
final public const STATUS_EMPTY = 'empty';
final public const STATUS_READY = 'ready';
final public const STATUS_PENDING = 'pending';
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')]
private array $datas = [];
#[Serializer\Groups(['write'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
private string $filename = '';
/**
* the prefix of each version.
*/
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
private string $prefix = '';
#[Serializer\Groups(['write'])]
#[ORM\Id]
@@ -53,25 +62,10 @@ class StoredObject implements Document, TrackCreationInterface
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
/**
* @var int[]
*/
#[Serializer\Groups(['write'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, name: 'iv')]
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')]
#[ORM\Column(name: 'title', type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
private string $title = '';
#[Serializer\Groups(['write'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, name: 'type', options: ['default' => ''])]
private string $type = '';
#[Serializer\Groups(['write'])]
#[ORM\Column(type: 'uuid', unique: true)]
private UuidInterface $uuid;
@@ -94,14 +88,22 @@ class StoredObject implements Document, TrackCreationInterface
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
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
*/
public function __construct(
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => 'ready'])]
private string $status = 'ready'
private string $status = 'empty',
) {
$this->uuid = Uuid::uuid4();
$this->versions = new ArrayCollection();
$this->prefix = self::generatePrefix();
}
public function addGenerationTrial(): self
@@ -125,14 +127,34 @@ class StoredObject implements Document, TrackCreationInterface
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
{
return $this->datas;
}
public function getPrefix(): string
{
return $this->prefix;
}
public function getFilename(): string
{
return $this->filename;
return $this->getCurrentVersion()?->getFilename() ?? '';
}
public function getGenerationTrialsCounter(): int
@@ -145,14 +167,17 @@ class StoredObject implements Document, TrackCreationInterface
return $this->id;
}
/**
* @return list<int>
*/
public function getIv(): array
{
return $this->iv;
return $this->getCurrentVersion()?->getIv() ?? [];
}
public function getKeyInfos(): array
{
return $this->keyInfos;
return $this->getCurrentVersion()?->getKeyInfos() ?? [];
}
/**
@@ -171,14 +196,14 @@ class StoredObject implements Document, TrackCreationInterface
return $this->status;
}
public function getTitle()
public function getTitle(): string
{
return $this->title;
}
public function getType()
public function getType(): string
{
return $this->type;
return $this->getCurrentVersion()?->getType() ?? '';
}
public function getUuid(): UuidInterface
@@ -209,27 +234,6 @@ class StoredObject implements Document, TrackCreationInterface
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
*/
@@ -247,18 +251,21 @@ class StoredObject implements Document, TrackCreationInterface
return $this;
}
public function setType(?string $type): self
{
$this->type = (string) $type;
return $this;
}
public function getTemplate(): ?DocGeneratorTemplate
{
return $this->template;
}
public function getVersions(): Collection
{
return $this->versions;
}
public function hasCurrentVersion(): bool
{
return null !== $this->getCurrentVersion();
}
public function hasTemplate(): bool
{
return null !== $this->template;
@@ -314,18 +321,65 @@ class StoredObject implements Document, TrackCreationInterface
return $this;
}
public function saveHistory(): void
{
if ('' === $this->getFilename()) {
return;
public function registerVersion(
array $iv = [],
array $keyInfos = [],
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'][] = [
'filename' => $this->getFilename(),
'iv' => $this->getIv(),
'key_infos' => $this->getKeyInfos(),
'type' => $this->getType(),
'before' => (new \DateTimeImmutable('now'))->getTimestamp(),
];
return $version;
}
public function removeVersion(StoredObjectVersion $storedObjectVersion): void
{
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,67 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\Mapping as ORM;
/**
* Represents a snapshot of a stored object at a specific point in time.
*
* This entity tracks versions of stored objects, reasons for the snapshot,
* and the user who initiated the action.
*/
#[ORM\Entity]
#[ORM\Table(name: 'stored_object_point_in_time', schema: 'chill_doc')]
class StoredObjectPointInTime implements TrackCreationInterface
{
use TrackCreationTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
public function __construct(
#[ORM\ManyToOne(targetEntity: StoredObjectVersion::class, inversedBy: 'pointInTimes')]
#[ORM\JoinColumn(name: 'stored_object_version_id', nullable: false)]
private StoredObjectVersion $objectVersion,
#[ORM\Column(name: 'reason', type: 'text', nullable: false, enumType: StoredObjectPointInTimeReasonEnum::class)]
private StoredObjectPointInTimeReasonEnum $reason,
#[ORM\ManyToOne(targetEntity: User::class)]
private ?User $byUser = null,
) {
$this->objectVersion->addPointInTime($this);
}
public function getId(): ?int
{
return $this->id;
}
public function getByUser(): ?User
{
return $this->byUser;
}
public function getObjectVersion(): StoredObjectVersion
{
return $this->objectVersion;
}
public function getReason(): StoredObjectPointInTimeReasonEnum
{
return $this->reason;
}
}

View File

@@ -0,0 +1,18 @@
<?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;
enum StoredObjectPointInTimeReasonEnum: string
{
case KEEP_BEFORE_CONVERSION = 'keep-before-conversion';
case KEEP_BY_USER = 'keep-by-user';
}

View File

@@ -0,0 +1,173 @@
<?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\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Selectable;
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 = '';
/**
* @var Collection<int, StoredObjectPointInTime>&Selectable<int, StoredObjectPointInTime>
*/
#[ORM\OneToMany(mappedBy: 'objectVersion', targetEntity: StoredObjectPointInTime::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
private Collection&Selectable $pointInTimes;
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);
$this->pointInTimes = new ArrayCollection();
}
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;
}
/**
* @return Collection<int, StoredObjectPointInTime>&Selectable<int, StoredObjectPointInTime>
*/
public function getPointInTimes(): Selectable&Collection
{
return $this->pointInTimes;
}
public function hasPointInTimes(): bool
{
return $this->pointInTimes->count() > 0;
}
/**
* @return $this
*
* @internal use @see{StoredObjectPointInTime} constructor instead
*/
public function addPointInTime(StoredObjectPointInTime $storedObjectPointInTime): self
{
if (!$this->pointInTimes->contains($storedObjectPointInTime)) {
$this->pointInTimes->add($storedObjectPointInTime);
}
return $this;
}
public function removePointInTime(StoredObjectPointInTime $storedObjectPointInTime): self
{
if ($this->pointInTimes->contains($storedObjectPointInTime)) {
$this->pointInTimes->removeElement($storedObjectPointInTime);
}
return $this;
}
}

View File

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

View File

@@ -55,16 +55,8 @@ class StoredObjectDataMapper implements DataMapperInterface
return;
}
/** @var StoredObject $viewData */
if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) {
// 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']);
/* @var StoredObject $viewData */
$viewData = $forms['stored_object']->getData();
if (array_key_exists('title', $forms)) {
$viewData->setTitle($forms['title']->getData());

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Form\DataTransformer;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Serializer\SerializerInterface;
@@ -20,7 +19,7 @@ use Symfony\Component\Serializer\SerializerInterface;
class StoredObjectDataTransformer implements DataTransformerInterface
{
public function __construct(
private readonly SerializerInterface $serializer
private readonly SerializerInterface $serializer,
) {}
public function transform(mixed $value): mixed
@@ -30,11 +29,7 @@ class StoredObjectDataTransformer implements DataTransformerInterface
}
if ($value instanceof StoredObject) {
return $this->serializer->serialize($value, 'json', [
'groups' => [
StoredObjectNormalizer::ADD_DAV_EDIT_LINK_CONTEXT,
],
]);
return $this->serializer->serialize($value, 'json');
}
throw new UnexpectedTypeException($value, StoredObject::class);
@@ -46,6 +41,6 @@ class StoredObjectDataTransformer implements DataTransformerInterface
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(
FormView $view,
FormInterface $form,
array $options
array $options,
) {
$view->vars['attr']['data-async-file-upload'] = true;
$view->vars['attr']['data-generate-temp-url-post'] = $this

View File

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

View File

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

View File

@@ -46,7 +46,7 @@ final readonly class Manager
?\DateTimeImmutable $startDate = null,
?\DateTimeImmutable $endDate = null,
?string $content = null,
array $places = []
array $places = [],
): int {
['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 $endDate = null,
?string $content = null,
array $places = []
array $places = [],
): int {
['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 $endDate = null,
?string $content = null,
array $places = []
array $places = [],
): iterable {
['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 $endDate = null,
?string $content = null,
array $places = []
array $places = [],
): iterable {
['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 $endDate = null,
?string $content = null,
?string $origin = null
?string $origin = null,
): FetchQueryInterface {
return $this->personDocumentACLAwareRepository->buildFetchQueryForPerson(
$person,

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
<?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\StoredObjectPointInTime;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @template-extends ServiceEntityRepository<StoredObjectPointInTime>
*/
class StoredObjectPointInTimeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, StoredObjectPointInTime::class);
}
}

View File

@@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\StoredObject;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface
{
@@ -53,6 +54,21 @@ final readonly class StoredObjectRepository implements StoredObjectRepositoryInt
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
{
return StoredObject::class;

View File

@@ -17,4 +17,12 @@ use Doctrine\Persistence\ObjectRepository;
/**
* @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;
}

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