Compare commits

..

19 Commits

Author SHA1 Message Date
d33dcacc46 Use better namespacing for configuring workflow signature documents 2024-07-18 16:09:34 +02:00
8d97df9f96 Remove trailing ParamConverter annotation 2024-07-18 15:17:35 +02:00
2822800c76 Changie added 2024-07-18 15:12:47 +02:00
8973b7c20b Move logic from twig template to controller and refactor workflow controller 2024-07-18 15:11:43 +02:00
7f144da1a7 Remove todo 2024-07-10 16:27:06 +02:00
ab4193938d Adjust the structure of the signature metadata 2024-07-10 15:39:38 +02:00
e2426ba1d8 Rename configuration parameter for document kinds 2024-07-10 15:38:51 +02:00
8209990437 Add todo as reminder to change isSignature logic in controller 2024-07-10 15:05:08 +02:00
b1885de3e2 Merge branch '288-signature-zone-workflow' of https://gitlab.com/Chill-Projet/chill-bundles into 288-signature-zone-workflow 2024-07-10 14:59:50 +02:00
218280304c php cs fixes 2024-07-10 12:49:51 +02:00
8a7b48b201 Implement logic to save metadata to signature 2024-07-10 12:49:05 +02:00
52a9aab73f Create form for adding document info in metadata property of signature 2024-07-10 12:48:48 +02:00
8f358112b1 Add configuration for signature document types 2024-07-10 12:48:03 +02:00
57a07af3db Display names and sign buttons for person or user signatures 2024-07-09 17:39:17 +02:00
fd216ff66e Remove metadata property from workflow entity 2024-07-09 15:47:06 +02:00
689c2c574a Correct translation 2024-07-09 10:05:15 +00:00
a8de18beac Layout template for adding signature zone to workflow 2024-07-04 17:12:32 +02:00
babca5fc0f Add metadata property to workflow to allow adding info about signatures needed 2024-07-04 17:11:57 +02:00
f2c5663b05 Adjust and add necessary twig templates for signature zone 2024-07-04 09:23:58 +02:00
377 changed files with 1174 additions and 17228 deletions

View File

@@ -0,0 +1,5 @@
kind: Feature
body: '[DX] move async-upload-bundle features into chill-bundles'
time: 2023-12-12T15:48:41.954970271+01:00
custom:
Issue: "221"

View File

@@ -0,0 +1,6 @@
kind: Feature
body: |
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
time: 2024-05-30T16:00:03.440767606+02:00
custom:
Issue: ""

View File

@@ -0,0 +1,6 @@
kind: Feature
body: |
Upgrade CKEditor and refactor configuration with use of typescript
time: 2024-05-31T19:02:42.776662753+02:00
custom:
Issue: ""

View File

@@ -1,7 +0,0 @@
kind: Feature
body: The behavoir of the voters for stored objects is adjusted so as to limit edit
and delete possibilities to users related to the activity, social action or workflow
entity.
time: 2024-06-14T15:35:37.582159301+02:00
custom:
Issue: "286"

View File

@@ -0,0 +1,6 @@
kind: Fixed
body: Fix resolving of centers for an household, which will fix in turn the access
control
time: 2024-04-10T10:37:36.462484988+02:00
custom:
Issue: ""

View File

@@ -1,6 +0,0 @@
## v2.22.0 - 2024-06-25
### Feature
* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module
### Traduction francophone
* Exports sont ajoutés pour la module événement.

View File

@@ -1,5 +0,0 @@
## v2.22.1 - 2024-07-01
### Fixed
* Remove debug word
### DX
* Add a command for reading official address DB from Luxembourg and update chill addresses

View File

@@ -1,3 +0,0 @@
## v2.22.2 - 2024-07-03
### Fixed
* Remove scope required for event participation stats

View File

@@ -1,11 +0,0 @@
## v2.23.0 - 2024-07-23
### Feature
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
* Add job bundle (module emploi)
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
* Upgrade CKEditor and refactor configuration with use of typescript
### Fixed
* Fix resolving of centers for an household, which will fix in turn the access control
* Resolved type hinting error in activity list export

View File

@@ -1,5 +0,0 @@
## v3.0.0 - 2024-08-26
### Fixed
* Fix delete action for accompanying periods in draft state
* Fix connection to azure when making an calendar event in chill
* CollectionType js fixes for remove button and adding multiple entries

View File

@@ -138,4 +138,4 @@ release:
- echo "running release_job" - echo "running release_job"
release: release:
tag_name: '$CI_COMMIT_TAG' tag_name: '$CI_COMMIT_TAG'
description: "./.changes/$CI_COMMIT_TAG.md" description: "./.changes/v$CI_COMMIT_TAG.md"

View File

@@ -6,41 +6,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie). and is generated by [Changie](https://github.com/miniscruff/changie).
## v3.0.0 - 2024-08-26
### Fixed
* Fix delete action for accompanying periods in draft state
* Fix connection to azure when making an calendar event in chill
* CollectionType js fixes for remove button and adding multiple entries
## v2.23.0 - 2024-07-23
### Feature
* ([#221](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/221)) [DX] move async-upload-bundle features into chill-bundles
* Add job bundle (module emploi)
* Upgrade import of address list to the last version of compiled addresses of belgian-best-address
* Upgrade CKEditor and refactor configuration with use of typescript
### Fixed
* Fix resolving of centers for an household, which will fix in turn the access control
* Resolved type hinting error in activity list export
## v2.22.2 - 2024-07-03
### Fixed
* Remove scope required for event participation stats
## v2.22.1 - 2024-07-01
### Fixed
* Remove debug word
### DX
* Add a command for reading official address DB from Luxembourg and update chill addresses
## v2.22.0 - 2024-06-25
### Feature
* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module
### Traduction francophone
* Exports sont ajoutés pour la module événement.
## v2.21.0 - 2024-06-18 ## v2.21.0 - 2024-06-18
### Feature ### Feature
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period * Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period

View File

@@ -115,8 +115,6 @@
"Chill\\DocGeneratorBundle\\": "src/Bundle/ChillDocGeneratorBundle", "Chill\\DocGeneratorBundle\\": "src/Bundle/ChillDocGeneratorBundle",
"Chill\\DocStoreBundle\\": "src/Bundle/ChillDocStoreBundle", "Chill\\DocStoreBundle\\": "src/Bundle/ChillDocStoreBundle",
"Chill\\EventBundle\\": "src/Bundle/ChillEventBundle", "Chill\\EventBundle\\": "src/Bundle/ChillEventBundle",
"Chill\\FranceTravailApiBundle\\": "src/Bundle/ChillFranceTravailApiBundle/src",
"Chill\\JobBundle\\": "src/Bundle/ChillJobBundle/src",
"Chill\\MainBundle\\": "src/Bundle/ChillMainBundle", "Chill\\MainBundle\\": "src/Bundle/ChillMainBundle",
"Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle", "Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle",
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle", "Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",

View File

@@ -21,7 +21,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
class BirthdateFilter implements ExportElementValidatedInterface, FilterInterface class BirthdateFilter implements ExportElementValidatedInterface, FilterInterface
{ {
// add specific role for this filter // add specific role for this filter
public function addRole(): ?string public function addRole()
{ {
// we do not need any new role for this filter, so we return null // we do not need any new role for this filter, so we return null
return null; return null;

View File

@@ -56,7 +56,7 @@ We strongly encourage you to initialize a git repository at this step, to track
cat <<< "$(jq '.extra.symfony += {"endpoint": ["flex://defaults", "https://gitlab.com/api/v4/projects/57371968/repository/files/index.json/raw?ref=main"]}' composer.json)" > composer.json cat <<< "$(jq '.extra.symfony += {"endpoint": ["flex://defaults", "https://gitlab.com/api/v4/projects/57371968/repository/files/index.json/raw?ref=main"]}' composer.json)" > composer.json
# install chill and some dependencies # install chill and some dependencies
# TODO fix the suffix "alpha1" and replace by ^3.0.0 when version 3.0.0 will be released # TODO fix the suffix "alpha1" and replace by ^3.0.0 when version 3.0.0 will be released
symfony composer require chill-project/chill-bundles v3.0.0-RC3 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev symfony composer require chill-project/chill-bundles v3.0.0-alpha1 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev
We encourage you to accept the inclusion of the "Docker configuration from recipes": this is the documented way to run the database. We encourage you to accept the inclusion of the "Docker configuration from recipes": this is the documented way to run the database.
You must also accept to configure recipes from the contrib repository, unless you want to configure the bundles manually). You must also accept to configure recipes from the contrib repository, unless you want to configure the bundles manually).
@@ -110,14 +110,15 @@ you can either:
.. code-block:: env .. code-block:: env
ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm
# note: if you copy-paste the line above, the password will be "admin".
- add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env, - add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env,
not the password in clear text). not the password in clear text).
- set up the jwt authentication bundle - set up the jwt authentication bundle
Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. You must also run the command
:code:`symfony console lexik:jwt:generate-keypair` to generate some keys that will be stored in the paths set up in the :code:`JWT_SECRET_KEY`
and the :code:`JWT_PUBLIC_KEY` env variables. This is only required for using the stored documents in Chill.
Prepare migrations and other tools Prepare migrations and other tools
********************************** **********************************
@@ -135,8 +136,6 @@ To continue the installation process, you will have to run migrations:
symfony console messenger:setup-transports symfony console messenger:setup-transports
# prepare some views # prepare some views
symfony console chill:db:sync-views symfony console chill:db:sync-views
# generate jwt token, required for some api features (webdav access, ...)
symfony console lexik:jwt:generate-keypair
.. warning:: .. warning::

View File

@@ -27,7 +27,7 @@
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"postcss-loader": "^7.0.2", "postcss-loader": "^7.0.2",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"sass-loader": "^14.0.0", "sass-loader": "^13.0.0",
"select2": "^4.0.13", "select2": "^4.0.13",
"select2-bootstrap-theme": "0.1.0-beta.10", "select2-bootstrap-theme": "0.1.0-beta.10",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
@@ -53,13 +53,12 @@
"marked": "^12.0.2", "marked": "^12.0.2",
"masonry-layout": "^4.2.2", "masonry-layout": "^4.2.2",
"mime": "^4.0.0", "mime": "^4.0.0",
"pdfjs-dist": "^4.3.136",
"swagger-ui": "^4.15.5", "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",
"vue-multiselect": "3.0.0-alpha.2", "vue-multiselect": "3.0.0-alpha.2",
"vue-toast-notification": "^3.1.2", "vue-toast-notification": "^2.0",
"vuex": "^4.0.0" "vuex": "^4.0.0"
}, },
"browserslist": [ "browserslist": [

View File

@@ -1,29 +1,34 @@
parameters: parameters:
ignoreErrors: ignoreErrors:
-
message: "#^Foreach overwrites \\$key with its key variable\\.$#"
count: 1
path: src/Bundle/ChillCustomFieldsBundle/Controller/CustomFieldsGroupController.php
- -
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#" message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1 count: 1
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomField.php path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomField.php
- -
message: "#^Property Chill\\\\CustomFieldsBundle\\\\Entity\\\\CustomField\\:\\:\\$required \\(false\\) does not accept bool\\.$#" message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1 count: 1
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomField.php path: src/Bundle/ChillPersonBundle/Form/PersonType.php
- -
message: "#^Parameter \\#1 \\$user of method Chill\\\\DocStoreBundle\\\\Entity\\\\Document\\:\\:setUser\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User\\|null, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#" message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 2 count: 1
path: src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseController.php path: src/Bundle/ChillMainBundle/Templating/ChillTwigRoutingHelper.php
- -
message: "#^Parameter \\#1 \\$user of method Chill\\\\DocStoreBundle\\\\Entity\\\\Document\\:\\:setUser\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User\\|null, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#" message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1
path: src/Bundle/ChillCustomFieldsBundle/Entity/CustomFieldsGroup.php
-
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 2 count: 2
path: src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php path: src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
-
message: "#^Foreach overwrites \\$key with its key variable\\.$#"
count: 1
path: src/Bundle/ChillCustomFieldsBundle/Controller/CustomFieldsGroupController.php
- -
message: "#^Variable \\$participation might not be defined\\.$#" message: "#^Variable \\$participation might not be defined\\.$#"
@@ -35,106 +40,6 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php path: src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php
-
message: "#^Comparison operation \"\\>\" between \\(bool\\|int\\|Redis\\) and 0 results in an error\\.$#"
count: 1
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/ApiWrapper.php
-
message: "#^Variable \\$response might not be defined\\.$#"
count: 1
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/ApiWrapper.php
-
message: "#^Function GuzzleHttp\\\\Psr7\\\\get not found\\.$#"
count: 1
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
-
message: "#^Function GuzzleHttp\\\\Psr7\\\\str not found\\.$#"
count: 2
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
-
message: "#^Parameter \\#1 \\$seconds of function sleep expects int, string given\\.$#"
count: 1
path: src/Bundle/ChillFranceTravailApiBundle/src/ApiHelper/PartenaireRomeAppellation.php
-
message: "#^Unreachable statement \\- code above always terminates\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Controller/CSPersonController.php
-
message: "#^Parameter \\#1 \\$interval of method DateTimeImmutable\\:\\:add\\(\\) expects DateInterval, string\\|null given\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Entity/Immersion.php
-
message: "#^Parameter \\#1 \\$object of static method DateTimeImmutable\\:\\:createFromMutable\\(\\) expects DateTime, DateTimeInterface given\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Entity/Immersion.php
-
message: "#^Property Chill\\\\JobBundle\\\\Entity\\\\Rome\\\\Metier\\:\\:\\$appellations is never read, only written\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Entity/Rome/Metier.php
-
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListCSPerson\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Export/ListCSPerson.php
-
message: "#^Variable \\$f might not be defined\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Export/ListCSPerson.php
-
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListFrein\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Export/ListFrein.php
-
message: "#^Method Chill\\\\JobBundle\\\\Export\\\\ListProjetProfessionnel\\:\\:splitArrayToColumns\\(\\) never returns Closure so it can be removed from the return type\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Export/ListProjetProfessionnel.php
-
message: "#^Property Chill\\\\JobBundle\\\\Form\\\\ChoiceLoader\\\\RomeAppellationChoiceLoader\\:\\:\\$appellationRepository \\(Chill\\\\JobBundle\\\\Repository\\\\Rome\\\\AppellationRepository\\) does not accept Doctrine\\\\ORM\\\\EntityRepository\\<Chill\\\\JobBundle\\\\Entity\\\\Rome\\\\Appellation\\>\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
-
message: "#^Result of && is always false\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
-
message: "#^Strict comparison using \\=\\=\\= between array\\{\\} and Symfony\\\\Component\\\\Validator\\\\ConstraintViolationListInterface will always evaluate to false\\.$#"
count: 2
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
-
message: "#^Strict comparison using \\=\\=\\= between null and string will always evaluate to false\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
-
message: "#^Variable \\$metier might not be defined\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Form/ChoiceLoader/RomeAppellationChoiceLoader.php
-
message: "#^Parameter \\#1 \\$interval of method DateTimeImmutable\\:\\:add\\(\\) expects DateInterval, string\\|null given\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Security/Authorization/CSConnectesVoter.php
-
message: "#^Parameter \\#1 \\$object of static method DateTimeImmutable\\:\\:createFromMutable\\(\\) expects DateTime, DateTimeInterface given\\.$#"
count: 1
path: src/Bundle/ChillJobBundle/src/Security/Authorization/CSConnectesVoter.php
- -
message: "#^Cannot unset offset '_token' on array\\{formatter\\: mixed, export\\: mixed, centers\\: mixed, alias\\: string\\}\\.$#" message: "#^Cannot unset offset '_token' on array\\{formatter\\: mixed, export\\: mixed, centers\\: mixed, alias\\: string\\}\\.$#"
count: 1 count: 1
@@ -160,31 +65,11 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillMainBundle/Form/ChoiceLoader/PostalCodeChoiceLoader.php path: src/Bundle/ChillMainBundle/Form/ChoiceLoader/PostalCodeChoiceLoader.php
-
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 2
path: src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
-
message: "#^Parameter \\#1 \\$user of method Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\:\\:userHasAccessForCenter\\(\\) expects Chill\\\\MainBundle\\\\Entity\\\\User, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface given\\.$#"
count: 1
path: src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php
-
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1
path: src/Bundle/ChillMainBundle/Templating/ChillTwigRoutingHelper.php
- -
message: "#^Foreach overwrites \\$value with its value variable\\.$#" message: "#^Foreach overwrites \\$value with its value variable\\.$#"
count: 1 count: 1
path: src/Bundle/ChillPersonBundle/Form/ChoiceLoader/PersonChoiceLoader.php path: src/Bundle/ChillPersonBundle/Form/ChoiceLoader/PersonChoiceLoader.php
-
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Form/PersonType.php
- -
message: "#^Foreach overwrites \\$value with its value variable\\.$#" message: "#^Foreach overwrites \\$value with its value variable\\.$#"
count: 1 count: 1

View File

@@ -28,9 +28,6 @@ return static function (RectorConfig $rectorConfig): void {
// register a single rule // register a single rule
$rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
$rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromPropertyTypeRector::class);
$rectorConfig->rule(Rector\TypeDeclaration\Rector\Class_\MergeDateTimePropertyTypeDeclarationRector::class);
$rectorConfig->rule(Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector::class);
// part of the symfony 54 rules // part of the symfony 54 rules
$rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class); $rectorConfig->rule(\Rector\Symfony\Symfony53\Rector\StaticPropertyFetch\KernelTestCaseContainerPropertyDeprecationRector::class);
@@ -39,14 +36,14 @@ return static function (RectorConfig $rectorConfig): void {
//define sets of rules //define sets of rules
$rectorConfig->sets([ $rectorConfig->sets([
LevelSetList::UP_TO_PHP_82, \Rector\Symfony\Set\SymfonySetList::SYMFONY_50,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_40, \Rector\Symfony\Set\SymfonySetList::SYMFONY_50_TYPES,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_41, \Rector\Symfony\Set\SymfonySetList::SYMFONY_51,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_42, \Rector\Symfony\Set\SymfonySetList::SYMFONY_52,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_43, \Rector\Symfony\Set\SymfonySetList::SYMFONY_53,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_44, \Rector\Symfony\Set\SymfonySetList::SYMFONY_54,
\Rector\Doctrine\Set\DoctrineSetList::DOCTRINE_CODE_QUALITY, \Rector\Doctrine\Set\DoctrineSetList::DOCTRINE_CODE_QUALITY,
\Rector\PHPUnit\Set\PHPUnitSetList::PHPUNIT_90, \Rector\Doctrine\Set\DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
]); ]);
$rectorConfig->ruleWithConfiguration(\Rector\Php80\Rector\Class_\AnnotationToAttributeRector::class, [ $rectorConfig->ruleWithConfiguration(\Rector\Php80\Rector\Class_\AnnotationToAttributeRector::class, [
@@ -69,8 +66,9 @@ return static function (RectorConfig $rectorConfig): void {
// skip some path... // skip some path...
$rectorConfig->skip([ $rectorConfig->skip([
// waiting for fixing this bug: https://github.com/rectorphp/rector-doctrine/issues/342 // we must adapt service definition
\Rector\Doctrine\CodeQuality\Rector\Property\ImproveDoctrineCollectionDocTypeInEntityRector::class, \Rector\Symfony\Symfony28\Rector\MethodCall\GetToConstructorInjectionRector::class,
\Rector\Symfony\Symfony34\Rector\Closure\ContainerGetNameToTypeInTestsRector::class,
]); ]);
$rectorConfig->ruleWithConfiguration(AnnotationToAttributeRector::class, [ $rectorConfig->ruleWithConfiguration(AnnotationToAttributeRector::class, [

View File

@@ -99,10 +99,10 @@ final class ActivityController extends AbstractController
$form = $this->createDeleteForm($activity->getId(), $person, $accompanyingPeriod); $form = $this->createDeleteForm($activity->getId(), $person, $accompanyingPeriod);
if (Request::METHOD_POST === $request->getMethod()) { if (Request::METHOD_DELETE === $request->getMethod()) {
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isValid()) {
$this->logger->notice('An activity has been removed', [ $this->logger->notice('An activity has been removed', [
'by_user' => $this->getUser()->getUsername(), 'by_user' => $this->getUser()->getUsername(),
'activity_id' => $activity->getId(), 'activity_id' => $activity->getId(),
@@ -640,6 +640,7 @@ final class ActivityController extends AbstractController
return $this->createFormBuilder() return $this->createFormBuilder()
->setAction($this->generateUrl('chill_activity_activity_delete', $params)) ->setAction($this->generateUrl('chill_activity_activity_delete', $params))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -80,7 +80,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
private \DateTime $date; private \DateTime $date;
/** /**
* @var Collection<int, StoredObject> * @var Collection<StoredObject>
*/ */
#[Assert\Valid(traverse: true)] #[Assert\Valid(traverse: true)]
#[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist'])] #[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist'])]
@@ -107,7 +107,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
private ?Person $person = null; private ?Person $person = null;
/** /**
* @var Collection<int, \Chill\PersonBundle\Entity\Person> * @var Collection<Person>
*/ */
#[Groups(['read', 'docgen:read'])] #[Groups(['read', 'docgen:read'])]
#[ORM\ManyToMany(targetEntity: Person::class)] #[ORM\ManyToMany(targetEntity: Person::class)]
@@ -117,7 +117,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
private PrivateCommentEmbeddable $privateComment; private PrivateCommentEmbeddable $privateComment;
/** /**
* @var Collection<int, ActivityReason> * @var Collection<ActivityReason>
*/ */
#[Groups(['docgen:read'])] #[Groups(['docgen:read'])]
#[ORM\ManyToMany(targetEntity: ActivityReason::class)] #[ORM\ManyToMany(targetEntity: ActivityReason::class)]
@@ -132,7 +132,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
private string $sentReceived = ''; private string $sentReceived = '';
/** /**
* @var Collection<int, \Chill\PersonBundle\Entity\SocialWork\SocialAction> * @var Collection<SocialAction>
*/ */
#[Groups(['read', 'docgen:read'])] #[Groups(['read', 'docgen:read'])]
#[ORM\ManyToMany(targetEntity: SocialAction::class)] #[ORM\ManyToMany(targetEntity: SocialAction::class)]
@@ -140,7 +140,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
private Collection $socialActions; private Collection $socialActions;
/** /**
* @var Collection<int, SocialIssue> * @var Collection<SocialIssue>
*/ */
#[Groups(['read', 'docgen:read'])] #[Groups(['read', 'docgen:read'])]
#[ORM\ManyToMany(targetEntity: SocialIssue::class)] #[ORM\ManyToMany(targetEntity: SocialIssue::class)]
@@ -148,7 +148,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
private Collection $socialIssues; private Collection $socialIssues;
/** /**
* @var Collection<int, ThirdParty> * @var Collection<ThirdParty>
*/ */
#[Groups(['read', 'docgen:read'])] #[Groups(['read', 'docgen:read'])]
#[ORM\ManyToMany(targetEntity: ThirdParty::class)] #[ORM\ManyToMany(targetEntity: ThirdParty::class)]
@@ -162,7 +162,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
private ?User $user = null; private ?User $user = null;
/** /**
* @var Collection<int, User> * @var Collection<User>
*/ */
#[Groups(['read', 'docgen:read'])] #[Groups(['read', 'docgen:read'])]
#[ORM\ManyToMany(targetEntity: User::class)] #[ORM\ManyToMany(targetEntity: User::class)]

View File

@@ -79,9 +79,11 @@ class ActivityReason
/** /**
* Set active. * Set active.
* *
* @param bool $active
*
* @return ActivityReason * @return ActivityReason
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;
@@ -108,9 +110,11 @@ class ActivityReason
/** /**
* Set name. * Set name.
* *
* @param array $name
*
* @return ActivityReason * @return ActivityReason
*/ */
public function setName(array $name) public function setName($name)
{ {
$this->name = $name; $this->name = $name;

View File

@@ -40,9 +40,9 @@ class ActivityReasonCategory implements \Stringable
/** /**
* Array of ActivityReason. * Array of ActivityReason.
* *
* @var Collection<int, ActivityReason> * @var Collection<ActivityReason>
*/ */
#[ORM\OneToMany(mappedBy: 'category', targetEntity: ActivityReason::class)] #[ORM\OneToMany(targetEntity: ActivityReason::class, mappedBy: 'category')]
private Collection $reasons; private Collection $reasons;
/** /**

View File

@@ -152,7 +152,7 @@ class ListActivityHelper
return ''; return '';
} }
return $this->translator->trans((string) $value); return $this->translator->trans($value);
}, },
}; };
} }

View File

@@ -73,7 +73,7 @@ final readonly class PeriodHavingActivityBetweenDatesFilter implements FilterInt
$qb->andWhere( $qb->andWhere(
$qb->expr()->exists( $qb->expr()->exists(
'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = activity.accompanyingPeriod" 'SELECT 1 FROM '.Activity::class." {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = acp"
) )
); );

View File

@@ -12,8 +12,6 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Repository; namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\Activity;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
@@ -25,7 +23,7 @@ use Doctrine\Persistence\ManagerRegistry;
* @method Activity[] findAll() * @method Activity[] findAll()
* @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */
class ActivityRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface class ActivityRepository extends ServiceEntityRepository
{ {
public function __construct(ManagerRegistry $registry) public function __construct(ManagerRegistry $registry)
{ {
@@ -99,16 +97,4 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?Activity
{
$qb = $this->createQueryBuilder('a');
$query = $qb
->leftJoin('a.documents', 'ad')
->where('ad.id = :storedObjectId')
->setParameter('storedObjectId', $storedObject->getId())
->getQuery();
return $query->getOneOrNullResult();
}
} }

View File

@@ -15,9 +15,9 @@ use Chill\ActivityBundle\Entity\ActivityType;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
final readonly class ActivityTypeRepository implements ActivityTypeRepositoryInterface final class ActivityTypeRepository implements ActivityTypeRepositoryInterface
{ {
private EntityRepository $repository; private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em)
{ {

View File

@@ -87,6 +87,7 @@
<li> <li>
{% if bloc.type == 'user' %} {% if bloc.type == 'user' %}
<span class="badge-user"> <span class="badge-user">
hello
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
</span> </span>
{% else %} {% else %}

View File

@@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Security\Authorization;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Symfony\Component\Security\Core\Security;
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
{
public function __construct(
private readonly ActivityRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService
) {
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface
{
return $this->repository;
}
protected function getClass(): string
{
return Activity::class;
}
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
{
return match ($attribute) {
StoredObjectRoleEnum::EDIT => ActivityVoter::UPDATE,
StoredObjectRoleEnum::SEE => ActivityVoter::SEE_DETAILS,
};
}
protected function canBeAssociatedWithWorkflow(): bool
{
return false;
}
}

View File

@@ -22,9 +22,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
class AsideActivityCategory class AsideActivityCategory
{ {
/** /**
* @var Collection<int, AsideActivityCategory> * @var Collection<AsideActivityCategory>
*/ */
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: AsideActivityCategory::class)] #[ORM\OneToMany(targetEntity: AsideActivityCategory::class, mappedBy: 'parent')]
private Collection $children; private Collection $children;
#[ORM\Id] #[ORM\Id]

View File

@@ -54,7 +54,7 @@ abstract class AbstractElementController extends AbstractController
$indexPage = 'chill_budget_elements_household_index'; $indexPage = 'chill_budget_elements_household_index';
} }
if (Request::METHOD_POST === $request->getMethod()) { if (Request::METHOD_DELETE === $request->getMethod()) {
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
@@ -198,9 +198,10 @@ abstract class AbstractElementController extends AbstractController
/** /**
* Creates a form to delete a help request entity by id. * Creates a form to delete a help request entity by id.
*/ */
private function createDeleteForm(): \Symfony\Component\Form\FormInterface private function createDeleteForm(): Form
{ {
return $this->createFormBuilder() return $this->createFormBuilder()
->setMethod(Request::METHOD_DELETE)
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -100,7 +100,7 @@ class Charge extends AbstractElement implements HasCentersInterface
return $this; return $this;
} }
public function setHelp(?string $help) public function setHelp($help)
{ {
$this->help = $help; $this->help = $help;

View File

@@ -15,9 +15,9 @@ use Chill\BudgetBundle\Entity\ChargeKind;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
final readonly class ChargeKindRepository implements ChargeKindRepositoryInterface final class ChargeKindRepository implements ChargeKindRepositoryInterface
{ {
private EntityRepository $repository; private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {

View File

@@ -15,9 +15,9 @@ use Chill\BudgetBundle\Entity\ResourceKind;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
final readonly class ResourceKindRepository implements ResourceKindRepositoryInterface final class ResourceKindRepository implements ResourceKindRepositoryInterface
{ {
private EntityRepository $repository; private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {

View File

@@ -84,7 +84,7 @@ class CalendarController extends AbstractController
$form = $this->createDeleteForm($entity); $form = $this->createDeleteForm($entity);
if (Request::METHOD_POST === $request->getMethod()) { if (Request::METHOD_DELETE === $request->getMethod()) {
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isValid()) { if ($form->isValid()) {
@@ -512,6 +512,7 @@ class CalendarController extends AbstractController
{ {
return $this->createFormBuilder() return $this->createFormBuilder()
->setAction($this->generateUrl('chill_calendar_calendar_delete', ['id' => $calendar->getId()])) ->setAction($this->generateUrl('chill_calendar_calendar_delete', ['id' => $calendar->getId()]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -103,7 +103,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
private int $dateTimeVersion = 0; private int $dateTimeVersion = 0;
/** /**
* @var Collection<int, \Chill\CalendarBundle\Entity\CalendarDoc> * @var Collection<CalendarDoc>
*/ */
#[ORM\OneToMany(mappedBy: 'calendar', targetEntity: CalendarDoc::class, orphanRemoval: true)] #[ORM\OneToMany(mappedBy: 'calendar', targetEntity: CalendarDoc::class, orphanRemoval: true)]
private Collection $documents; private Collection $documents;
@@ -120,7 +120,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
private ?int $id = null; private ?int $id = null;
/** /**
* @var \Doctrine\Common\Collections\Collection<int, \Chill\CalendarBundle\Entity\Invite>&Selectable * @var Collection&Selectable<int, Invite>
*/ */
#[Serializer\Groups(['read', 'docgen:read'])] #[Serializer\Groups(['read', 'docgen:read'])]
#[ORM\OneToMany(mappedBy: 'calendar', targetEntity: Invite::class, cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)] #[ORM\OneToMany(mappedBy: 'calendar', targetEntity: Invite::class, cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)]
@@ -143,7 +143,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
private ?Person $person = null; private ?Person $person = null;
/** /**
* @var Collection<int, Person> * @var Collection<Person>
*/ */
#[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])] #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
#[Assert\Count(min: 1, minMessage: 'calendar.At least {{ limit }} person is required.')] #[Assert\Count(min: 1, minMessage: 'calendar.At least {{ limit }} person is required.')]
@@ -157,7 +157,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
private PrivateCommentEmbeddable $privateComment; private PrivateCommentEmbeddable $privateComment;
/** /**
* @var Collection<int, ThirdParty> * @var Collection<ThirdParty>
*/ */
#[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])] #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])]
#[ORM\ManyToMany(targetEntity: ThirdParty::class)] #[ORM\ManyToMany(targetEntity: ThirdParty::class)]

View File

@@ -47,7 +47,7 @@ final class CalendarContextTest extends TestCase
{ {
$expected = $expected =
[ [
'trackDatetime' => true, 'track_datetime' => true,
'askMainPerson' => true, 'askMainPerson' => true,
'mainPersonLabel' => 'docgen.calendar.Destinee', 'mainPersonLabel' => 'docgen.calendar.Destinee',
'askThirdParty' => false, 'askThirdParty' => false,
@@ -61,7 +61,7 @@ final class CalendarContextTest extends TestCase
{ {
$expected = $expected =
[ [
'trackDatetime' => true, 'track_datetime' => true,
'askMainPerson' => true, 'askMainPerson' => true,
'mainPersonLabel' => 'docgen.calendar.Destinee', 'mainPersonLabel' => 'docgen.calendar.Destinee',
'askThirdParty' => false, 'askThirdParty' => false,

View File

@@ -172,9 +172,11 @@ class CustomField
/** /**
* Set active. * Set active.
* *
* @param bool $active
*
* @return CustomField * @return CustomField
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;
@@ -222,16 +224,18 @@ class CustomField
/** /**
* Set order. * Set order.
* *
* @param float $order
*
* @return CustomField * @return CustomField
*/ */
public function setOrdering(?float $order) public function setOrdering($order)
{ {
$this->ordering = $order; $this->ordering = $order;
return $this; return $this;
} }
public function setRequired(bool $required) public function setRequired($required)
{ {
$this->required = $required; $this->required = $required;
@@ -241,7 +245,7 @@ class CustomField
/** /**
* @return $this * @return $this
*/ */
public function setSlug(?string $slug) public function setSlug($slug)
{ {
$this->slug = $slug; $this->slug = $slug;
@@ -251,9 +255,11 @@ class CustomField
/** /**
* Set type. * Set type.
* *
* @param string $type
*
* @return CustomField * @return CustomField
*/ */
public function setType(?string $type) public function setType($type)
{ {
$this->type = $type; $this->type = $type;

View File

@@ -23,9 +23,9 @@ class Option
private bool $active = true; private bool $active = true;
/** /**
* @var Collection<int, Option> * @var Collection<Option>
*/ */
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: Option::class)] #[ORM\OneToMany(targetEntity: Option::class, mappedBy: 'parent')]
private Collection $children; private Collection $children;
#[ORM\Id] #[ORM\Id]
@@ -129,7 +129,7 @@ class Option
/** /**
* @return $this * @return $this
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;
@@ -139,7 +139,7 @@ class Option
/** /**
* @return $this * @return $this
*/ */
public function setInternalKey(string $internal_key) public function setInternalKey($internal_key)
{ {
$this->internalKey = $internal_key; $this->internalKey = $internal_key;
@@ -149,7 +149,7 @@ class Option
/** /**
* @return $this * @return $this
*/ */
public function setKey(?string $key) public function setKey($key)
{ {
$this->key = $key; $this->key = $key;

View File

@@ -69,7 +69,7 @@ class CustomFieldsDefaultGroup
* *
* @return CustomFieldsDefaultGroup * @return CustomFieldsDefaultGroup
*/ */
public function setCustomFieldsGroup(?CustomFieldsGroup $customFieldsGroup) public function setCustomFieldsGroup($customFieldsGroup)
{ {
$this->customFieldsGroup = $customFieldsGroup; $this->customFieldsGroup = $customFieldsGroup;
@@ -79,9 +79,11 @@ class CustomFieldsDefaultGroup
/** /**
* Set entity. * Set entity.
* *
* @param string $entity
*
* @return CustomFieldsDefaultGroup * @return CustomFieldsDefaultGroup
*/ */
public function setEntity(?string $entity) public function setEntity($entity)
{ {
$this->entity = $entity; $this->entity = $entity;

View File

@@ -32,9 +32,9 @@ class CustomFieldsGroup
* The custom fields of the group. * The custom fields of the group.
* The custom fields are asc-ordered regarding to their property "ordering". * The custom fields are asc-ordered regarding to their property "ordering".
* *
* @var Collection<int, CustomField> * @var Collection<CustomField>
*/ */
#[ORM\OneToMany(mappedBy: 'customFieldGroup', targetEntity: CustomField::class)] #[ORM\OneToMany(targetEntity: CustomField::class, mappedBy: 'customFieldGroup')]
#[ORM\OrderBy(['ordering' => \Doctrine\Common\Collections\Criteria::ASC])] #[ORM\OrderBy(['ordering' => \Doctrine\Common\Collections\Criteria::ASC])]
private Collection $customFields; private Collection $customFields;
@@ -165,9 +165,11 @@ class CustomFieldsGroup
/** /**
* Set entity. * Set entity.
* *
* @param string $entity
*
* @return CustomFieldsGroup * @return CustomFieldsGroup
*/ */
public function setEntity(?string $entity) public function setEntity($entity)
{ {
$this->entity = $entity; $this->entity = $entity;

View File

@@ -21,14 +21,14 @@ use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
final readonly class RelatorioDriver implements DriverInterface final class RelatorioDriver implements DriverInterface
{ {
private string $url; private readonly string $url;
public function __construct( public function __construct(
private HttpClientInterface $client, private readonly HttpClientInterface $client,
ParameterBagInterface $parameterBag, ParameterBagInterface $parameterBag,
private LoggerInterface $logger private readonly LoggerInterface $logger
) { ) {
$this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url']; $this->url = $parameterBag->get('chill_doc_generator')['driver']['relatorio']['url'];
} }

View File

@@ -16,11 +16,11 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
final readonly class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface
{ {
private EntityRepository $repository; private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager, private RequestStack $requestStack) public function __construct(EntityManagerInterface $entityManager, private readonly RequestStack $requestStack)
{ {
$this->repository = $entityManager->getRepository(DocGeneratorTemplate::class); $this->repository = $entityManager->getRepository(DocGeneratorTemplate::class);
} }

View File

@@ -89,7 +89,6 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
$g = new SignedUrlPost( $g = new SignedUrlPost(
$url = $this->generateUrl($object_name), $url = $this->generateUrl($object_name),
$expires, $expires,
$object_name,
$this->max_post_file_size, $this->max_post_file_size,
$max_file_count, $max_file_count,
$submit_delay, $submit_delay,
@@ -128,7 +127,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf
]; ];
$url = $url.'?'.\http_build_query($args); $url = $url.'?'.\http_build_query($args);
$signature = new SignedUrl(strtoupper($method), $url, $expires, $object_name); $signature = new SignedUrl(strtoupper($method), $url, $expires);
$this->event_dispatcher->dispatch( $this->event_dispatcher->dispatch(
new TempUrlGenerateEvent($signature) new TempUrlGenerateEvent($signature)

View File

@@ -21,8 +21,6 @@ readonly class SignedUrl
#[Serializer\Groups(['read'])] #[Serializer\Groups(['read'])]
public string $url, public string $url,
public \DateTimeImmutable $expires, public \DateTimeImmutable $expires,
#[Serializer\Groups(['read'])]
public string $object_name,
) {} ) {}
#[Serializer\Groups(['read'])] #[Serializer\Groups(['read'])]

View File

@@ -18,7 +18,6 @@ readonly class SignedUrlPost extends SignedUrl
public function __construct( public function __construct(
string $url, string $url,
\DateTimeImmutable $expires, \DateTimeImmutable $expires,
string $object_name,
#[Serializer\Groups(['read'])] #[Serializer\Groups(['read'])]
public int $max_file_size, public int $max_file_size,
#[Serializer\Groups(['read'])] #[Serializer\Groups(['read'])]
@@ -32,6 +31,6 @@ readonly class SignedUrlPost extends SignedUrl
#[Serializer\Groups(['read'])] #[Serializer\Groups(['read'])]
public string $signature, public string $signature,
) { ) {
parent::__construct('POST', $url, $expires, $object_name); parent::__construct('POST', $url, $expires);
} }
} }

View File

@@ -26,8 +26,6 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
/** /**
* Class DocumentPersonController. * Class DocumentPersonController.
@@ -42,8 +40,6 @@ class DocumentPersonController extends AbstractController
protected TranslatorInterface $translator, protected TranslatorInterface $translator,
protected EventDispatcherInterface $eventDispatcher, protected EventDispatcherInterface $eventDispatcher,
protected AuthorizationHelper $authorizationHelper, protected AuthorizationHelper $authorizationHelper,
protected PDFSignatureZoneParser $PDFSignatureZoneParser,
protected StoredObjectManagerInterface $storedObjectManagerInterface,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
) {} ) {}
@@ -201,36 +197,4 @@ class DocumentPersonController extends AbstractController
['document' => $document, 'person' => $person] ['document' => $document, 'person' => $person]
); );
} }
#[Route(path: '/{id}/signature', name: 'person_document_signature', methods: 'GET')]
public function signature(Person $person, PersonDocument $document): Response
{
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
$this->denyAccessUnlessGranted('CHILL_PERSON_DOCUMENT_SEE', $document);
$event = new PrivacyEvent($person, [
'element_class' => PersonDocument::class,
'element_id' => $document->getId(),
'action' => 'show',
]);
$this->eventDispatcher->dispatch($event, PrivacyEvent::PERSON_PRIVACY_EVENT);
$storedObject = $document->getObject();
$content = $this->storedObjectManagerInterface->read($storedObject);
$zones = $this->PDFSignatureZoneParser->findSignatureZones($content);
$signature = [];
$signature['id'] = 1;
$signature['storedObject'] = [ // TEMP
'filename' => $storedObject->getFilename(),
'iv' => $storedObject->getIv(),
'keyInfos' => $storedObject->getKeyInfos(),
];
$signature['zones'] = $zones;
return $this->render(
'@ChillDocStore/PersonDocument/signature.html.twig',
['document' => $document, 'person' => $person, 'signature' => $signature]
);
}
} }

View File

@@ -11,57 +11,36 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Controller; namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage; use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
use Chill\DocStoreBundle\Service\Signature\PDFPage; use Chill\DocStoreBundle\Service\Signature\PDFPage;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone; use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; use Symfony\Component\HttpFoundation\Response;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
class SignatureRequestController class SignatureRequestController
{ {
public function __construct( public function __construct(
private readonly MessageBusInterface $messageBus, private MessageBusInterface $messageBus,
private readonly StoredObjectManagerInterface $storedObjectManager, private StoredObjectManagerInterface $storedObjectManager,
private readonly EntityWorkflowManager $entityWorkflowManager,
) {} ) {}
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')] #[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse public function processSignature(StoredObject $storedObject): Response
{ {
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
$content = $this->storedObjectManager->read($storedObject); $content = $this->storedObjectManager->read($storedObject);
$data = \json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR); // TODO parse payload: json_decode ou, mieux, dataTransfertObject
$zone = new PDFSignatureZone(
$data['zone']['index'],
$data['zone']['x'],
$data['zone']['y'],
$data['zone']['height'],
$data['zone']['width'],
new PDFPage($data['zone']['PDFPage']['index'], $data['zone']['PDFPage']['width'], $data['zone']['PDFPage']['height'])
);
$this->messageBus->dispatch(new RequestPdfSignMessage( $this->messageBus->dispatch(new RequestPdfSignMessage(
$signature->getId(), 0,
$zone, new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
$data['zone']['index'], 0,
'test signature', // reason (string) 'test signature',
'Mme Caroline Diallo', // signerText (string) 'Mme Caroline Diallo',
$content $content
)); ));
return new JsonResponse(null, JsonResponse::HTTP_OK, []); return new Response('<html><head><title>test</title></head><body><p>ok</p></body></html>');
}
#[Route('/api/1.0/document/workflow/{id}/check-signature', name: 'chill_docstore_check_signature')]
public function checkSignature(EntityWorkflowStepSignature $signature): JsonResponse
{
return new JsonResponse($signature->getState(), JsonResponse::HTTP_OK, []);
} }
} }

View File

@@ -14,7 +14,6 @@ namespace Chill\DocStoreBundle\DependencyInjection;
use Chill\DocStoreBundle\Controller\StoredObjectApiController; 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 Symfony\Component\Config\FileLocator; 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;
@@ -36,8 +35,6 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
$container->setParameter('chill_doc_store', $config); $container->setParameter('chill_doc_store', $config);
$container->registerForAutoconfiguration(StoredObjectVoterInterface::class)->addTag('stored_object_voter');
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml'); $loader->load('services.yaml');
$loader->load('services/controller.yaml'); $loader->load('services/controller.yaml');
@@ -45,7 +42,6 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf
$loader->load('services/fixtures.yaml'); $loader->load('services/fixtures.yaml');
$loader->load('services/form.yaml'); $loader->load('services/form.yaml');
$loader->load('services/templating.yaml'); $loader->load('services/templating.yaml');
$loader->load('services/security.yaml');
} }
public function prepend(ContainerBuilder $container) public function prepend(ContainerBuilder $container)

View File

@@ -129,7 +129,7 @@ class Document implements TrackCreationInterface, TrackUpdateInterface
return $this; return $this;
} }
public function setUser(?\Chill\MainBundle\Entity\User $user): self public function setUser($user): self
{ {
$this->user = $user; $this->user = $user;

View File

@@ -86,7 +86,7 @@ class DocumentCategory
return $this; return $this;
} }
public function setDocumentClass(?string $documentClass): self public function setDocumentClass($documentClass): self
{ {
$this->documentClass = $documentClass; $this->documentClass = $documentClass;

View File

@@ -55,14 +55,14 @@ class PersonDocument extends Document implements HasCenterInterface, HasScopeInt
return $this->scope; return $this->scope;
} }
public function setPerson(Person $person): self public function setPerson($person): self
{ {
$this->person = $person; $this->person = $person;
return $this; return $this;
} }
public function setScope(?Scope $scope): self public function setScope($scope): self
{ {
$this->scope = $scope; $this->scope = $scope;

View File

@@ -12,14 +12,13 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Repository; namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
class AccompanyingCourseDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface class AccompanyingCourseDocumentRepository implements ObjectRepository
{ {
private readonly EntityRepository $repository; private readonly EntityRepository $repository;
@@ -46,16 +45,6 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat
return $qb->getQuery()->getSingleScalarResult(); return $qb->getQuery()->getSingleScalarResult();
} }
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
{
$qb = $this->repository->createQueryBuilder('d');
$query = $qb->where('d.object = :storedObject')
->setParameter('storedObject', $storedObject)
->getQuery();
return $query->getOneOrNullResult();
}
public function find($id): ?AccompanyingCourseDocument public function find($id): ?AccompanyingCourseDocument
{ {
return $this->repository->find($id); return $this->repository->find($id);
@@ -66,7 +55,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat
return $this->repository->findAll(); return $this->repository->findAll();
} }
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
{ {
return $this->repository->findBy($criteria, $orderBy, $limit, $offset); return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
} }
@@ -76,7 +65,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat
return $this->findOneBy($criteria); return $this->findOneBy($criteria);
} }
public function getClassName(): string public function getClassName()
{ {
return AccompanyingCourseDocument::class; return AccompanyingCourseDocument::class;
} }

View File

@@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\StoredObject;
interface AssociatedEntityToStoredObjectInterface
{
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object;
}

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Repository; namespace Chill\DocStoreBundle\Repository;
use Chill\DocStoreBundle\Entity\PersonDocument; use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
@@ -20,7 +19,7 @@ use Doctrine\Persistence\ObjectRepository;
/** /**
* @template ObjectRepository<PersonDocument::class> * @template ObjectRepository<PersonDocument::class>
*/ */
readonly class PersonDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface readonly class PersonDocumentRepository implements ObjectRepository
{ {
private EntityRepository $repository; private EntityRepository $repository;
@@ -54,14 +53,4 @@ readonly class PersonDocumentRepository implements ObjectRepository, AssociatedE
{ {
return PersonDocument::class; return PersonDocument::class;
} }
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
{
$qb = $this->repository->createQueryBuilder('d');
$query = $qb->where('d.object = :storedObject')
->setParameter('storedObject', $storedObject)
->getQuery();
return $query->getOneOrNullResult();
}
} }

View File

@@ -62,26 +62,3 @@ export interface PostStoreObjectSignature {
signature: string, signature: string,
} }
export interface PDFPage {
index: number,
width: number,
height: number,
}
export interface SignatureZone {
index: number | null,
x: number,
y: number,
width: number,
height: number,
PDFPage: PDFPage,
}
export interface Signature {
id: number,
storedObject: StoredObject,
zones: SignatureZone[],
}
export type SignedState = 'pending' | 'signed' | 'rejected' | 'canceled' | 'error';
export type CanvasEvent = 'select' | 'add';

View File

@@ -1,559 +0,0 @@
<template>
<teleport to="body">
<modal v-if="modalOpen" @close="modalOpen = false">
<template v-slot:header>
<h2>{{ $t("signature_confirmation") }}</h2>
</template>
<template v-slot:body>
<div class="signature-modal-body text-center" v-if="loading">
<p>{{ $t("electronic_signature_in_progress") }}</p>
<div class="loading">
<i
class="fa fa-circle-o-notch fa-spin fa-3x"
:title="$t('loading')"
></i>
</div>
</div>
<div class="signature-modal-body text-center" v-else>
<p>{{ $t("you_are_going_to_sign") }}</p>
<p>{{ $t("are_you_sure") }}</p>
</div>
</template>
<template v-slot:footer>
<button class="btn btn-action" @click.prevent="confirmSign">
{{ $t("yes") }}
</button>
</template>
</modal>
</teleport>
<div class="col-12">
<div
class="row justify-content-center mb-2"
v-if="signature.zones.length > 1"
>
<div class="col-4 gap-2 d-grid">
<button
:disabled="userSignatureZone === null || userSignatureZone?.index < 1"
class="btn btn-light btn-sm"
@click="turnSignature(-1)"
>
{{ $t("last_sign_zone") }}
</button>
</div>
<div class="col-4 gap-2 d-grid">
<button
:disabled="userSignatureZone?.index >= signature.zones.length - 1"
class="btn btn-light btn-sm"
@click="turnSignature(1)"
>
{{ $t("next_sign_zone") }}
</button>
</div>
</div>
<div
id="turn-page"
class="row justify-content-center mb-2"
v-if="pageCount > 1"
>
<div class="col-6-sm col-3-md text-center">
<button
class="btn btn-light btn-sm"
:disabled="page <= 1"
@click="turnPage(-1)"
>
</button>
<span>page {{ page }} / {{ pageCount }}</span>
<button
class="btn btn-light btn-sm"
:disabled="page >= pageCount"
@click="turnPage(1)"
>
</button>
</div>
</div>
</div>
<div v-if="multiPage" class="col-12 text-center">
<canvas
v-for="p in pageCount"
:key="p"
class="m-auto"
:id="`canvas-${p}`"
></canvas>
</div>
<div v-else class="col-12 text-center">
<canvas class="m-auto" :id="canvas"></canvas>
</div>
<div class="col-12 p-4" id="action-buttons" v-if="signedState !== 'signed'">
<div class="row mb-3">
<div class="col-12 d-flex justify-content-end">
<div class="col-4 col-xl-3 gap-2 d-grid">
<button
v-if="adding"
class="btn btn-misc btn-cancel me-2 btn-sm"
@click="removeNewZone()"
>
{{ $t("remove_sign_zone") }}
</button>
</div>
<div class="col-4 gap-2 d-grid">
<button
class="btn btn-create btn-sm"
:class="{ active: canvasEvent === 'add' }"
@click="toggleAddZone()"
>
{{ $t("add_sign_zone") }}
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-4">
<button
class="btn btn-action me-2"
:disabled="!userSignatureZone"
@click="sign"
>
{{ $t("sign") }}
</button>
</div>
<div class="col-8 d-flex justify-content-end">
<button
class="btn btn-misc me-2"
:hidden="!userSignatureZone"
@click="undoSign"
v-if="signature.zones.length > 1"
>
{{ $t("choose_another_signature") }}
</button>
<button
class="btn btn-misc me-2"
:hidden="!userSignatureZone"
@click="undoSign"
v-else
>
{{ $t("cancel") }}
</button>
<button class="btn btn-delete" @click="undoSign">
{{ $t("cancel_signing") }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, Ref, reactive } from "vue";
import { useToast } from "vue-toast-notification";
import "vue-toast-notification/dist/theme-sugar.css";
import {
CanvasEvent,
Signature,
SignatureZone,
SignedState,
} from "../../types";
import { makeFetch } from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import * as pdfjsLib from "pdfjs-dist";
import {
PDFDocumentProxy,
PDFPageProxy,
} from "pdfjs-dist/types/src/display/api";
// @ts-ignore
import * as PdfWorker from "pdfjs-dist/build/pdf.worker.mjs";
console.log(PdfWorker); // incredible but this is needed
// import { PdfWorker } from 'pdfjs-dist/build/pdf.worker.mjs'
// pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker;
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import {
build_download_info_link,
download_and_decrypt_doc,
} from "../StoredObjectButton/helpers";
pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs";
const multiPage: Ref<boolean> = ref(true);
const modalOpen: Ref<boolean> = ref(false);
const loading: Ref<boolean> = ref(false);
const adding: Ref<boolean> = ref(false);
const canvasEvent: Ref<CanvasEvent> = ref("select");
const signedState: Ref<SignedState> = ref("pending");
const page: Ref<number> = ref(1);
const pageCount: Ref<number> = ref(0);
let userSignatureZone: Ref<null | SignatureZone> = ref(null);
let pdf = {} as PDFDocumentProxy;
declare global {
interface Window {
signature: Signature;
}
}
const $toast = useToast();
const signature = window.signature;
const urlInfo = build_download_info_link(signature.storedObject.filename);
console.log(signature);
const mountPdf = async (url: string) => {
const loadingTask = pdfjsLib.getDocument(url);
pdf = await loadingTask.promise;
pageCount.value = pdf.numPages;
if (multiPage.value) {
await setAllPages();
} else {
await setPage(1);
}
};
const getRenderContext = (pdfPage: PDFPageProxy) => {
const scale = 1;
const viewport = pdfPage.getViewport({ scale });
let canvas;
if (multiPage.value) {
canvas = document.getElementById(
`canvas-${pdfPage.pageNumber}`
) as HTMLCanvasElement;
} else {
canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
}
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
canvas.height = viewport.height;
canvas.width = viewport.width;
return {
canvasContext: context,
viewport: viewport,
};
};
const setAllPages = async () =>
Array.from(Array(pageCount.value).keys()).map((p) => setPage(p + 1));
const setPage = async (page: number) => {
const pdfPage = await pdf.getPage(page);
const renderContext = getRenderContext(pdfPage);
await pdfPage.render(renderContext);
};
async function downloadAndOpen(): Promise<Blob> {
let raw;
try {
raw = await download_and_decrypt_doc(
urlInfo,
signature.storedObject.keyInfos,
new Uint8Array(signature.storedObject.iv)
);
} catch (e) {
console.error("error while downloading and decrypting document", e);
throw e;
}
await mountPdf(URL.createObjectURL(raw));
initPdf();
return raw;
}
const initPdf = () => {
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
canvas.addEventListener("pointerup", canvasClick, false);
setTimeout(() => addZones(page.value), 800);
};
const scaleXToCanvas = (x: number, canvasWidth: number, PDFWidth: number) =>
Math.round((x * canvasWidth) / PDFWidth);
const scaleYToCanvas = (h: number, canvasHeight: number, PDFHeight: number) =>
Math.round((h * canvasHeight) / PDFHeight);
const hitSignature = (
zone: SignatureZone,
xy: number[],
canvasWidth: number,
canvasHeight: number
) =>
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) < xy[0] &&
xy[0] <
scaleXToCanvas(zone.x + zone.width, canvasWidth, zone.PDFPage.width) &&
zone.PDFPage.height -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) <
xy[1] &&
xy[1] <
scaleYToCanvas(zone.height - zone.y, canvasHeight, zone.PDFPage.height) +
zone.PDFPage.height;
const selectZone = (z: SignatureZone, canvas: HTMLCanvasElement) => {
userSignatureZone.value = z;
const ctx = canvas.getContext("2d");
if (ctx) {
setPage(page.value);
setTimeout(() => drawZone(z, ctx, canvas.width, canvas.height), 200);
}
};
const selectZoneOnCanvas = (e: PointerEvent, canvas: HTMLCanvasElement) =>
signature.zones
.filter((z) => z.PDFPage.index + 1 === page.value)
.map((z) => {
if (
hitSignature(z, [e.offsetX, e.offsetY], canvas.width, canvas.height)
) {
if (userSignatureZone.value === null) {
selectZone(z, canvas);
} else {
if (userSignatureZone.value.index === z.index) {
sign();
}
}
}
});
const canvasClick = (e: PointerEvent) => {
const canvas = document.querySelectorAll("canvas")[0] as HTMLCanvasElement;
canvasEvent.value === "select"
? selectZoneOnCanvas(e, canvas)
: addZoneOnCanvas(e, canvas);
};
const turnPage = async (upOrDown: number) => {
userSignatureZone.value = null;
page.value = page.value + upOrDown;
await setPage(page.value);
setTimeout(() => addZones(page.value), 200);
};
const turnSignature = async (upOrDown: number) => {
let zoneIndex = userSignatureZone.value?.index ?? -1;
if (zoneIndex < -1) {
zoneIndex = -1;
}
if (zoneIndex < signature.zones.length) {
zoneIndex = zoneIndex + upOrDown;
} else {
zoneIndex = 0;
}
let currentZone = signature.zones[zoneIndex];
if (currentZone) {
page.value = currentZone.PDFPage.index + 1;
userSignatureZone.value = currentZone;
const canvas = document.querySelectorAll("canvas")[0];
selectZone(currentZone, canvas);
}
};
const drawZone = (
zone: SignatureZone,
ctx: CanvasRenderingContext2D,
canvasWidth: number,
canvasHeight: number
) => {
const unselectedBlue = "#007bff";
const selectedBlue = "#034286";
ctx.strokeStyle =
userSignatureZone.value?.index === zone.index
? selectedBlue
: unselectedBlue;
ctx.lineWidth = 2;
ctx.lineJoin = "bevel";
ctx.strokeRect(
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width),
zone.PDFPage.height -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height),
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width),
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height)
);
ctx.font = "bold 16px serif";
ctx.textAlign = "center";
ctx.fillStyle = "black";
const xText =
scaleXToCanvas(zone.x, canvasWidth, zone.PDFPage.width) +
scaleXToCanvas(zone.width, canvasWidth, zone.PDFPage.width) / 2;
const yText =
zone.PDFPage.height -
scaleYToCanvas(zone.y, canvasHeight, zone.PDFPage.height) +
scaleYToCanvas(zone.height, canvasHeight, zone.PDFPage.height) / 2;
if (userSignatureZone.value?.index === zone.index) {
ctx.fillStyle = selectedBlue;
ctx.fillText("Signer ici", xText, yText);
} else {
ctx.fillStyle = unselectedBlue;
ctx.fillText("Choisir cette", xText, yText - 12);
ctx.fillText("zone de signature", xText, yText + 12);
// ctx.strokeStyle = "#c6c6c6"; // halo
// ctx.strokeText("Choisir cette", xText, yText - 12);
// ctx.strokeText("zone de signature", xText, yText + 12);
}
};
const addZones = (page: number) => {
const canvas = document.querySelectorAll("canvas")[0];
const ctx = canvas.getContext("2d");
if (ctx) {
signature.zones
.filter((z) => z.PDFPage.index + 1 === page)
.map((z) => drawZone(z, ctx, canvas.width, canvas.height));
}
};
const checkSignature = () => {
const url = `/api/1.0/document/workflow/${signature.id}/check-signature`;
return makeFetch("GET", url)
.then((r) => {
signedState.value = r as SignedState;
checkForReady();
})
.catch((error) => {
signedState.value = "error";
console.log("Error while checking the signature", error);
$toast.error(
`Erreur lors de la vérification de la signature: ${error.txt}`
);
});
};
const maxTryForReady = 60; //2 minutes for trying to sign
let tryForReady = 0;
const stopTrySigning = () => {
loading.value = false;
modalOpen.value = false;
};
const checkForReady = () => {
if (tryForReady > maxTryForReady) {
stopTrySigning();
tryForReady = 0;
console.log("Reached the maximum number of tentative to try signing");
$toast.error(
"Le nombre maximum de tentatives pour essayer de signer est atteint"
);
}
if (signedState.value === "rejected") {
stopTrySigning();
console.log("Signature rejected by the server");
$toast.error("Signature rejetée par le serveur");
}
if (signedState.value === "canceled") {
stopTrySigning();
console.log("Signature canceled");
$toast.error("Signature annulée");
}
if (signedState.value === "pending") {
tryForReady = tryForReady + 1;
setTimeout(() => checkSignature(), 2000);
} else {
stopTrySigning();
if (signedState.value === "signed") {
userSignatureZone.value = null;
downloadAndOpen();
}
}
};
const sign = () => (modalOpen.value = true);
const confirmSign = () => {
loading.value = true;
const url = `/api/1.0/document/workflow/${signature.id}/signature-request`;
const body = {
storedObject: signature.storedObject,
zone: userSignatureZone.value,
};
makeFetch("POST", url, body)
.then((r) => {
checkForReady();
})
.catch((error) => {
console.log("Error while posting the signature", error);
stopTrySigning();
$toast.error(
`Erreur lors de la soumission de la signature: ${error.txt}`
);
});
};
const undoSign = async () => {
signature.zones = signature.zones.filter((z) => z.index !== null);
await setPage(page.value);
setTimeout(() => addZones(page.value), 200);
userSignatureZone.value = null;
adding.value = false;
};
const toggleAddZone = () => {
canvasEvent.value === "select"
? (canvasEvent.value = "add")
: (canvasEvent.value = "select");
};
const addZoneOnCanvas = (e: PointerEvent, canvas: HTMLCanvasElement) => {
const BOX_WIDTH = 180;
const BOX_HEIGHT = 90;
const PDFPageHeight = canvas.height;
const PDFPageWidth = canvas.width;
const x = e.offsetX;
const y = e.offsetY;
const newZone: SignatureZone = {
index: null,
x:
scaleXToCanvas(x, canvas.width, PDFPageWidth) -
scaleXToCanvas(BOX_WIDTH / 2, canvas.width, PDFPageWidth),
y:
PDFPageHeight -
scaleYToCanvas(y, canvas.height, PDFPageHeight) +
scaleYToCanvas(BOX_HEIGHT / 2, canvas.height, PDFPageHeight),
width: BOX_WIDTH,
height: BOX_HEIGHT,
PDFPage: {
index: page.value - 1,
width: PDFPageWidth,
height: PDFPageHeight,
},
};
signature.zones.push(newZone);
setTimeout(() => addZones(page.value), 200);
canvasEvent.value = "select";
adding.value = true;
};
const removeNewZone = async () => {
signature.zones = signature.zones.filter((z) => z.index !== null);
userSignatureZone.value = null;
await setPage(page.value);
setTimeout(() => addZones(page.value), 200);
canvasEvent.value = "select";
adding.value = false;
};
downloadAndOpen();
</script>
<style scoped lang="scss">
canvas {
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.1);
}
div#action-buttons {
position: sticky;
bottom: 0px;
background-color: white;
z-index: 100;
}
div#turn-page {
span {
font-size: 0.8rem;
margin: 0 0.4rem;
}
}
div.signature-modal-body {
height: 8rem;
}
</style>

View File

@@ -1,32 +0,0 @@
import { createApp } from "vue";
// @ts-ignore
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
import App from "./App.vue";
const appMessages = {
fr: {
yes: 'Oui',
are_you_sure: 'Êtes-vous sûr·e?',
you_are_going_to_sign: 'Vous allez signer le document',
signature_confirmation: 'Confirmation de la signature',
sign: 'Signer',
choose_another_signature: 'Choisir une autre zone de signature',
cancel: 'Annuler',
cancel_signing: 'Refuser de signer',
last_sign_zone: 'Zone de signature précédente',
next_sign_zone: 'Zone de signature suivante',
electronic_signature_in_progress: 'Signature électronique en cours...',
loading: 'Chargement...',
add_sign_zone: 'Ajouter une zone de signature',
remove_sign_zone: 'Enlever la zone',
}
}
const i18n = _createI18n(appMessages);
const app = createApp({
template: `<app></app>`,
})
.use(i18n)
.component("app", App)
.mount("#document-signature");

View File

@@ -71,7 +71,7 @@
</li> </li>
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %} {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
<li> <li>
{{ document.object|chill_document_button_group(document.title) }} {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
</li> </li>
<li> <li>
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a> <a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
@@ -90,7 +90,7 @@
{% else %} {% else %}
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %} {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
<li> <li>
{{ document.object|chill_document_button_group(document.title) }} {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
</li> </li>
<li> <li>
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a> <a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>

View File

@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
<title>Signature</title>
{{ encore_entry_link_tags('mod_bootstrap') }}
{{ encore_entry_link_tags('mod_forkawesome') }}
{{ encore_entry_link_tags('chill') }}
{{ encore_entry_link_tags('vue_document_signature') }}
</head>
<body>
{% block js %}
{{ encore_entry_script_tags('mod_document_action_buttons_group') }}
<script type="text/javascript">
window.signature = {{ signature|json_encode|raw }};
</script>
{{ encore_entry_script_tags('vue_document_signature') }}
{% endblock %}
<div class="content" id="content">
<div class="container-xxl">
<div class="row">
<div class="col-xs-12 col-md-12 col-lg-9 my-5 m-auto">
<h4>{{ 'Document %title%' | trans({ '%title%': document.title }) }}</h4>
<div class="row" id="document-signature"></div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -70,12 +70,12 @@ class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements Prov
return []; return [];
} }
public function supports($attribute, $subject): bool protected function supports($attribute, $subject): bool
{ {
return $this->voterHelper->supports($attribute, $subject); return $this->voterHelper->supports($attribute, $subject);
} }
public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{ {
if (!$token->getUser() instanceof User) { if (!$token->getUser() instanceof User) {
return false; return false;

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Security\Authorization; namespace Chill\DocStoreBundle\Security\Authorization;
use Chill\DocStoreBundle\AsyncUpload\SignedUrl; use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
@@ -23,7 +22,6 @@ final class AsyncUploadVoter extends Voter
public function __construct( public function __construct(
private readonly Security $security, private readonly Security $security,
private readonly StoredObjectRepository $storedObjectRepository
) {} ) {}
protected function supports($attribute, $subject): bool protected function supports($attribute, $subject): bool
@@ -34,16 +32,10 @@ final class AsyncUploadVoter extends Voter
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{ {
/** @var SignedUrl $subject */ /** @var SignedUrl $subject */
if (!in_array($subject->method, ['POST', 'GET', 'HEAD', 'PUT'], true)) { if (!in_array($subject->method, ['POST', 'GET', 'HEAD'], true)) {
return false; return false;
} }
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $subject->object_name]); return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN');
return match ($subject->method) {
'GET', 'HEAD' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject),
'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject),
'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')
};
} }
} }

View File

@@ -12,10 +12,9 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Security\Authorization; namespace Chill\DocStoreBundle\Security\Authorization;
use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Entity\StoredObject;
use Psr\Log\LoggerInterface; use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
/** /**
* Voter for the content of a stored object. * Voter for the content of a stored object.
@@ -24,10 +23,6 @@ use Symfony\Component\Security\Core\Security;
*/ */
class StoredObjectVoter extends Voter class StoredObjectVoter extends Voter
{ {
public const LOG_PREFIX = '[stored object voter] ';
public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters, private readonly LoggerInterface $logger) {}
protected function supports($attribute, $subject): bool protected function supports($attribute, $subject): bool
{ {
return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
@@ -37,28 +32,24 @@ class StoredObjectVoter extends Voter
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{ {
/** @var StoredObject $subject */ /** @var StoredObject $subject */
$attributeAsEnum = StoredObjectRoleEnum::from($attribute); if (
!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
// Loop through context-specific voters || $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
foreach ($this->storedObjectVoters as $storedObjectVoter) { ) {
if ($storedObjectVoter->supports($attributeAsEnum, $subject)) {
$grant = $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token);
if (false === $grant) {
$this->logger->debug(self::LOG_PREFIX.'deny access by storedObjectVoter', ['stored_object_voter' => $storedObjectVoter::class]);
}
return $grant;
}
}
// User role-based fallback
if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) {
// TODO: this maybe considered as a security issue, as all authenticated users can reach a stored object which
// is potentially detached from an existing entity.
return true;
}
return false; return false;
} }
if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) {
return false;
}
$askedRole = StoredObjectRoleEnum::from($attribute);
$tokenRoleAuthorization =
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS);
return match ($askedRole) {
StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization,
StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization
};
}
} }

View File

@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
{
abstract protected function getRepository(): AssociatedEntityToStoredObjectInterface;
/**
* @return class-string
*/
abstract protected function getClass(): string;
abstract protected function attributeToRole(StoredObjectRoleEnum $attribute): string;
abstract protected function canBeAssociatedWithWorkflow(): bool;
public function __construct(
private readonly Security $security,
private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
) {}
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
{
$class = $this->getClass();
return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class;
}
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
{
// Retrieve the related accompanying course document
$entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
$voterAttribute = $this->attributeToRole($attribute);
if (false === $this->security->isGranted($voterAttribute, $entity)) {
return false;
}
if (StoredObjectRoleEnum::SEE !== $attribute && $this->canBeAssociatedWithWorkflow()) {
if (null === $this->workflowDocumentService) {
throw new \LogicException('Provide a workflow document service');
}
return $this->workflowDocumentService->notBlockedByWorkflow($entity);
}
return true;
}
}

View File

@@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Symfony\Component\Security\Core\Security;
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
{
public function __construct(
private readonly AccompanyingCourseDocumentRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService
) {
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface
{
return $this->repository;
}
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
{
return match ($attribute) {
StoredObjectRoleEnum::EDIT => AccompanyingCourseDocumentVoter::UPDATE,
StoredObjectRoleEnum::SEE => AccompanyingCourseDocumentVoter::SEE_DETAILS,
};
}
protected function getClass(): string
{
return AccompanyingCourseDocument::class;
}
protected function canBeAssociatedWithWorkflow(): bool
{
return true;
}
}

View File

@@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Symfony\Component\Security\Core\Security;
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
{
public function __construct(
private readonly PersonDocumentRepository $repository,
Security $security,
WorkflowStoredObjectPermissionHelper $workflowDocumentService
) {
parent::__construct($security, $workflowDocumentService);
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface
{
return $this->repository;
}
protected function getClass(): string
{
return PersonDocument::class;
}
protected function attributeToRole(StoredObjectRoleEnum $attribute): string
{
return match ($attribute) {
StoredObjectRoleEnum::EDIT => PersonDocumentVoter::UPDATE,
StoredObjectRoleEnum::SEE => PersonDocumentVoter::SEE_DETAILS,
};
}
protected function canBeAssociatedWithWorkflow(): bool
{
return true;
}
}

View File

@@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Security\Authorization;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
interface StoredObjectVoterInterface
{
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool;
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool;
}

View File

@@ -15,7 +15,6 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface; use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -33,8 +32,7 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
public function __construct( public function __construct(
private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider, private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider,
private readonly UrlGeneratorInterface $urlGenerator, private readonly UrlGeneratorInterface $urlGenerator
private readonly Security $security
) {} ) {}
public function normalize($object, ?string $format = null, array $context = []) public function normalize($object, ?string $format = null, array $context = [])
@@ -57,13 +55,13 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
// deprecated property // deprecated property
$datas['creationDate'] = $datas['createdAt']; $datas['creationDate'] = $datas['createdAt'];
$canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object); $canDavSee = in_array(self::ADD_DAV_SEE_LINK_CONTEXT, $context['groups'] ?? [], true);
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object); $canDavEdit = in_array(self::ADD_DAV_EDIT_LINK_CONTEXT, $context['groups'] ?? [], true);
if ($canSee || $canEdit) { if ($canDavSee || $canDavEdit) {
$accessToken = $this->JWTDavTokenProvider->createToken( $accessToken = $this->JWTDavTokenProvider->createToken(
$object, $object,
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE $canDavEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
); );
$datas['_links'] = [ $datas['_links'] = [

View File

@@ -11,13 +11,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner; namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
@@ -29,33 +23,10 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
public function __construct( public function __construct(
private LoggerInterface $logger, private LoggerInterface $logger,
private EntityWorkflowManager $entityWorkflowManager,
private StoredObjectManagerInterface $storedObjectManager,
private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
private EntityManagerInterface $entityManager,
private ClockInterface $clock,
) {} ) {}
public function __invoke(PdfSignedMessage $message): void public function __invoke(PdfSignedMessage $message): void
{ {
$this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]); $this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]);
$signature = $this->entityWorkflowStepSignatureRepository->find($message->signatureId);
if (null === $signature) {
throw new \RuntimeException('no signature found');
}
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($signature->getStep()->getEntityWorkflow());
if (null === $storedObject) {
throw new \RuntimeException('no stored object found');
}
$this->storedObjectManager->write($storedObject, $message->content);
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate($this->clock->now());
$this->entityManager->flush();
$this->entityManager->clear();
} }
} }

View File

@@ -25,7 +25,7 @@ final readonly class PdfSignedMessageSerializer implements SerializerInterface
$body = $encodedEnvelope['body']; $body = $encodedEnvelope['body'];
try { try {
$decoded = json_decode((string) $body, true, 512, JSON_THROW_ON_ERROR); $decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) { } catch (\JsonException $e) {
throw new MessageDecodingFailedException('Could not deserialize message', previous: $e); throw new MessageDecodingFailedException('Could not deserialize message', previous: $e);
} }
@@ -34,7 +34,7 @@ final readonly class PdfSignedMessageSerializer implements SerializerInterface
throw new MessageDecodingFailedException('Could not find expected keys: signatureId or content'); throw new MessageDecodingFailedException('Could not find expected keys: signatureId or content');
} }
$content = base64_decode((string) $decoded['content'], true); $content = base64_decode($decoded['content'], true);
if (false === $content) { if (false === $content) {
throw new MessageDecodingFailedException('Invalid character found in the base64 encoded content'); throw new MessageDecodingFailedException('Invalid character found in the base64 encoded content');

View File

@@ -39,13 +39,13 @@ final readonly class RequestPdfSignMessageSerializer implements SerializerInterf
throw new MessageDecodingFailedException('serializer does not support this message'); throw new MessageDecodingFailedException('serializer does not support this message');
} }
$data = json_decode((string) $body, true); $data = json_decode($body, true);
$zoneSignature = $this->denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [ $zoneSignature = $this->denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [
AbstractNormalizer::GROUPS => ['write'], AbstractNormalizer::GROUPS => ['write'],
]); ]);
$content = base64_decode((string) $data['content'], true); $content = base64_decode($data['content'], true);
if (false === $content) { if (false === $content) {
throw new MessageDecodingFailedException('the content could not be converted from base64 encoding'); throw new MessageDecodingFailedException('the content could not be converted from base64 encoding');

View File

@@ -16,8 +16,6 @@ use Symfony\Component\Serializer\Annotation\Groups;
final readonly class PDFSignatureZone final readonly class PDFSignatureZone
{ {
public function __construct( public function __construct(
#[Groups(['read'])]
public int $index,
#[Groups(['read'])] #[Groups(['read'])]
public float $x, public float $x,
#[Groups(['read'])] #[Groups(['read'])]
@@ -33,8 +31,7 @@ final readonly class PDFSignatureZone
public function equals(self $other): bool public function equals(self $other): bool
{ {
return return
$this->index == $other->index $this->x == $other->x
&& $this->x == $other->x
&& $this->y == $other->y && $this->y == $other->y
&& $this->height == $other->height && $this->height == $other->height
&& $this->width == $other->width && $this->width == $other->width

View File

@@ -17,10 +17,10 @@ class PDFSignatureZoneParser
{ {
public const ZONE_SIGNATURE_START = 'signature_zone'; public const ZONE_SIGNATURE_START = 'signature_zone';
private readonly Parser $parser; private Parser $parser;
public function __construct( public function __construct(
public float $defaultHeight = 90.0, public float $defaultHeight = 180.0,
public float $defaultWidth = 180.0, public float $defaultWidth = 180.0,
) { ) {
$this->parser = new Parser(); $this->parser = new Parser();
@@ -37,7 +37,6 @@ class PDFSignatureZoneParser
$defaults = $pdf->getObjectsByType('Pages'); $defaults = $pdf->getObjectsByType('Pages');
$defaultPage = reset($defaults); $defaultPage = reset($defaults);
$defaultPageDetails = $defaultPage->getDetails(); $defaultPageDetails = $defaultPage->getDetails();
$zoneIndex = 0;
foreach ($pdf->getPages() as $index => $page) { foreach ($pdf->getPages() as $index => $page) {
$details = $page->getDetails(); $details = $page->getDetails();
@@ -48,9 +47,8 @@ class PDFSignatureZoneParser
); );
foreach ($page->getDataTm() as $dataTm) { foreach ($page->getDataTm() as $dataTm) {
if (str_starts_with((string) $dataTm[1], self::ZONE_SIGNATURE_START)) { if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) {
$zones[] = new PDFSignatureZone($zoneIndex, (float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage); $zones[] = new PDFSignatureZone((float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
++$zoneIndex;
} }
} }
} }

View File

@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Service;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\Security\Core\Security;
class WorkflowStoredObjectPermissionHelper
{
public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {}
public function notBlockedByWorkflow(object $entity): bool
{
$workflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
$currentUser = $this->security->getUser();
foreach ($workflows as $workflow) {
if ($workflow->isFinal()) {
return false;
}
if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
return false;
}
}
return true;
}
}

View File

@@ -16,7 +16,6 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface; use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Twig\Environment; use Twig\Environment;
@@ -129,7 +128,6 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
private NormalizerInterface $normalizer, private NormalizerInterface $normalizer,
private JWTDavTokenProviderInterface $davTokenProvider, private JWTDavTokenProviderInterface $davTokenProvider,
private UrlGeneratorInterface $urlGenerator, private UrlGeneratorInterface $urlGenerator,
private Security $security,
) {} ) {}
/** /**
@@ -150,10 +148,8 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
* @throws \Twig\Error\RuntimeError * @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError * @throws \Twig\Error\SyntaxError
*/ */
public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $showEditButtons = true, array $options = []): string public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string
{ {
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $document) && $showEditButtons;
$accessToken = $this->davTokenProvider->createToken( $accessToken = $this->davTokenProvider->createToken(
$document, $document,
$canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE $canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE

View File

@@ -122,8 +122,7 @@ class TempUrlOpenstackGeneratorTest extends TestCase
$signedUrl = new SignedUrl( $signedUrl = new SignedUrl(
'GET', 'GET',
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543', 'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
\DateTimeImmutable::createFromFormat('U', '1702043543'), \DateTimeImmutable::createFromFormat('U', '1702043543')
$objectName
); );
foreach ($baseUrls as $baseUrl) { foreach ($baseUrls as $baseUrl) {
@@ -154,7 +153,6 @@ class TempUrlOpenstackGeneratorTest extends TestCase
$signedUrl = new SignedUrlPost( $signedUrl = new SignedUrlPost(
'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543', 'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543',
\DateTimeImmutable::createFromFormat('U', '1702043543'), \DateTimeImmutable::createFromFormat('U', '1702043543'),
$objectName,
150, 150,
1, 1,
1800, 1800,

View File

@@ -35,7 +35,7 @@ class AsyncUploadExtensionTest extends KernelTestCase
{ {
$generator = $this->prophesize(TempUrlGeneratorInterface::class); $generator = $this->prophesize(TempUrlGeneratorInterface::class);
$generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any()) $generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any())
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'), $args[1])); ->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class); $urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
$urlGenerator->generate('async_upload.generate_url', Argument::type('array')) $urlGenerator->generate('async_upload.generate_url', Argument::type('array'))

View File

@@ -73,7 +73,6 @@ class AsyncUploadControllerTest extends TestCase
return new SignedUrlPost( return new SignedUrlPost(
'https://object.store.example', 'https://object.store.example',
new \DateTimeImmutable('1 hour'), new \DateTimeImmutable('1 hour'),
'abc',
150, 150,
1, 1,
1800, 1800,
@@ -88,8 +87,7 @@ class AsyncUploadControllerTest extends TestCase
return new SignedUrl( return new SignedUrl(
$method, $method,
'https://object.store.example', 'https://object.store.example',
new \DateTimeImmutable('1 hour'), new \DateTimeImmutable('1 hour')
$object_name
); );
} }
}; };

View File

@@ -23,7 +23,6 @@ use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Form\PreloadedExtension; use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Serializer;
@@ -81,15 +80,11 @@ class StoredObjectTypeTest extends TypeTestCase
$urlGenerator->generate('chill_docstore_dav_document_get', Argument::type('array'), UrlGeneratorInterface::ABSOLUTE_URL) $urlGenerator->generate('chill_docstore_dav_document_get', Argument::type('array'), UrlGeneratorInterface::ABSOLUTE_URL)
->willReturn('http://url/fake'); ->willReturn('http://url/fake');
$security = $this->prophesize(Security::class);
$security->isGranted(Argument::cetera())->willReturn(true);
$serializer = new Serializer( $serializer = new Serializer(
[ [
new StoredObjectNormalizer( new StoredObjectNormalizer(
$jwtTokenProvider->reveal(), $jwtTokenProvider->reveal(),
$urlGenerator->reveal(), $urlGenerator->reveal(),
$security->reveal()
), ),
], ],
[ [

View File

@@ -1,168 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Tests\Security\Authorization;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
use Chill\MainBundle\Entity\User;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
/**
* @internal
*
* @coversNothing
*/
class AbstractStoredObjectVoterTest extends TestCase
{
private AssociatedEntityToStoredObjectInterface $repository;
private Security $security;
private WorkflowStoredObjectPermissionHelper $workflowDocumentService;
protected function setUp(): void
{
$this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class);
$this->security = $this->createMock(Security::class);
$this->workflowDocumentService = $this->createMock(WorkflowStoredObjectPermissionHelper::class);
}
private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null): AbstractStoredObjectVoter
{
// Anonymous class extending the abstract class
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
public function __construct(
private readonly bool $canBeAssociatedWithWorkflow,
private readonly AssociatedEntityToStoredObjectInterface $repository,
Security $security,
?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null
) {
parent::__construct($security, $workflowDocumentService);
}
protected function attributeToRole($attribute): string
{
return 'SOME_ROLE';
}
protected function getRepository(): AssociatedEntityToStoredObjectInterface
{
return $this->repository;
}
protected function getClass(): string
{
return \stdClass::class;
}
protected function canBeAssociatedWithWorkflow(): bool
{
return $this->canBeAssociatedWithWorkflow;
}
};
}
private function setupMockObjects(): array
{
$user = new User();
$token = $this->createMock(TokenInterface::class);
$subject = new StoredObject();
$entity = new \stdClass();
return [$user, $token, $subject, $entity];
}
private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForEntity, object $entity, bool $workflowAllowed): void
{
// Set up token to return user
$token->method('getUser')->willReturn($user);
// Mock the return of an AccompanyingCourseDocument by the repository
$this->repository->method('findAssociatedEntityToStoredObject')->willReturn($entity);
// Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument
$this->security->method('isGranted')->willReturn($isGrantedForEntity);
// Mock case where user is blocked or not by workflow
$this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed);
}
public function testSupportsOnAttribute(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
// Setup mocks for voteOnAttribute method
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, $subject));
}
public function testVoteOnAttributeAllowedAndWorkflowAllowed(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
// Setup mocks for voteOnAttribute method
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
// The voteOnAttribute method should return True when workflow is allowed
self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
}
public function testVoteOnAttributeNotAllowed(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
// Setup mocks for voteOnAttribute method where isGranted() returns false
$this->setupMocksForVoteOnAttribute($user, $token, false, $entity, true);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
// The voteOnAttribute method should return True when workflow is allowed
self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
}
public function testVoteOnAttributeAllowedWorkflowNotAllowed(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
// Setup mocks for voteOnAttribute method
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
// Test voteOnAttribute method
$attribute = StoredObjectRoleEnum::EDIT;
$result = $voter->voteOnAttribute($attribute, $subject, $token);
// Assert that access is denied when workflow is not allowed
$this->assertFalse($result);
}
public function testVoteOnAttributeAllowedWorkflowAllowedToSeeDocument(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
// Setup mocks for voteOnAttribute method
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
// Test voteOnAttribute method
$attribute = StoredObjectRoleEnum::SEE;
$result = $voter->voteOnAttribute($attribute, $subject, $token);
// Assert that access is denied when workflow is not allowed
$this->assertTrue($result);
}
}

View File

@@ -14,14 +14,11 @@ namespace Chill\DocStoreBundle\Tests\Security\Authorization;
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\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
use Chill\MainBundle\Entity\User;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger; use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Security;
/** /**
* @internal * @internal
@@ -30,93 +27,97 @@ use Symfony\Component\Security\Core\Security;
*/ */
class StoredObjectVoterTest extends TestCase class StoredObjectVoterTest extends TestCase
{ {
use ProphecyTrait;
/** /**
* @dataProvider provideDataVote * @dataProvider provideDataVote
*/ */
public function testVote(array $storedObjectVotersDefinition, object $subject, string $attribute, bool $fallbackSecurityExpected, bool $securityIsGrantedResult, mixed $expected): void public function testVote(TokenInterface $token, ?object $subject, string $attribute, mixed $expected): void
{ {
$storedObjectVoters = array_map(fn (array $definition) => $this->buildStoredObjectVoter($definition[0], $definition[1], $definition[2]), $storedObjectVotersDefinition); $voter = new StoredObjectVoter();
$token = new UsernamePasswordToken(new User(), 'chill_main', ['ROLE_USER']);
$security = $this->createMock(Security::class);
$security->expects($fallbackSecurityExpected ? $this->atLeastOnce() : $this->never())
->method('isGranted')
->with($this->logicalOr($this->identicalTo('ROLE_USER'), $this->identicalTo('ROLE_ADMIN')))
->willReturn($securityIsGrantedResult);
$voter = new StoredObjectVoter($security, $storedObjectVoters, new NullLogger());
self::assertEquals($expected, $voter->vote($token, $subject, [$attribute])); self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
} }
private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface public function provideDataVote(): iterable
{
$storedObjectVoter = $this->createMock(StoredObjectVoterInterface::class);
$storedObjectVoter->expects($supportsIsCalled ? $this->once() : $this->never())->method('supports')
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class))
->willReturn($supports);
$storedObjectVoter->expects($supportsIsCalled && $supports ? $this->once() : $this->never())->method('voteOnAttribute')
->with(self::isInstanceOf(StoredObjectRoleEnum::class), $this->isInstanceOf(StoredObject::class), $this->isInstanceOf(TokenInterface::class))
->willReturn($voteOnAttribute);
return $storedObjectVoter;
}
public static function provideDataVote(): iterable
{ {
yield [ yield [
// we try with something else than a SToredObject, the voter should abstain $this->buildToken(StoredObjectRoleEnum::EDIT, new StoredObject()),
[[false, false, false]],
new \stdClass(), new \stdClass(),
'SOMETHING', 'SOMETHING',
false,
false,
VoterInterface::ACCESS_ABSTAIN, VoterInterface::ACCESS_ABSTAIN,
]; ];
yield [ yield [
// we try with an unsupported attribute, the voter must abstain $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
[[false, false, false]], $so,
new StoredObject(),
'SOMETHING', 'SOMETHING',
false,
false,
VoterInterface::ACCESS_ABSTAIN, VoterInterface::ACCESS_ABSTAIN,
]; ];
yield [ yield [
// happy scenario: there is a role voter $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
[[true, true, true]], $so,
new StoredObject(),
StoredObjectRoleEnum::SEE->value, StoredObjectRoleEnum::SEE->value,
false,
false,
VoterInterface::ACCESS_GRANTED, VoterInterface::ACCESS_GRANTED,
]; ];
yield [ yield [
// there is a role voter, but not allowed to see the stored object $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
[[true, true, false]], $so,
new StoredObject(), StoredObjectRoleEnum::EDIT->value,
StoredObjectRoleEnum::SEE->value,
false,
false,
VoterInterface::ACCESS_DENIED,
];
yield [
// there is no role voter, fallback to security, which does not grant access
[[true, false, false]],
new StoredObject(),
StoredObjectRoleEnum::SEE->value,
true,
false,
VoterInterface::ACCESS_DENIED,
];
yield [
// there is no role voter, fallback to security, which does grant access
[[true, false, false]],
new StoredObject(),
StoredObjectRoleEnum::SEE->value,
true,
true,
VoterInterface::ACCESS_GRANTED, VoterInterface::ACCESS_GRANTED,
]; ];
yield [
$this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
$so,
StoredObjectRoleEnum::EDIT->value,
VoterInterface::ACCESS_DENIED,
];
yield [
$this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
$so,
StoredObjectRoleEnum::SEE->value,
VoterInterface::ACCESS_GRANTED,
];
yield [
$this->buildToken(null, null),
new StoredObject(),
StoredObjectRoleEnum::SEE->value,
VoterInterface::ACCESS_DENIED,
];
yield [
$this->buildToken(null, null),
new StoredObject(),
StoredObjectRoleEnum::SEE->value,
VoterInterface::ACCESS_DENIED,
];
}
private function buildToken(?StoredObjectRoleEnum $storedObjectRoleEnum = null, ?StoredObject $storedObject = null): TokenInterface
{
$token = $this->prophesize(TokenInterface::class);
if (null !== $storedObjectRoleEnum) {
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(true);
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn($storedObjectRoleEnum);
} else {
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(false);
$token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willThrow(new \InvalidArgumentException());
}
if (null !== $storedObject) {
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(true);
$token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn($storedObject->getUuid()->toString());
} else {
$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(false);
$token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willThrow(new \InvalidArgumentException());
}
return $token->reveal();
} }
} }

View File

@@ -38,8 +38,7 @@ class SignedUrlNormalizerTest extends KernelTestCase
$signedUrl = new SignedUrl( $signedUrl = new SignedUrl(
'GET', 'GET',
'https://object.store.example/container/object', 'https://object.store.example/container/object',
\DateTimeImmutable::createFromFormat('U', '1700000'), \DateTimeImmutable::createFromFormat('U', '1700000')
'object'
); );
$actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]); $actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]);
@@ -49,7 +48,6 @@ class SignedUrlNormalizerTest extends KernelTestCase
'method' => 'GET', 'method' => 'GET',
'expires' => 1_700_000, 'expires' => 1_700_000,
'url' => 'https://object.store.example/container/object', 'url' => 'https://object.store.example/container/object',
'object_name' => 'object',
], ],
$actual $actual
); );

View File

@@ -38,7 +38,6 @@ class SignedUrlPostNormalizerTest extends KernelTestCase
$signedUrl = new SignedUrlPost( $signedUrl = new SignedUrlPost(
'https://object.store.example/container/object', 'https://object.store.example/container/object',
\DateTimeImmutable::createFromFormat('U', '1700000'), \DateTimeImmutable::createFromFormat('U', '1700000'),
'abc',
15000, 15000,
1, 1,
180, 180,
@@ -60,7 +59,6 @@ class SignedUrlPostNormalizerTest extends KernelTestCase
'method' => 'POST', 'method' => 'POST',
'expires' => 1_700_000, 'expires' => 1_700_000,
'url' => 'https://object.store.example/container/object', 'url' => 'https://object.store.example/container/object',
'object_name' => 'abc',
], ],
$actual $actual
); );

View File

@@ -1,99 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessage;
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessageHandler;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\Clock\MockClock;
/**
* @internal
*
* @coversNothing
*/
class PdfSignedMessageHandlerTest extends TestCase
{
public function testThatObjectIsWrittenInStoredObjectManagerHappyScenario(): void
{
// a dummy stored object
$storedObject = new StoredObject();
// build the associated EntityWorkflow, with one step with a person signature
$entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futurePersonSignatures[] = new Person();
$entityWorkflow->setStep('new_step', $dto, 'new_transition', new \DateTimeImmutable(), new User());
$step = $entityWorkflow->getCurrentStep();
$signature = $step->getSignatures()->first();
$handler = new PdfSignedMessageHandler(
new NullLogger(),
$this->buildEntityWorkflowManager($storedObject),
$this->buildStoredObjectManager($storedObject, $expectedContent = '1234'),
$this->buildSignatureRepository($signature),
$this->buildEntityManager(true),
new MockClock('now'),
);
// we simply call the handler. The mocked StoredObjectManager will check that the "write" method is invoked once
// with the content "1234"
$handler(new PdfSignedMessage(10, $expectedContent));
self::assertEquals('signed', $signature->getState()->value);
}
private function buildSignatureRepository(EntityWorkflowStepSignature $signature): EntityWorkflowStepSignatureRepository
{
$entityWorkflowStepSignatureRepository = $this->createMock(EntityWorkflowStepSignatureRepository::class);
$entityWorkflowStepSignatureRepository->method('find')->with($this->isType('int'))->willReturn($signature);
return $entityWorkflowStepSignatureRepository;
}
private function buildEntityWorkflowManager(?StoredObject $associatedStoredObject): EntityWorkflowManager
{
$entityWorkflowManager = $this->createMock(EntityWorkflowManager::class);
$entityWorkflowManager->method('getAssociatedStoredObject')->willReturn($associatedStoredObject);
return $entityWorkflowManager;
}
private function buildStoredObjectManager(StoredObject $expectedStoredObject, string $expectedContent): StoredObjectManagerInterface
{
$storedObjectManager = $this->createMock(StoredObjectManagerInterface::class);
$storedObjectManager->expects($this->once())
->method('write')
->with($this->identicalTo($expectedStoredObject), $expectedContent);
return $storedObjectManager;
}
private function buildEntityManager(bool $willFlush): EntityManagerInterface
{
$em = $this->createMock(EntityManagerInterface::class);
$em->expects($willFlush ? $this->once() : $this->never())->method('flush');
$em->expects($willFlush ? $this->once() : $this->never())->method('clear');
return $em;
}
}

View File

@@ -36,7 +36,7 @@ class RequestPdfSignMessageSerializerTest extends TestCase
$envelope = new Envelope( $envelope = new Envelope(
$request = new RequestPdfSignMessage( $request = new RequestPdfSignMessage(
0, 0,
new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)), new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
0, 0,
'metadata to add to the signature', 'metadata to add to the signature',
'Mme Caroline Diallo', 'Mme Caroline Diallo',
@@ -66,7 +66,7 @@ class RequestPdfSignMessageSerializerTest extends TestCase
$request = new RequestPdfSignMessage( $request = new RequestPdfSignMessage(
0, 0,
new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)), new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
0, 0,
'metadata to add to the signature', 'metadata to add to the signature',
'Mme Caroline Diallo', 'Mme Caroline Diallo',
@@ -121,7 +121,7 @@ class RequestPdfSignMessageSerializerTest extends TestCase
$denormalizer = new class () implements DenormalizerInterface { $denormalizer = new class () implements DenormalizerInterface {
public function denormalize($data, string $type, ?string $format = null, array $context = []) public function denormalize($data, string $type, ?string $format = null, array $context = [])
{ {
return new PDFSignatureZone(0, 10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)); return new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0));
} }
public function supportsDenormalization($data, string $type, ?string $format = null) public function supportsDenormalization($data, string $type, ?string $format = null)

View File

@@ -58,18 +58,16 @@ class PDFSignatureZoneParserTest extends TestCase
__DIR__.'/data/signature_2_signature_page_1.pdf', __DIR__.'/data/signature_2_signature_page_1.pdf',
[ [
new PDFSignatureZone( new PDFSignatureZone(
0,
127.7, 127.7,
95.289, 95.289,
90.0, 180.0,
180.0, 180.0,
$page = new PDFPage(0, 595.30393, 841.8897) $page = new PDFPage(0, 595.30393, 841.8897)
), ),
new PDFSignatureZone( new PDFSignatureZone(
1,
269.5, 269.5,
95.289, 95.289,
90.0, 180.0,
180.0, 180.0,
$page, $page,
), ),

View File

@@ -202,8 +202,7 @@ final class StoredObjectManagerTest extends TestCase
$response = new SignedUrl( $response = new SignedUrl(
'PUT', 'PUT',
'https://example.com/'.$storedObject->getFilename(), 'https://example.com/'.$storedObject->getFilename(),
new \DateTimeImmutable('1 hours'), new \DateTimeImmutable('1 hours')
$storedObject->getFilename()
); );
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class); $tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);

View File

@@ -43,7 +43,7 @@ class AsyncFileExistsValidatorTest extends ConstraintValidatorTestCase
$generator = $this->prophesize(TempUrlGeneratorInterface::class); $generator = $this->prophesize(TempUrlGeneratorInterface::class);
$generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any()) $generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any())
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'), $args[1])); ->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
return new AsyncFileExistsValidator($generator->reveal(), $client); return new AsyncFileExistsValidator($generator->reveal(), $client);
} }

View File

@@ -12,25 +12,27 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Workflow; namespace Chill\DocStoreBundle\Workflow;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface
* @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingCourseDocument>
*/
readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
{ {
private readonly EntityRepository $repository;
/**
* TODO: injecter le repository directement.
*/
public function __construct( public function __construct(
private TranslatorInterface $translator, EntityManagerInterface $em,
private EntityWorkflowRepository $workflowRepository, private readonly TranslatorInterface $translator
private AccompanyingCourseDocumentRepository $repository ) {
) {} $this->repository = $em->getRepository(AccompanyingCourseDocument::class);
}
public function getDeletionRoles(): array public function getDeletionRoles(): array
{ {
@@ -71,6 +73,8 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl
} }
/** /**
* @param AccompanyingCourseDocument $object
*
* @return array[] * @return array[]
*/ */
public function getRelatedObjects(object $object): array public function getRelatedObjects(object $object): array
@@ -117,23 +121,4 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl
{ {
return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass(); return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass();
} }
public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject
{
return $this->getRelatedEntity($entityWorkflow)?->getObject();
}
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return false;
}
public function findByRelatedEntity(object $object): array
{
if (!$object instanceof AccompanyingCourseDocument) {
return [];
}
return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId());
}
} }

View File

@@ -5,5 +5,4 @@ module.exports = function(encore)
}); });
encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.ts'); encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.ts');
encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index'); encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index');
encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index.ts');
}; };

View File

@@ -1,13 +0,0 @@
services:
_defaults:
autowire: true
autoconfigure: true
Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter:
arguments:
$storedObjectVoters: !tagged_iterator stored_object_voter
tags:
- { name: security.voter }
Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter:
tags:
- { name: security.voter }

View File

@@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Form\EventType; use Chill\EventBundle\Form\EventType;
use Chill\EventBundle\Form\Type\PickEventType; use Chill\EventBundle\Form\Type\PickEventType;
use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
@@ -61,7 +61,7 @@ final class EventController extends AbstractController
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry
) {} ) {}
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'POST', 'DELETE'])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/delete', name: 'chill_event__event_delete', requirements: ['event_id' => '\d+'], methods: ['GET', 'DELETE'])]
public function deleteAction($event_id, Request $request): \Symfony\Component\HttpFoundation\RedirectResponse|Response public function deleteAction($event_id, Request $request): \Symfony\Component\HttpFoundation\RedirectResponse|Response
{ {
$em = $this->managerRegistry->getManager(); $em = $this->managerRegistry->getManager();
@@ -78,10 +78,10 @@ final class EventController extends AbstractController
$form = $this->createDeleteForm($event_id); $form = $this->createDeleteForm($event_id);
if (Request::METHOD_POST === $request->getMethod()) { if (Request::METHOD_DELETE === $request->getMethod()) {
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isValid()) {
foreach ($participations as $participation) { foreach ($participations as $participation) {
$em->remove($participation); $em->remove($participation);
} }
@@ -108,6 +108,28 @@ final class EventController extends AbstractController
]); ]);
} }
/**
* Displays a form to edit an existing Event entity.
*/
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/edit', name: 'chill_event__event_edit')]
public function editAction($event_id): Response
{
$em = $this->managerRegistry->getManager();
$entity = $em->getRepository(Event::class)->find($event_id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Event entity.');
}
$editForm = $this->createEditForm($entity);
return $this->render('@ChillEvent/Event/edit.html.twig', [
'entity' => $entity,
'edit_form' => $editForm->createView(),
]);
}
/** /**
* List events subscriptions for a person. * List events subscriptions for a person.
* *
@@ -291,7 +313,7 @@ final class EventController extends AbstractController
/** /**
* Edits an existing Event entity. * Edits an existing Event entity.
*/ */
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/edit', name: 'chill_event__event_edit', methods: ['GET', 'POST', 'PUT'])] #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/event/{event_id}/update', name: 'chill_event__event_update', methods: ['POST', 'PUT'])]
public function updateAction(Request $request, $event_id): \Symfony\Component\HttpFoundation\RedirectResponse|Response public function updateAction(Request $request, $event_id): \Symfony\Component\HttpFoundation\RedirectResponse|Response
{ {
$em = $this->managerRegistry->getManager(); $em = $this->managerRegistry->getManager();
@@ -302,20 +324,14 @@ final class EventController extends AbstractController
throw $this->createNotFoundException('Unable to find Event entity.'); throw $this->createNotFoundException('Unable to find Event entity.');
} }
$editForm = $this->createForm(EventType::class, $entity, [ $editForm = $this->createEditForm($entity);
'center' => $entity->getCenter(),
'role' => EventVoter::UPDATE,
]);
$editForm->add('submit', SubmitType::class, ['label' => 'Update']);
$editForm->handleRequest($request); $editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) { if ($editForm->isValid()) {
$em->persist($entity);
$em->flush(); $em->flush();
$this->addFlash('success', $this->translator->trans('The event was updated')); $this->addFlash('success', $this->translator
->trans('The event was updated'));
return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]); return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]);
} }
@@ -402,6 +418,7 @@ final class EventController extends AbstractController
$builder->add('event_id', HiddenType::class, [ $builder->add('event_id', HiddenType::class, [
'data' => $event->getId(), 'data' => $event->getId(),
]); ]);
dump($event->getId());
return $builder->getForm(); return $builder->getForm();
} }
@@ -583,7 +600,29 @@ final class EventController extends AbstractController
->setAction($this->generateUrl('chill_event__event_delete', [ ->setAction($this->generateUrl('chill_event__event_delete', [
'event_id' => $event_id, 'event_id' => $event_id,
])) ]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }
/**
* Creates a form to edit a Event entity.
*
* @return \Symfony\Component\Form\FormInterface
*/
private function createEditForm(Event $entity)
{
$form = $this->createForm(EventType::class, $entity, [
'action' => $this->generateUrl('chill_event__event_update', ['event_id' => $entity->getId()]),
'method' => 'PUT',
'center' => $entity->getCenter(),
'role' => 'CHILL_EVENT_CREATE',
]);
$form->remove('center');
$form->add('submit', SubmitType::class, ['label' => 'Update']);
return $form;
}
} }

View File

@@ -201,7 +201,7 @@ class EventTypeController extends AbstractController
/** /**
* Creates a form to delete a EventType entity by id. * Creates a form to delete a EventType entity by id.
* *
* @return \Symfony\Component\Form\FormInterface The form * @return \Symfony\Component\Form\Form The form
*/ */
private function createDeleteForm(mixed $id) private function createDeleteForm(mixed $id)
{ {
@@ -210,6 +210,7 @@ class EventTypeController extends AbstractController
'chill_eventtype_admin_delete', 'chill_eventtype_admin_delete',
['id' => $id] ['id' => $id]
)) ))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Form\ParticipationType; use Chill\EventBundle\Form\ParticipationType;
use Chill\EventBundle\Repository\EventRepository; use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\ParticipationVoter; use Chill\EventBundle\Security\Authorization\ParticipationVoter;
use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
@@ -259,10 +259,10 @@ final class ParticipationController extends AbstractController
$form = $this->createDeleteForm($participation_id); $form = $this->createDeleteForm($participation_id);
if (Request::METHOD_POST === $request->getMethod()) { if (Request::METHOD_DELETE === $request->getMethod()) {
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isValid()) {
$em->remove($participation); $em->remove($participation);
$em->flush(); $em->flush();
@@ -753,6 +753,7 @@ final class ParticipationController extends AbstractController
->setAction($this->generateUrl('chill_event_participation_delete', [ ->setAction($this->generateUrl('chill_event_participation_delete', [
'participation_id' => $participation_id, 'participation_id' => $participation_id,
])) ]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -201,7 +201,7 @@ class RoleController extends AbstractController
/** /**
* Creates a form to delete a Role entity by id. * Creates a form to delete a Role entity by id.
* *
* @return \Symfony\Component\Form\FormInterface The form * @return \Symfony\Component\Form\Form The form
*/ */
private function createDeleteForm(mixed $id) private function createDeleteForm(mixed $id)
{ {

View File

@@ -201,12 +201,13 @@ class StatusController extends AbstractController
/** /**
* Creates a form to delete a Status entity by id. * Creates a form to delete a Status entity by id.
* *
* @return \Symfony\Component\Form\FormInterface The form * @return \Symfony\Component\Form\Form The form
*/ */
private function createDeleteForm(mixed $id) private function createDeleteForm(mixed $id)
{ {
return $this->createFormBuilder() return $this->createFormBuilder()
->setAction($this->generateUrl('chill_event_admin_status_delete', ['id' => $id])) ->setAction($this->generateUrl('chill_event_admin_status_delete', ['id' => $id]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->getForm();
} }

View File

@@ -11,8 +11,8 @@ declare(strict_types=1);
namespace Chill\EventBundle\DependencyInjection; namespace Chill\EventBundle\DependencyInjection;
use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\ParticipationVoter; use Chill\EventBundle\Security\Authorization\ParticipationVoter;
use Symfony\Component\Config\FileLocator; 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;
@@ -33,13 +33,12 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml'); $loader->load('services.yaml');
$loader->load('services/security.yaml'); $loader->load('services/authorization.yaml');
$loader->load('services/fixtures.yaml'); $loader->load('services/fixtures.yaml');
$loader->load('services/forms.yaml'); $loader->load('services/forms.yaml');
$loader->load('services/repositories.yaml'); $loader->load('services/repositories.yaml');
$loader->load('services/search.yaml'); $loader->load('services/search.yaml');
$loader->load('services/timeline.yaml'); $loader->load('services/timeline.yaml');
$loader->load('services/export.yaml');
} }
/** (non-PHPdoc). /** (non-PHPdoc).

View File

@@ -31,7 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert;
/** /**
* Class Event. * Class Event.
*/ */
#[ORM\Entity] #[ORM\Entity(repositoryClass: \Chill\EventBundle\Repository\EventRepository::class)]
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'chill_event_event')] #[ORM\Table(name: 'chill_event_event')]
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
@@ -47,7 +47,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
private ?Scope $circle = null; private ?Scope $circle = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE)] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE)]
private ?\DateTime $date = null; private ?\DateTime $date;
#[ORM\Id] #[ORM\Id]
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)] #[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
@@ -62,9 +62,9 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
private ?string $name = null; private ?string $name = null;
/** /**
* @var Collection<int, Participation> * @var Collection<Participation>
*/ */
#[ORM\OneToMany(mappedBy: 'event', targetEntity: Participation::class)] #[ORM\OneToMany(targetEntity: Participation::class, mappedBy: 'event')]
private Collection $participations; private Collection $participations;
#[Assert\NotNull] #[Assert\NotNull]
@@ -79,7 +79,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
private ?Location $location = null; private ?Location $location = null;
/** /**
* @var Collection<int, StoredObject> * @var Collection<StoredObject>
*/ */
#[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist', 'refresh'])] #[ORM\ManyToMany(targetEntity: StoredObject::class, cascade: ['persist', 'refresh'])]
#[ORM\JoinTable('chill_event_event_documents')] #[ORM\JoinTable('chill_event_event_documents')]
@@ -192,7 +192,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
{ {
$iterator = iterator_to_array($this->participations->getIterator()); $iterator = iterator_to_array($this->participations->getIterator());
uasort($iterator, static fn ($first, $second) => strnatcasecmp($first->getPerson()->getFirstName(), $second->getPerson()->getFirstName())); uasort($iterator, static fn ($first, $second) => strnatcasecmp((string) $first->getPerson()->getFirstName(), (string) $second->getPerson()->getFirstName()));
return new \ArrayIterator($iterator); return new \ArrayIterator($iterator);
} }
@@ -265,9 +265,11 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/** /**
* Set label. * Set label.
* *
* @param string $label
*
* @return Event * @return Event
*/ */
public function setName(?string $label) public function setName($label)
{ {
$this->name = $label; $this->name = $label;

View File

@@ -38,13 +38,13 @@ class EventType
private $name; private $name;
/** /**
* @var Collection<int, Role> * @var Collection<Role>
*/ */
#[ORM\OneToMany(mappedBy: 'type', targetEntity: Role::class)] #[ORM\OneToMany(targetEntity: Role::class, mappedBy: 'type')]
private Collection $roles; private Collection $roles;
/** /**
* @var Collection<int, Status> * @var Collection<Status>
*/ */
#[ORM\OneToMany(targetEntity: Status::class, mappedBy: 'type')] #[ORM\OneToMany(targetEntity: Status::class, mappedBy: 'type')]
private Collection $statuses; private Collection $statuses;
@@ -146,9 +146,11 @@ class EventType
/** /**
* Set active. * Set active.
* *
* @param bool $active
*
* @return EventType * @return EventType
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;

View File

@@ -81,9 +81,11 @@ class Role
/** /**
* Set active. * Set active.
* *
* @param bool $active
*
* @return Role * @return Role
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;

View File

@@ -81,9 +81,11 @@ class Status
/** /**
* Set active. * Set active.
* *
* @param bool $active
*
* @return Status * @return Status
*/ */
public function setActive(bool $active) public function setActive($active)
{ {
$this->active = $active; $this->active = $active;

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