From 4a229ebf6b20b6578975bf935829c436c27656fd Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 15:32:51 +0200 Subject: [PATCH 001/375] Initial commit --- .changes/unreleased/Feature-20240614-153236.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changes/unreleased/Feature-20240614-153236.yaml diff --git a/.changes/unreleased/Feature-20240614-153236.yaml b/.changes/unreleased/Feature-20240614-153236.yaml new file mode 100644 index 000000000..a29000cc1 --- /dev/null +++ b/.changes/unreleased/Feature-20240614-153236.yaml @@ -0,0 +1,8 @@ +kind: Feature +body: |- + Electronic signature + + Implementation of the electronic signature for documents within chill. +time: 2024-06-14T15:32:36.875891692+02:00 +custom: + Issue: "" From 7923b5a1ef25f7209fedde12d955e4e61af2005f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 15:35:50 +0200 Subject: [PATCH 002/375] initial commit --- .changes/unreleased/Feature-20240614-153537.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changes/unreleased/Feature-20240614-153537.yaml diff --git a/.changes/unreleased/Feature-20240614-153537.yaml b/.changes/unreleased/Feature-20240614-153537.yaml new file mode 100644 index 000000000..c16d49b2d --- /dev/null +++ b/.changes/unreleased/Feature-20240614-153537.yaml @@ -0,0 +1,7 @@ +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" From 65c41e6fa957b9616d9e9be3f5feec9080f23d21 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 16:48:09 +0200 Subject: [PATCH 003/375] Add StoredObjectVoterInterface An interface is defined that can be implemented by each context-specific voter in the future. --- .../StoredObjectVoterInterface.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php new file mode 100644 index 000000000..47b93eabb --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -0,0 +1,22 @@ + Date: Fri, 14 Jun 2024 17:22:27 +0200 Subject: [PATCH 004/375] Refactorize StoredObjectVoter.php The StoredObjectVoter.php has been refactorized to handle context-specific voters.\ This way we can check if the context-specific voter should handle the authorization or not.\ If not, there is a simple fallback to check on the USER_ROLE. --- .../Authorization/StoredObjectVoter.php | 28 ++++++++++++++----- .../config/services/voter.yaml | 14 ++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/config/services/voter.yaml diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 2e253cf3c..ecfc56615 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\Security; /** * Voter for the content of a stored object. @@ -23,6 +24,14 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; */ class StoredObjectVoter extends Voter { + private $security; + private $storedObjectVoters; + + public function __construct(Security $security, iterable $storedObjectVoters) { + $this->security = $security; + $this->storedObjectVoters = $storedObjectVoters; + } + protected function supports($attribute, $subject): bool { return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum @@ -43,13 +52,18 @@ class StoredObjectVoter extends Voter return false; } - $askedRole = StoredObjectRoleEnum::from($attribute); - $tokenRoleAuthorization = - $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS); + // Loop through context-specific voters + foreach ($this->storedObjectVoters as $storedObjectVoter) { + if ($storedObjectVoter->supports($attribute, $subject)) { + return $storedObjectVoter->voteOnAttribute($attribute, $subject, $token); + } + } - return match ($askedRole) { - StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization, - StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization - }; + // User role-based fallback + if ($this->security->isGranted('ROLE_USER')) { + return true; + } + + return false; } } diff --git a/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml b/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml new file mode 100644 index 000000000..922d29cba --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml @@ -0,0 +1,14 @@ +services: + _defaults: + autowire: true + autoconfigure: true + Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter: + arguments: + $storedObjectVoters: + # context specific voters + - '@accompanying_course_document_voter' + tags: + - { name: security.voter } + + accompanying_course_document_voter: + class: Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter From e9a9262fae52274da1012d070182f4825175def9 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 17:27:22 +0200 Subject: [PATCH 005/375] Add config voter.yaml The voter.yaml was not configured to be taken into account. Now added\ to ChillDocStoreExtension.php --- .../DependencyInjection/ChillDocStoreExtension.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index fe9aeecfa..5f87199a3 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -42,6 +42,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf $loader->load('services/fixtures.yaml'); $loader->load('services/form.yaml'); $loader->load('services/templating.yaml'); + $loader->load('services/voter.yaml'); } public function prepend(ContainerBuilder $container) From c8ccce83fda19699c427db88f38639c1bc76f09b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 18 Jun 2024 17:47:16 +0200 Subject: [PATCH 006/375] add a dependency on smalot/pdfparser to parse signature zone within pdf --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 34f9bbfc7..05068125d 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "phpoffice/phpspreadsheet": "^1.16", "ramsey/uuid-doctrine": "^1.7", "sensio/framework-extra-bundle": "^5.5", + "smalot/pdfparser": "^2.10", "spomky-labs/base64url": "^2.0", "symfony/asset": "^5.4", "symfony/browser-kit": "^5.4", From 4b82e67952983c7b2942e693596b467a4e75833c Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 09:51:21 +0200 Subject: [PATCH 007/375] Type-hint $subject in StoredObjectVoterInterface.php Since the subject passed to these voters should\ always be of the type StoredObject, type-hinting\ added. --- .../Security/Authorization/StoredObjectVoterInterface.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index 47b93eabb..a3a76e79a 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -11,12 +11,13 @@ declare(strict_types=1); namespace ChillDocStoreBundle\Security\Authorization; +use Chill\DocStoreBundle\Entity\StoredObject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; interface StoredObjectVoterInterface { - public function supports(string $attribute, $subject): bool; + public function supports(string $attribute, StoredObject $subject): bool; - public function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool; + public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool; } From ad4fe8024092e7b365ee0f6aba7a5987ca964d18 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 09:52:59 +0200 Subject: [PATCH 008/375] Add fall-back right for ROLE_ADMIN Within the StoredObjectVoter.php also the admin\ user should be able to edit documents as a fall-back --- .../Security/Authorization/StoredObjectVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index ecfc56615..781c2c542 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -60,7 +60,7 @@ class StoredObjectVoter extends Voter } // User role-based fallback - if ($this->security->isGranted('ROLE_USER')) { + if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) { return true; } From 04a48f22ad85abd8fdb13155c805ec709a7757c9 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:00:10 +0200 Subject: [PATCH 009/375] Use constructor property promotion In accordance with php8.1 use property promotion\ within the constructor method. Less clutter. --- .../Security/Authorization/StoredObjectVoter.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 781c2c542..7e2becab4 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -24,12 +24,8 @@ use Symfony\Component\Security\Core\Security; */ class StoredObjectVoter extends Voter { - private $security; - private $storedObjectVoters; - public function __construct(Security $security, iterable $storedObjectVoters) { - $this->security = $security; - $this->storedObjectVoters = $storedObjectVoters; + public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters) { } protected function supports($attribute, $subject): bool From e015f71bb001ca00662e9f97a82cc8095efdb284 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:02:25 +0200 Subject: [PATCH 010/375] Rename voter.yaml file to security.yaml For consistency with other bundles voters are\ registered under the security.yaml file. --- .../DependencyInjection/ChillDocStoreExtension.php | 2 +- .../config/services/{voter.yaml => security.yaml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Bundle/ChillDocStoreBundle/config/services/{voter.yaml => security.yaml} (100%) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index 5f87199a3..a0fcc2d5d 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -42,7 +42,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf $loader->load('services/fixtures.yaml'); $loader->load('services/form.yaml'); $loader->load('services/templating.yaml'); - $loader->load('services/voter.yaml'); + $loader->load('services/security.yaml'); } public function prepend(ContainerBuilder $container) diff --git a/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml similarity index 100% rename from src/Bundle/ChillDocStoreBundle/config/services/voter.yaml rename to src/Bundle/ChillDocStoreBundle/config/services/security.yaml From e0828b1f0fa9d9c255a6a1189492e33d7c2ca31d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:16:39 +0200 Subject: [PATCH 011/375] Use service tags to inject all voters into StoredObjectVoter.php Instead of manually injecting services into StoredObjectVoter\ config is added to automatically tag each service that implements\ StoredObjectVoterInterface.php --- .../DependencyInjection/ChillDocStoreExtension.php | 3 +++ .../ChillDocStoreBundle/config/services/security.yaml | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index a0fcc2d5d..9f277e716 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\DependencyInjection; use Chill\DocStoreBundle\Controller\StoredObjectApiController; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; +use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -35,6 +36,8 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf $container->setParameter('chill_doc_store', $config); + $container->registerForAutoconfiguration(StoredObjectVoterInterface::class)->addTag('stored_object_voter'); + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader->load('services.yaml'); $loader->load('services/controller.yaml'); diff --git a/src/Bundle/ChillDocStoreBundle/config/services/security.yaml b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml index 922d29cba..c57eb63c5 100644 --- a/src/Bundle/ChillDocStoreBundle/config/services/security.yaml +++ b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml @@ -4,11 +4,10 @@ services: autoconfigure: true Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter: arguments: - $storedObjectVoters: - # context specific voters - - '@accompanying_course_document_voter' + $storedObjectVoters: !tagged_iterator stored_object_voter tags: - { name: security.voter } - accompanying_course_document_voter: - class: Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter + Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter: + tags: + - { name: security.voter } From 482f279dc5635a79f0ee14c21670e570c9a5ba07 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:21:24 +0200 Subject: [PATCH 012/375] Implement StoredObjectVoterInterface An interface was created to be implemented by Stored Doc voters\ these will automatically be tagged and injected into DocStoreVoter. --- .../Authorization/AccompanyingCourseDocumentVoter.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index 5febc7e42..93243b1f4 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -19,11 +19,12 @@ use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; +use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; -class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface +class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface, StoredObjectVoterInterface { final public const CREATE = 'CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE'; @@ -70,12 +71,12 @@ class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements Prov return []; } - protected function supports($attribute, $subject): bool + public function supports($attribute, $subject): bool { return $this->voterHelper->supports($attribute, $subject); } - protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool + public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool { if (!$token->getUser() instanceof User) { return false; From 5bc542a5676ae595fa4d1ea6ac2210d383e84c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 19 Jun 2024 12:16:51 +0200 Subject: [PATCH 013/375] remove symfony/phpunit-bridge --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 05068125d..edd382445 100644 --- a/composer.json +++ b/composer.json @@ -98,7 +98,6 @@ "symfony/debug-bundle": "^5.4", "symfony/dotenv": "^5.4", "symfony/maker-bundle": "^1.20", - "symfony/phpunit-bridge": "^5.4", "symfony/runtime": "^5.4", "symfony/stopwatch": "^5.4", "symfony/var-dumper": "^5.4" From a9f00597430ef44be59e388575afd9e65c96f083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 19 Jun 2024 12:17:25 +0200 Subject: [PATCH 014/375] Add PDF signature zone parsing functionality This update introduces new services into the ChillDocStoreBundle for signature zone parsing within PDFs. The PDFSignatureZoneParser service identifies signature zones within PDF content while the additional classes, PDFPage and PDFSignatureZone, help define these zones and pages. Corresponding tests have also been --- .../Service/Signature/PDFPage.php | 28 +++++++ .../Service/Signature/PDFSignatureZone.php | 33 ++++++++ .../Signature/PDFSignatureZoneParser.php | 57 +++++++++++++ .../Signature/PDFSignatureZoneParserTest.php | 77 ++++++++++++++++++ .../data/signature_2_signature_page_1.pdf | Bin 0 -> 16589 bytes 5 files changed, 195 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneParserTest.php create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/data/signature_2_signature_page_1.pdf diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php new file mode 100644 index 000000000..138c4e1c8 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php @@ -0,0 +1,28 @@ +index === $this->index + && round($page->width, 2) === round($this->width, 2) + && round($page->height, 2) === round($this->height, 2); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php new file mode 100644 index 000000000..515356e52 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php @@ -0,0 +1,33 @@ +x == $other->x + && $this->y == $other->y + && $this->height == $other->height + && $this->width == $other->width + && $this->PDFPage->equals($other->PDFPage); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php new file mode 100644 index 000000000..3aade2fcc --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php @@ -0,0 +1,57 @@ +parser = new Parser(); + } + + /** + * @return list + */ + public function findSignatureZones(string $fileContent): array + { + $pdf = $this->parser->parseContent($fileContent); + $zones = []; + + $defaults = $pdf->getObjectsByType('Pages'); + $defaultPage = reset($defaults); + $defaultPageDetails = $defaultPage->getDetails(); + + foreach ($pdf->getPages() as $index => $page) { + $pdfPage = new PDFPage( + $index, + $page['MediaBox'][2] ?? $defaultPageDetails['MediaBox'][2], + $page['MediaBox'][3] ?? $defaultPageDetails['MediaBox'][3], + ); + + foreach ($page->getDataTm() as $dataTm) { + if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) { + $zones[] = new PDFSignatureZone($dataTm[0][4], $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage); + } + } + } + + return $zones; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneParserTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneParserTest.php new file mode 100644 index 000000000..5a78c8bbf --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/PDFSignatureZoneParserTest.php @@ -0,0 +1,77 @@ + $expected + */ + public function testFindSignatureZones(string $filePath, array $expected): void + { + $content = file_get_contents($filePath); + + if (false === $content) { + throw new \LogicException("Unable to read file {$filePath}"); + } + + $actual = self::$parser->findSignatureZones($content); + + self::assertEquals(count($expected), count($actual)); + + foreach ($actual as $index => $signatureZone) { + self::assertObjectEquals($expected[$index], $signatureZone); + } + } + + public static function provideFiles(): iterable + { + yield [ + __DIR__.'/data/signature_2_signature_page_1.pdf', + [ + new PDFSignatureZone( + 127.7, + 95.289, + 180.0, + 180.0, + $page = new PDFPage(0, 595.30393, 841.8897) + ), + new PDFSignatureZone( + 269.5, + 95.289, + 180.0, + 180.0, + $page, + ), + ], + ]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/data/signature_2_signature_page_1.pdf b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/data/signature_2_signature_page_1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..89d91e67cffe721456aa7c0c870a49e7c3b4b278 GIT binary patch literal 16589 zcmd73WmsIzvM3zff_n%sKyaJEo!}7M-QC?SxVw7-Aq01KcMtCF?h^Qh_w0T4e$PJl zx%cO{o~LKkR9AOZcggBCQ$;Q(EJ_bzU_&JD{?XmiUDut1$OHfaYz-_Bd3YG5jBQMv z%mB<_i~^&mxs{W#1EZ*wzLT-Av7xPzF&`hIqmzTNzBQs-=8R@*%yR?s$z6FWSBDJU zgX7oUciH8nD-cLO;HP%t89tXa`o3C)krI<@+|tF9Hf-LSdK!KCL`dY=OPic*=pp<4ga+ zIHRk>%k`h}c5l|x!}bqrHPWumy3)RXj@#3}deXqaw+8ntah^cG1h?lq!ywtGjytzI@Mp%s!Rl+~5)~7^Pm9=ZZmt zqvyyzkb9iBlyH=6m8i`3^<}vK>S+siTI7$D%@^ECWSx&#W$s?nWFO*W9fMhK^#v9! z`ROC7=|(CdAH`vt-Pj|kR>?!A4d~C3^JT}HDjkzIW=Tx%k)@ZS_H+GAwkTXdCt6m= znZnUX`i2kmJOBYx7(p+6#5Uv6U5*v!Xtd|em!=3V-`o6%m)FanfZCCihsA!}Y1#m| zIZnEiY!Hus?L`(z@a)1w?Y01+Luc0T3a1WiNMtIf!K6XGW$sk~KJSC|IKK}^eZH@U zOmr!9lK9X^k;0yf&gjjXrl5Wj#I0?W`U&o`1S8`S6MHmh8*ww2qp#7)GimOh9v%V< zB|&n~i6wt{xfncL=w`KFIq8JD_#bY@FiRMS-rw0?=$b1NU2_P3zmK&hDJgCG&APWL z+RH*xU>(ya0>mN;Q~ZV>a2KOD#Kix>A~yrFcKtg9J)(@-=(;`RI)nap8(JR62lTKR zi{9@!na@hrJSg80;1^w9{f;7W-rDq<2@5AIteqC6@^ z$Xbd1nRcoo(r_k1DI zL-u_=O0aG@kHQZ!1BGvKCrGH`wEP`5iJm9 zM$I1)6s;ehershu((N!TlLgPPzxPxaNx+7^eD2$_vE_q6WxnX{Ui zwt^=Xbz(#EeWg=RM6m2nFl=B}F!VEuptL01-_KaRpF&7*{N!b-VF5EH8;XuO7E>Vr zF!qO6U-mBt8AHGFTn)|iD(>Q}NiL3Z4LGTnKbE(<6|19VM_G<6fc(Nf4>F4nrl-!i zd^Ft=W2xPdxmQ2*)mWD*!Loz&6HgGNFz2J^%ztNuC@m9#^GBgerywP7{I-}DvoMF zj6t+ri0HB64ceeKH3%v5v}`#k^YInj-7OW*GKXn==o1n2G6et4S{1KpVH;;$l&DDt zEGbfDI%UU}&|pKR{-5_eb6m5Pat$e`fh*LURM>{S1@Ay;bt<}}p-|3W5A|0&O%O}A zT`0G`gcNR|Oz3MRJ$FjaJcRUL5Re+;JXvQ2+-{#{%=1L#vl5D)df#D`;OtTp>6qIf^@U6G5*EW&#L(|;_t-HAULtE2c=h)?vgC&@gL=8KnKZ* zL-4^j3 zGUN%|#MFLhCM{ej1R=^+?FJu}xx4)5us7k*P?A zMBqfXzsH;`j;@bCdm?GxZjUBL1qhm&GD#Utues)jPVlq5Y6!I9w@SK%r;2D>`(;1# zpQ~s3PQB8^O1WMb?s{w1V*Q~CKVT$3lOIMO(><`Q^^s!PfX4b09gObkaxE}~cLx{= ztPM5xACIq)Q&_b9ILE09e#>ug{VvS({|NO3hcO9+PfGNv#`ET{$DHcxqE_b?$o6rbrmYBke_E z;nQc*3|}1(*?A*|gp$14=O2ecrqneA(DRbkpyR%uk;Tt_oVfSjgFf&dy2fXHu^6_C zBF~Jv->XsJ@oh(fqZIG>Xw|WsBO!8;4 z(?HRkt-XkfAdS z&Zce=UdEyhx3r_Zy3S4I#Y zq%BsBy%0fF4EQo-+`k&8tkV>daY;K5W#Gr!9xn6FO-%@BR%s*=dgU)N)36N8c|`DX zbKfn8I}{tEjgS9438*HfUzpg1C+J_8g5{dFnpxG{Td1j?wHuRhW6uy<43rE-BQ?v2 zu2$Dv`CWz38$=XTFK)Qo<;RAaWv78KPu4w6uV`;`>_#rhQ2u0LhaM}_7kX8^?AS=&z*zy+plQ*1hvPY1Q@uzDN&Y)|7X%C7;m<2j zG>pz#c45yr6Vs$b?4#h{Rx+Zo@=_&Ygvq{Ey|u@{dXGkq;W$UAqj10H^cBH&)_X=R zSEq6VC5LsMxPN}{>ut=7ek)`H#J~y%u+p(h%v4A^G2e9N!G=xgZFy76n z69ZuP&|`;x$63?NVOh@^^?`r6SHE3FYAJ--1GEh zadfKMEO0kDRhF_Sf#twE%;@2WR#!g_(r3muFMrN@_Ve%J>a#(Uc|x2ND)(7LUix98 z)PmQwy}v)@n3cCEWf7cx}>E1ad?F45e1MS8rvBC<7f_s z-cIv~Z-@9dfQge0_}2XwlJ!5%`fUFXXMN@gW+kdU8YQ3{aDo|FB?qi!nPFyPSFQkp zepds*eP4DE=M)CE5S8A&tH*#Z`(LDaWAi2f>%U~-_zzh){|~bK zk4MVPOz_Ggy+J))TConjb1EtH~!PIV}eS5q~v-Ahbd~>$Wt+SCC@npXm z-Mc3rZzyKRv&Owu&r|mm$J2qZww03}VOV&{A9bf@&qx8!2qfEU@DP?u*?|p4S>EW$ zVhH*#tz>O^Pkz3u4$){fsc0+b(OGj&R>q{zXlL0VlWNO1Zvy3Ts*L#Xk(-nvJ#MA%!nm8ONQmSC8CX+XF zfXpeHz&Y22G|Q90ySCwk-B93;*qUqGLeD_*%CD0y)FbnORWdZF=fH8O zmxzqPfu97En5uTLpv7eK>NQ1aU$OjYAkQfEBdKj{sf7>}LL5UC0m;H#%m6LCav9o! z6cJ^BF4Hl{0b@zg4c(!jDqd2D#DS4hz=zE^8SQ{#j)Yz|j$M!FfwSlf;{){dHpL}r z^dmwSL;G(zDF!|+vr00gN1O)=$Ji2sF@qhS{2&A%52pxGpf!gB+K*_<< zCE53w1WBF^DmLjR2zL?fY$3NtTI{zJm}i4?gP>83(LAcse)u>QtxC@2spT5`?#GFy z+LUtjRtw{;@ZiU$o!a3a>G_g4j7=pT8~zW{O^h|P%)MEX-AJ@q( zb7|!HV;<|OKUY&D-U{mprRpXipq?UVbNi#Ow!>JhX;#DKtx;He8J~JdWRr)GN%l;F zqM(9e&r@$FKc-gnYd7jo!CV?)&7RjZs@_l4JO~6DUPC)JpLI}C*zS9SI|uZxw|hEV z+6SKCVlt!xE5c7yL&AP@cRH2~e^V2V6=%!OhnHI3!&-^@$hv+q8=hqoOA}!11h} zVv4mHbc(r`uPbJ+dr#4rn*{RZb$Hfe;jAF6J^CU7^x;c;DqPwlu0b?#21wb9{ID=C zk(9D9Ax)@Ib%Fa$kUtENrESdlsYBdk@1N$cTynbw9OAfnc?^B@l`X)8LyMb?>ZKMj zvyk}WtQMPF)N>e<0#_l%;tzRLl-DH%`+V_ZR5v58Oi;ypt`BEXcFhJp_4K5f?UrvjKk;l$tHL)oI`~Hbjt{$9v zM(QB>E`47~Pc?b&?TJizcI9Z`Ow!Nf8G=8>bGA0>zcAu0cJn8MDHtBIsXS zeCOAwDe~nas@Q^f6NhP-sm7JnS)~h&K}ZY2Uh|n{R3lPsSk2XG8Cn8IX%<^BqqNhb zo8MO3J+{6vHRuw?0(NK!uZtWq8^mh2PW8#J0aS zUzKW*$KTlLUCNY_R1!QRU7Jt7zi1Kjv{84{Pu^g?wVQ0}7KvnQY2h9ZVdDOS&C3h6 zu9k;v^QXY~NJ!hQtXfatrQHO+N|3n$e0u)U3K-zy}>n0|C~QX;_i6(kuxw zpU`178#KwXP)g~+-q|15 z`uWRtb=U`N%jmxc1SjU6*X<4#X0X?10yj>Nf68Y5QfZXa&dj2+T2?viP0gj63*m+% z)9;-Z=L?5@6~cIyl$UzR7Z06H9J#2UqlkLZfYHh8uvD-sg`E2yF36{M2a)Jag%A#_ zHMuF2fQ969O0>)?K*s2`kSSs`oY@Q27!{oDX8 zl>%nGdGUEYLY(zq@5G?z89xhtMS);Lm;iK0L0rO2Alk`%T|=-F{hEV#Uh^&nu_7dw zHGk5Fyn`B|s(%ph8$pibT`1%{Gyqa*OV9K`FBA{+8`DZhBm$78!yG~sIur?jAjd%U z=bp|@;Va4Fhg^8g8B5hiixb6iYk2DwF87o4mPZOk1x32pZ_E=-)|Mdqsc^j@i|Ce5KjTFQq;d6ney0}YH$ zdf<)|jwA3Sb`nhUGoYj5%tE^h$B~0>cnpz((!3@SdJytt;5ol9BeY=lncdW2ie|<- zb;V+$aO-y<*i#B;0ys%#j3>LXuyVgSAnuq8xxA7rGS49AXfkSD*Oh8At|RV1YV(w} zXuu;5ExNB=rzt001zNB}9*=Ba4@Wl$Y#ZJ0Rx@r zXxr%LPFIjrx;ImBsu_oPPoFO|RLu(`u(k7>Y3nBJ(gNi~k;OBzq{u>kj555F^jo#5 z^hDJDgjLtj!qC^#q(xE8%8yldNiq6ZLGAkkEj|dS1RsK={|#+32#q>WjwDu4Eu66s zZ3sz#4!RAkkBW#ie}QlC>V1KP^OpmqY&hk*CF`KCUOIZ?Ft68XkcgkLj=vt?DUU(h z47L$o#U!TqnoeuB-|rwzkgqK*Ah}xmv6lqgg_QIKtyZCazB-_E=!bPXugs%7Ro#}0 zjB7=j<3i0&q<~IvS!5wj<87|_ZE6sJeKjPw;hwzEX6|2D)4+mts}X*}P&%tuVGU1k zub%2wAwtka*G>ta;Sc18XZDXJhPFoIL+)AtWFmCQ2H3H{-$Cnpq5-f1n*fQ5Jep_? z0H^4D^pJp9cz>+GFN=}Zd;aGtXrlhHKMapi1?hgF#C2^`iE;#Vi_`#%TQy6ayx6n9jKI4{X5*NIb)f z~smOgfHPAqV*S3spUK#`)rbAEQ{2!)DA!OK;iglK^Zh3cb%mahgC zIH>7c((WAp@TIc5Y?sPFuO&4~mp+nxVsMFPo|DKYk5^Kl+40hJK_>iaaJr(T{kT~* zqJY1dFjpZ_g9~>N2Q~F5h-6yJmB|^MdEgT=6HHsvIBGy;HFnqFe0rlV(_g*MtQN5y z0>O{Quaxqn{~ML8hUg+7QMv3w$b@%@OR1kA^V;PASzI{)P(~^1Y=`~eLa>#?N4u7w z03$v~v~uaK>X)77X|{77(f-m~hH@gWzBt@tGhgp%eLr+%6;kZf85uwRdPMec1^&IH ziW$wW%2Lc{od6fQm!C!2tD`0SC`-CaAz#*%7MM4j%{+H(NEqEEH-ECOZb(&Pcm=mB z=*1wsD`>_L>bMcsIKhQGI~sKD0nb2Ii*s!~lVC$n7N@Q(0EeNejQ^D>IK1D_M{7m% zV-Vfd;f1foy^Z@p%Z%#l{fpgGa`+k1>cwcsa#7pOXi9mjC-bF@Z@+%b&7X`TqSm^p ztzt{dh8LML$rV32?VCma@T#4R{kAR9wHFl6_pWQU;-x(nyBq;C>vk5GmsfW94>%}7 z1IYT!IkrrJT@+7fE1cjHgZYiw?CO_&lgx#Hgo9RA7=gfJdKT-;=AWS~Zhk!2I?%Mg z1ZhOMB`+=+_2Ad2CVBm#5`h;&qit^2kCL`8n3!0W&3t0H<_rg&ZnYSM7~!K zvh>n*4BbXY#gCm`;2^Vabc<%Z-o5y4N3;m|0GO2ShPh%m?%2xHt6cS7%SJc&9=y6u?niaseQbl;%goQ=-=Yah$$ssm+w~^Fn}Tw~XY3wWhKH2zOgdEFPOi+6}>Xi*i^5l8MgS1_q)Vh^Fd8XP|SCJrai7&xe-v z!=sZ0Mgs8ld*CI=-cQolLK_5vG(=jgYN&n0oVgXfeEJ}$TRsy>HKpnFHoq4T3fNR+ ziBt^hwKg-%UzYxrK>I_Tv$3&>C)t^UGXZ_aXab*}O)&6AKmHY{T=jtx`j?Gy*Ec=U zDB!zX^jKKJ%=Pjjzi+%|cSwmVcuXCO;z!QxHFM98a}h&~OZ#iKM@yZ{+FVDqbLo9| z^u;?VoUKZ}ALM;qD)~um6oi}dKM3e5^@LF*L>RukhGa8)d(WHWLMstRtYZVQ1ZBUE z56;>Dl65ga4MN}tB59cVu^?4cI5T}!!dlZP?PL9+kKkD{I@%SqqegwPtiygy#je4PVNr=n zz9+L%q0@)>kVP>f!o6;R{@)?q~^O!EdhLCi3 zy+?Ip;A)&kD5MLX$7c1)o>srMM8!vi4|3_-P7=u4TECka-F-8l-D~2mJR&^Ve63D^ zK)eEf(f?9&jS*$wmEl#`NAsl~#`u-AT}@~UBQk5vV6CI!N30foY0VDK_JNZW!xlD- zEY*8R-Pi;}CeB2w^htB4?-hOYVL$*R`gg8RU1+7k!k_oWo9M#RQaK5kiZyNfM5MyR z0I4pBNKA1EQDVE(Ujyl__Y+-L5*=si!4bIy<18uSj!^CQnLH%MZPa*H}mpQtMP;G-#Wt5r=Q?n;{1U7MlZ37SV_Y;zk>UM z{ZuF(*dL)p8zjyg6_#@;+&z*K+mhURFWAd z2i9uA%CNlGdxEzug)`Kadl4i*1_a%*O+6d5Di>U#h4jns(|2SR0(32F7p;yTE23n{ zKVV1`0keMb9mA|5<+0?5du-To6*o8q$L0DkA!qXtq{Rc}bEL19A6VKe4t5J1H`1!{ z+}D#u!u)@(1kA+{w-7=u&U=$F{@JaSW-Je?Oz3u*ZV2$rqIFCj8hi+udwB`4e`q)L zDC9F38Ep%(ypA=^w+tQKW!l^QpcO1wUI4(lMI%u8h=-l_@s=le(ku{JADKTMGL{o08`NEZWl<<0^yf3$kvVG{kY;{;+1YLDj}3ibHza~Bi{70XlUUfGn_ zt`Dk!2YBp9h6R1^5~8GKpR$g<kFbIp~U z)OlLGXC&f-&FO}F{o~2@kX0TUddN21WS249pO^zx9*BX2 z)(-d@JDz4q{S!1fvBW9FosmeP4i=YueGN7aq84)>N=*`e0>*bYmu*tAMyKJqszIjd z{bVABQJMX_`X+8Pyw2IFrtN#b_7e9Z_e%P|b(jL=AdtzCQ6puO*u)GS*Zp zHam0r4(Y4ZOVSM$#quy}RtQO{mN1Z+6opYCJ%)>r<-GMyWE`-+CYaMD@l>&aHfWzO ziLX#Pg!GV7HL{5k6?->n?j3KG*p|rLmL^(HyzF_{7rtXnr(?XQehrY%E`7rkt%S}4 z(F7D`QQ7FzCicu$%em<3eIp6y?Z7?vv+1SKZrcH*8|j=>d$PvhIa(`(8gi-W)M zXmm#gvCdDj?%D6N8^TN|y>o_S8^*5YT{Kq53#9mu4KE15Z6hQe{kvT! zh)+m*zus-apwWnzR^8+((U+d%?m<~d8E!pqMiwb8Qk?T$8!eZ9(Ib1^X?Zq%VElpt zXHCYKZ;)|y=EHM)!EHrO-%h^v=NduTWN~f6dG(XJBOwYcT6LK5jYoA7yf20c`}&=ICnwZY&IUht-bz zhLPyf^MJ&F2|**Jc!K+(bDC^YsnbB-=aZhj??w=@n|?cPq|Scd5OGVzB2q+)+%XJ$ zkk2#{T&rCcZrL~KB&4gXpRUVw;pU&JR*qJalory!h}Svc((w)t*EH(ln9PqAu#*Tu}6ym*t zLx;y0tf{qKA8dgY&?+F0jl&?|51aTrz$Acz;P=9}DOhH!uVenzoof-ge^j}Q3>~pd zdtjxeV1?K?tO6hidaTi^{EPw|2twJ`t$|D&*EECy?f9`-Z2*1zxvD|Vk{#auR(pQ()3^&JC1e7`g_B_~5)+^)oZbvtNj`(UAKp!a$+ zGI=2yDuAhlBL6_VO$mHTr^rcYXCzK|edm+d4IJ8d_gM6?N`qzwJ+f3Z4^x`P5C_jGNLw7Q>6I!H&JYa^rBqAH0tcdyX`^Z z?$V?}%&B0r8Q+O3`L2f3_UhMJyJF*uoG|tqcAi%S*T>f@+ma&;hoAPim;aQAo~XG@aFM74 zq0r$T@r0`KUXUhRW5q~CZtx&seZZ!kIO2?5w@lq0>GMT(U_13VjY$vPEm+h#`RbyU zlGiNzs`$tE0|PTp_Ouxt3lW<%^G%venk{qAa4YDpoz8x^+6(EmMULdX`qAaj9p0Qf z+9i!8sJc1okS~KwHDkXSGu$u$nic1mw*~Nu8c5$MyZfc_vkiU*u*!KJh=*vHfJ{zQ zhCgmD|4at0lHC;=)1UgT$q#cm(3gRfXE6y93yq-$gEVS0p?J|0hDDj8-1)9jT#QFD zEEc4S)LJ#h$YG8#f;?m%kiHuptNvy}i!+>I5^~^d*Dx9MdheuQjAHNd%NUt@-9~v> zLt(&Mlm2Lc^Ax7GgtD5I@^_V=)O1UJvSVsJtFQd9XqDV+kp^2FJ#3x^TLLZeL8F62 z{zg($8KHJqndOq90bP1%xOMA=TOL@>A~V^b{95#-JP?umDyUQr)iC?y050EY)Pvu9X4Z6 zLq%I*Yq@TrQsO9Wak^CtT79u@WO!JcSxGDRqHEIp5-16!vfNut)i<{l)lZw(z;U6E z34|qGbN8voOjgQj$VqE%a&l%?#?Si1JB*6-j)E(0iq`ec`KG^VF>&E>mNBMWTpnJ>_H5VlKCY6pR!J z=OU~xvL>OUr&e@l)5Loik)Ur+S2S^yMW%z}sk`H#d`s!=rCYR<#B!Lv+>NnPy%32w zH5Ym%iCAehifZACy|4i6bHn9Gf3iC2EburZ8X2ncOzO_A)mE3(+luLFcYjb2YZ^dl z!F1D=T$G3mbrY*3>Qx{(Ah&lwKFCwcg2x`T3LNWng`uXrI>#AA>^bbc&6?N!G+enH z(wb+m8=Oh#}508|~@xnIxqJEKmQo;9UKIB1qi0cfw=S!)5v)!jS2QC3ktS`;f zH>xBhxKDpreL_R$Wb)@?=uc{J^73d5*~$7u!?AODv1LB(Tk&EQA7C(-b>nka_!1-; zeW;F#E;SRj*w&$QE#hg#cvumm4xRKlsJ+=5!IfN}YO0J=s1F%)=M18~@mW-fxbbn; zrp{)Sr}029IyYcc$Q@ad7?vLz$2%$ASBqJ68ks%>ak0*4y6jQ&wjk@7e#t;{c6kS_>h;EL@?7rc5(7!MUuTXMyANm-T@yr8RCb`gR1BaaLHm( zDT7+k9OXIYiQ1``8m{^zk5NdHeX2pU_9Z)V=47mDt>zP3E~<1F2 zQh1UG&I;0+2OAq9h~Fnyl;W%nSC+L5>mlHOZXt))4ZrDy*9~l&ju`*YGPmJPx%zRH zI?|yBex3W?=ZONi6~;?MUd`+{f9lB?z~G32)F39fd=XNm2Y9;WMA+uZ4{U#)#IM(-}k zHpL&ry^Qp>uLt%A=0knfAG*hx`Mns{YkBs(qc5`6Tn*L`*e>zZh_3kh0^9JxX=(QY z6+HXiz;nP;#~CsizuvsTk&*X!&`PrMznZeEk`8=PuzmID~u8;h97fp=lXE)5@3 zj5`Z(&)%&bouBqT$+Up>1e_RGA=Jp|jWpXK`kI$rvDchewBgRCdTj9yT#46p_{4`XJbo|c(a1xFkO#1qC$tuHqNR}k5f%pS($eDrN{TXaHxaE9>8S@of zg{azbflKis1W5%oK8^~Wn(-{QZMi!f9dli9D1@Z7*)xOTQS^hMlr3EN-TptYU@JO2 zqxTXP?RDFr7Fw^sUSd1#C>Mi~&GIMgd1dV;iS8 z=#AAsKp}lQabt5+GpDy9h>VI(#?~qT4lvEz%;0i8VoZGR!y6sm} zDu*Qd7eHnorVp1V`mXTZnZ6N*u89koAroSaQFrrUoP);jY0f{1o7_1Ms+;Q+lM5EN zw%CSm(8jhrvUb*{o0mQk@0w4?P(CE?=o&KG!{c6EJ9wb2VA%Dg2;h_M@j3?8q%nw^zaEDnph}SWM_m$i|Q6x;f-i>)C zXO69uv;9=cP>ATV&FGP7Tntbhs)h3GleCjq^`kXb{c72#+sRlmb5H9Ivl?4FPH@HR zCaDra=*aRi)&O(nj_Y96$>E{#sCu^E+YURuW}f+?b9JDbSCHQxQl0Os?RTKN(XRQZ z%Bc6~*`WUDdD4DwN&iRkMD`WKvErI>V|`zjbdCP1e}wRl|mQ^1G@z)5C~vqW(Kf;t@#bd#0mtw+4Em`4$i;sZ}1!L zt-aBKX%OG;Z}`9Zz+>3h*#2t_c&>lq-|p;8On)SZmy-jZD&M2e^4k#|>hQkzU|su>;|v+zXf$f{Eux!{MSB$2mC*5BmitU zlIBK^nt-=BssQ*)8E;4-TW7GcnEp)!(7zQ|{3U~sJ~*b^nt~N;3fALadPQWEwKKL6 zFnoJFX#UrXjB4r{02^m3tN;1{g6Edfw=tzMaiA9zq5hlI{IiGxyhs6OCo@|I9#Iid zQ6P{-0BiomFu00Kr=MpLFbCz5EXx zh!gA>f5U-TS=qtr_!|z$%=8vQ{)PjwfKL|AUU>Kj&iw@74dLV+C=5 z!_MFNf))Bd*9Kzc0D|rJpL9+R`rspj!<)+}ntOo5A3LLhtt~i&{pDMTj1o2`wgAwZ z&4Ht&gfKu8z#}Tm!p>^;x5fl|*;S>_#5ESKna{)&ueFvw%%mKs( PWMM@lCl`?wMg0E&^Ql1< literal 0 HcmV?d00001 From 99818c211d39ebd7c12751e9144c93e0a3a5bb40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 19 Jun 2024 12:18:20 +0200 Subject: [PATCH 015/375] Fix cs: upgrade of php-cs-fixer --- .../Tests/Form/Type/TranslatableActivityTypeTest.php | 2 +- .../Connector/MSGraph/RemoteEventConverter.php | 4 ++-- .../CompilerPass/ShortMessageCompilerPass.php | 2 +- src/Bundle/ChillMainBundle/Export/ExportManager.php | 4 ++-- .../ChillMainBundle/Search/Utils/ExtractDateFromPattern.php | 6 +++--- .../Search/Utils/ExtractPhonenumberFromPattern.php | 2 +- .../Tests/Controller/AccompanyingCourseControllerTest.php | 4 ++-- .../Tests/DependencyInjection/ChillReportExtensionTest.php | 2 +- .../ChillWopiBundle/src/Resources/config/services.php | 4 ++-- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Tests/Form/Type/TranslatableActivityTypeTest.php b/src/Bundle/ChillActivityBundle/Tests/Form/Type/TranslatableActivityTypeTest.php index c6ecc377c..6715af3ed 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Form/Type/TranslatableActivityTypeTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Form/Type/TranslatableActivityTypeTest.php @@ -60,7 +60,7 @@ final class TranslatableActivityTypeTest extends KernelTestCase $this->assertInstanceOf( ActivityType::class, $form->getData()['type'], - 'The data is an instance of Chill\\ActivityBundle\\Entity\\ActivityType' + 'The data is an instance of Chill\ActivityBundle\Entity\ActivityType' ); $this->assertEquals($type->getId(), $form->getData()['type']->getId()); diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php index 94b488ddd..84797f6f1 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php @@ -37,12 +37,12 @@ class RemoteEventConverter * valid when the remote string contains also a timezone, like in * lastModifiedDate. */ - final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P'; + final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\TH:i:s.u?P'; /** * Same as above, but sometimes the date is expressed with only 6 milliseconds. */ - final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\\TH:i:s.uP'; + final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\TH:i:s.uP'; private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0'; diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php index ebbfcee1b..9da9154b3 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php @@ -43,7 +43,7 @@ class ShortMessageCompilerPass implements CompilerPassInterface $defaultTransporter = new Reference(NullShortMessageSender::class); } elseif ('ovh' === $dsn['scheme']) { if (!class_exists('\\'.\Ovh\Api::class)) { - throw new RuntimeException('Class \\Ovh\\Api not found'); + throw new RuntimeException('Class \Ovh\Api not found'); } foreach (['user', 'host', 'pass'] as $component) { diff --git a/src/Bundle/ChillMainBundle/Export/ExportManager.php b/src/Bundle/ChillMainBundle/Export/ExportManager.php index 9cb2a54ba..d4e87d449 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportManager.php +++ b/src/Bundle/ChillMainBundle/Export/ExportManager.php @@ -190,7 +190,7 @@ class ExportManager // throw an error if the export require other modifier, which is // not allowed when the export return a `NativeQuery` if (\count($export->supportsModifiers()) > 0) { - throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\\Doctrine\\ORM\\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\\ORM\\QueryBuilder`'); + throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`'); } } elseif ($query instanceof QueryBuilder) { // handle filters @@ -203,7 +203,7 @@ class ExportManager 'dql' => $query->getDQL(), ]); } else { - throw new \UnexpectedValueException('The method `intiateQuery` should return a `\\Doctrine\\ORM\\NativeQuery` or a `Doctrine\\ORM\\QueryBuilder` object.'); + throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.'); } $result = $export->getResult($query, $data[ExportType::EXPORT_KEY]); diff --git a/src/Bundle/ChillMainBundle/Search/Utils/ExtractDateFromPattern.php b/src/Bundle/ChillMainBundle/Search/Utils/ExtractDateFromPattern.php index f858797f5..9cce3fa13 100644 --- a/src/Bundle/ChillMainBundle/Search/Utils/ExtractDateFromPattern.php +++ b/src/Bundle/ChillMainBundle/Search/Utils/ExtractDateFromPattern.php @@ -14,9 +14,9 @@ namespace Chill\MainBundle\Search\Utils; class ExtractDateFromPattern { private const DATE_PATTERN = [ - ['([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))', 'Y-m-d'], // 1981-05-12 - ['((0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/([12]\\d{3}))', 'd/m/Y'], // 15/12/1980 - ['((0[1-9]|[12]\\d|3[01])-(0[1-9]|1[0-2])-([12]\\d{3}))', 'd-m-Y'], // 15/12/1980 + ['([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))', 'Y-m-d'], // 1981-05-12 + ['((0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([12]\d{3}))', 'd/m/Y'], // 15/12/1980 + ['((0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])-([12]\d{3}))', 'd-m-Y'], // 15/12/1980 ]; public function extractDates(string $subject): SearchExtractionResult diff --git a/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php b/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php index 1823e462c..2be3d54db 100644 --- a/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php +++ b/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php @@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; class ExtractPhonenumberFromPattern { - private const PATTERN = '([\\+]{0,1}[0-9\\ ]{5,})'; + private const PATTERN = '([\+]{0,1}[0-9\ ]{5,})'; private readonly string $defaultCarrierCode; diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php index 4f8f1453d..240c3ab98 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php @@ -74,7 +74,7 @@ final class AccompanyingCourseControllerTest extends WebTestCase $this->assertResponseRedirects(); $location = $this->client->getResponse()->headers->get('Location'); - $this->assertEquals(1, \preg_match('|^\\/[^\\/]+\\/parcours/([\\d]+)/edit$|', (string) $location)); + $this->assertEquals(1, \preg_match('|^\/[^\/]+\/parcours/([\d]+)/edit$|', (string) $location)); } /** @@ -93,7 +93,7 @@ final class AccompanyingCourseControllerTest extends WebTestCase $location = $this->client->getResponse()->headers->get('Location'); $matches = []; - $this->assertEquals(1, \preg_match('|^\\/[^\\/]+\\/parcours/([\\d]+)/edit$|', (string) $location, $matches)); + $this->assertEquals(1, \preg_match('|^\/[^\/]+\/parcours/([\d]+)/edit$|', (string) $location, $matches)); $id = $matches[1]; $period = self::getContainer()->get(EntityManagerInterface::class) diff --git a/src/Bundle/ChillReportBundle/Tests/DependencyInjection/ChillReportExtensionTest.php b/src/Bundle/ChillReportBundle/Tests/DependencyInjection/ChillReportExtensionTest.php index ae01d8d90..db938f0bf 100644 --- a/src/Bundle/ChillReportBundle/Tests/DependencyInjection/ChillReportExtensionTest.php +++ b/src/Bundle/ChillReportBundle/Tests/DependencyInjection/ChillReportExtensionTest.php @@ -41,7 +41,7 @@ final class ChillReportExtensionTest extends KernelTestCase } if (!$reportFounded) { - throw new \Exception('Class Chill\\ReportBundle\\Entity\\Report not found in chill_custom_fields.customizables_entities', 1); + throw new \Exception('Class Chill\ReportBundle\Entity\Report not found in chill_custom_fields.customizables_entities', 1); } } } diff --git a/src/Bundle/ChillWopiBundle/src/Resources/config/services.php b/src/Bundle/ChillWopiBundle/src/Resources/config/services.php index 005994bb6..d29f33205 100644 --- a/src/Bundle/ChillWopiBundle/src/Resources/config/services.php +++ b/src/Bundle/ChillWopiBundle/src/Resources/config/services.php @@ -32,10 +32,10 @@ return static function (ContainerConfigurator $container) { ->autoconfigure(); $services - ->load('Chill\\WopiBundle\\Service\\', __DIR__.'/../../Service'); + ->load('Chill\WopiBundle\Service\\', __DIR__.'/../../Service'); $services - ->load('Chill\\WopiBundle\\Controller\\', __DIR__.'/../../Controller') + ->load('Chill\WopiBundle\Controller\\', __DIR__.'/../../Controller') ->tag('controller.service_arguments'); $services From 427f232ab805125a010a9a4a78cad62e795a32be Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 10:53:33 +0200 Subject: [PATCH 016/375] Correct namespace and use statement for StoredObjectVoterInterface.php The namespace was formed wrong and needed adjustment --- .../Security/Authorization/AccompanyingCourseDocumentVoter.php | 1 - .../Security/Authorization/StoredObjectVoterInterface.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index 93243b1f4..dac08b05b 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -19,7 +19,6 @@ use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; -use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index a3a76e79a..c0fd7e09f 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace ChillDocStoreBundle\Security\Authorization; +namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; From d26fa6bde6e8113cdb2e6d4aded57635a1d8254a Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 15:17:56 +0200 Subject: [PATCH 017/375] Implement voting logic: separation of concerns A separate AccompanyingCourseDocumentStoredObjectVoter was\ created to handle the specific access to a Stored object\ related to an Accompanying Course Document. --- .../AccompanyingCourseDocumentRepository.php | 13 +++++ ...panyingCourseDocumentStoredObjectVoter.php | 50 +++++++++++++++++++ ...nyingPeriodWorkEvaluationDocumentVoter.php | 7 +-- 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 2679993c4..239b88b90 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Repository; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; +use Chill\DocStoreBundle\Entity\StoredObject; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; @@ -45,6 +46,17 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } + public function findLinkedCourseDocument(int $storedObjectId): ?AccompanyingCourseDocument { + + $qb = $this->repository->createQueryBuilder('d'); + $query = $qb->leftJoin('d.storedObject', 'do') + ->where('do.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObjectId) + ->getQuery(); + + return $query->getResult(); + } + public function find($id): ?AccompanyingCourseDocument { return $this->repository->find($id); @@ -69,4 +81,5 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository { return AccompanyingCourseDocument::class; } + } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php new file mode 100644 index 000000000..db1499a99 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository->findLinkedCourseDocument($subject->getId())); + } + + public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool + { + if (!$token->getUser() instanceof User) { + return false; + } + + // Retrieve the related accompanying course document + $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject->getId()); + + // Determine the attribute to pass to AccompanyingCourseDocumentVoter + $voterAttribute = ($attribute === self::SEE_AND_EDIT) ? AccompanyingCourseDocumentVoter::UPDATE : AccompanyingCourseDocumentVoter::SEE_DETAILS; + + // Check access using AccompanyingCourseDocumentVoter + if ($this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { + // TODO implement logic to check for associated workflow + return true; + } else { + return false; + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php index 77c131cd9..8f4a1b995 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Security\Authorization; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; @@ -21,13 +22,13 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; * * Delegates to the sames authorization than for Evalution */ -class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter +class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter implements StoredObjectVoterInterface { final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW'; public function __construct(private readonly AccessDecisionManagerInterface $accessDecisionManager) {} - protected function supports($attribute, $subject) + public function supports($attribute, $subject): bool { return $subject instanceof AccompanyingPeriodWorkEvaluationDocument && self::SEE === $attribute; @@ -39,7 +40,7 @@ class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter * * @return bool|void */ - protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool + public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool { return match ($attribute) { self::SEE => $this->accessDecisionManager->decide( From 760d65b9723d66b4bd3dd1aec55cfd784864a962 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:27:21 +0200 Subject: [PATCH 018/375] Remove implementation of StoredObjectVoterInterface in AccompanyingCourseDocumentVoter.php This implementation has been moved to the voter\ AccompanyingCourseDocumentStoredObjectVoter.php --- .../Security/Authorization/AccompanyingCourseDocumentVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index dac08b05b..7a46184cb 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -23,7 +23,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; -class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface, StoredObjectVoterInterface +class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface { final public const CREATE = 'CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE'; From 3d40db749386c47e4e6b690b41a53113f18fce29 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:28:19 +0200 Subject: [PATCH 019/375] Refactor AccompanyingCourseDocumentRepository.php Build where clause using StoredObject directly instead\ of based on it's id. --- .../Repository/AccompanyingCourseDocumentRepository.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 239b88b90..93cf00ecb 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -46,12 +46,11 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } - public function findLinkedCourseDocument(int $storedObjectId): ?AccompanyingCourseDocument { + public function findLinkedCourseDocument(StoredObject $storedObject): ?AccompanyingCourseDocument { $qb = $this->repository->createQueryBuilder('d'); - $query = $qb->leftJoin('d.storedObject', 'do') - ->where('do.id = :storedObjectId') - ->setParameter('storedObjectId', $storedObjectId) + $query = $qb->where('d.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) ->getQuery(); return $query->getResult(); From 73797b98f670c20535b8f1ca03a5f7de5862d224 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:32:09 +0200 Subject: [PATCH 020/375] Add WorkflowDocumentService and use in StoredObject voters A WorkflowDocumentService was created that can be injected\ in context-specific StoredObject voters that need to check whether\ the document in question is attached to a workflow. --- ...panyingCourseDocumentStoredObjectVoter.php | 31 ++++++++++------ .../Service/WorkflowDocumentService.php | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php index db1499a99..931378a61 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php @@ -2,13 +2,15 @@ namespace ChillDocStoreBundle\Security\Authorization; +use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; +use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Security; class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterInterface { @@ -16,15 +18,15 @@ class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterIn public function __construct( private readonly AccompanyingCourseDocumentRepository $repository, - private readonly Security $security, - private readonly AccompanyingCourseDocumentVoter $accompanyingCourseDocumentVoter + private readonly AccompanyingCourseDocumentVoter $accompanyingCourseDocumentVoter, + private readonly WorkflowDocumentService $workflowDocumentService ){ } public function supports(string $attribute, StoredObject $subject): bool { // check if the stored object is linked to an AccompanyingCourseDocument - return !empty($this->repository->findLinkedCourseDocument($subject->getId())); + return $this->repository->findLinkedCourseDocument($subject) instanceof AccompanyingCourseDocument; } public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool @@ -34,17 +36,26 @@ class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterIn } // Retrieve the related accompanying course document - $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject->getId()); + $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject); // Determine the attribute to pass to AccompanyingCourseDocumentVoter - $voterAttribute = ($attribute === self::SEE_AND_EDIT) ? AccompanyingCourseDocumentVoter::UPDATE : AccompanyingCourseDocumentVoter::SEE_DETAILS; + $voterAttribute = match($attribute) { + self::SEE_AND_EDIT => AccompanyingCourseDocumentVoter::UPDATE, + default => AccompanyingCourseDocumentVoter::SEE_DETAILS, + }; // Check access using AccompanyingCourseDocumentVoter - if ($this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { - // TODO implement logic to check for associated workflow - return true; - } else { + if (false === $this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { return false; } + + // Check if entity is related to a workflow, if so, check if user can apply transition + $relatedWorkflow = $this->workflowDocumentService->getRelatedWorkflow($accompanyingCourseDocument); + + if ($relatedWorkflow instanceof EntityWorkflow){ + return $this->workflowDocumentService->canApplyTransition($relatedWorkflow); + } + + return true; } } diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php new file mode 100644 index 000000000..498348b96 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -0,0 +1,35 @@ +repository->findByRelatedEntity(get_class($entity), $entity->getId()); + } + + public function canApplyTransition(EntityWorkflow $entityWorkflow): bool + { + if ($entityWorkflow->isFinal()) { + return false; + } + + $currentUser = $this->security->getUser(); + if ($entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + return true; + } + + return false; + } + +} From 89f5231649c9ae1a76dc513d985d023f9c29ae85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 25 Jun 2024 13:25:49 +0200 Subject: [PATCH 021/375] Refactor PDFSignatureZoneParser to use float values This update changes how we handle values in PDFSignatureZoneParser class. Specifically, we've modified the 'MediaBox' and 'PDFSignatureZone' variables to use float values. The helps ensure greater precision, minimize errors, and maintain data consistency across the application. --- .../Service/Signature/PDFSignatureZoneParser.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php index 3aade2fcc..d57b98abf 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZoneParser.php @@ -39,15 +39,16 @@ class PDFSignatureZoneParser $defaultPageDetails = $defaultPage->getDetails(); foreach ($pdf->getPages() as $index => $page) { + $details = $page->getDetails(); $pdfPage = new PDFPage( $index, - $page['MediaBox'][2] ?? $defaultPageDetails['MediaBox'][2], - $page['MediaBox'][3] ?? $defaultPageDetails['MediaBox'][3], + (float) ($details['MediaBox'][2] ?? $defaultPageDetails['MediaBox'][2]), + (float) ($details['MediaBox'][3] ?? $defaultPageDetails['MediaBox'][3]), ); foreach ($page->getDataTm() as $dataTm) { if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) { - $zones[] = new PDFSignatureZone($dataTm[0][4], $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage); + $zones[] = new PDFSignatureZone((float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage); } } } From 610239930bb7318ecd6f2f19729a33faa1e6b41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 25 Jun 2024 13:43:48 +0200 Subject: [PATCH 022/375] Add serialization groups to PDFPage and PDFSignatureZone properties The Symfony Serializer groups annotation has been added to all properties of the PDFPage and PDFSignatureZone classes. This change allows the serialization and deserialization process to be group-specific, ensuring only relevant data is processed during these operations. --- .../ChillDocStoreBundle/Service/Signature/PDFPage.php | 5 +++++ .../Service/Signature/PDFSignatureZone.php | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php index 138c4e1c8..ffe6bbb17 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php @@ -11,11 +11,16 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Service\Signature; +use Symfony\Component\Serializer\Annotation\Groups; + final readonly class PDFPage { public function __construct( + #[Groups(['read'])] public int $index, + #[Groups(['read'])] public float $width, + #[Groups(['read'])] public float $height, ) {} diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php index 515356e52..bdb4dcd65 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/PDFSignatureZone.php @@ -11,13 +11,20 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Service\Signature; +use Symfony\Component\Serializer\Annotation\Groups; + final readonly class PDFSignatureZone { public function __construct( + #[Groups(['read'])] public float $x, + #[Groups(['read'])] public float $y, + #[Groups(['read'])] public float $height, + #[Groups(['read'])] public float $width, + #[Groups(['read'])] public PDFPage $PDFPage, ) {} From 1310d53589c4fbc0ca5ce6080aee18bcf195cb15 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 13:45:15 +0200 Subject: [PATCH 023/375] Implement context-specific voters for all current entities that can be linked to a document For reusability an AbstractStoredObjectVoter was created and a StoredObjectVoterInterface. A WorkflowDocumentService checks whether the StoredObject is involved in a workflow. --- .../Repository/ActivityRepository.php | 17 ++++- .../AccompanyingCourseDocumentRepository.php | 14 ++-- ...ssociatedEntityToStoredObjectInterface.php | 10 +++ .../Repository/PersonDocumentRepository.php | 13 +++- ...panyingCourseDocumentStoredObjectVoter.php | 61 ----------------- .../Authorization/StoredObjectVoter.php | 8 ++- .../StoredObjectVoterInterface.php | 5 +- .../AbstractStoredObjectVoter.php | 67 +++++++++++++++++++ .../AccompanyingCourseStoredObjectVoter.php | 46 +++++++++++++ ...gPeriodWorkEvaluationStoredObjectVoter.php | 50 ++++++++++++++ .../ActivityStoredObjectVoter.php | 50 ++++++++++++++ .../EventStoredObjectVoter.php | 49 ++++++++++++++ .../PersonStoredObjectVoter.php | 49 ++++++++++++++ .../Service/WorkflowDocumentService.php | 14 ++-- src/Bundle/ChillEventBundle/Entity/Event.php | 2 +- .../Repository/EventRepository.php | 59 ++++++++++++++-- .../Workflow/EntityWorkflowRepository.php | 16 +++++ ...PeriodWorkEvaluationDocumentRepository.php | 14 +++- 18 files changed, 456 insertions(+), 88 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index a7025d4a9..54672c6b7 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Repository; use Chill\ActivityBundle\Entity\Activity; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; @@ -23,7 +25,7 @@ use Doctrine\Persistence\ManagerRegistry; * @method Activity[] findAll() * @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class ActivityRepository extends ServiceEntityRepository +class ActivityRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface { public function __construct(ManagerRegistry $registry) { @@ -97,4 +99,17 @@ class ActivityRepository extends ServiceEntityRepository return $qb->getQuery()->getResult(); } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->createQueryBuilder('a'); + $query = $qb + ->join('a.documents', 'ad') + ->join('ad.storedObject', 'so') + ->where('so.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObject->getId()) + ->getQuery(); + + return $query->getResult(); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 93cf00ecb..246c7f2d9 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -19,7 +19,7 @@ use Doctrine\ORM\EntityRepository; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; -class AccompanyingCourseDocumentRepository implements ObjectRepository +class AccompanyingCourseDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { private readonly EntityRepository $repository; @@ -46,12 +46,12 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } - public function findLinkedCourseDocument(StoredObject $storedObject): ?AccompanyingCourseDocument { - + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { $qb = $this->repository->createQueryBuilder('d'); $query = $qb->where('d.storedObject = :storedObject') - ->setParameter('storedObject', $storedObject) - ->getQuery(); + ->setParameter('storedObject', $storedObject) + ->getQuery(); return $query->getResult(); } @@ -66,7 +66,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $this->repository->findAll(); } - public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null) + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array { return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } @@ -76,7 +76,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $this->findOneBy($criteria); } - public function getClassName() + public function getClassName(): string { return AccompanyingCourseDocument::class; } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php new file mode 100644 index 000000000..5349251c5 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php @@ -0,0 +1,10 @@ + */ -readonly class PersonDocumentRepository implements ObjectRepository +readonly class PersonDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { private EntityRepository $repository; @@ -53,4 +54,14 @@ readonly class PersonDocumentRepository implements ObjectRepository { return PersonDocument::class; } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->repository->createQueryBuilder('d'); + $query = $qb->where('d.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getResult(); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php deleted file mode 100644 index 931378a61..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php +++ /dev/null @@ -1,61 +0,0 @@ -repository->findLinkedCourseDocument($subject) instanceof AccompanyingCourseDocument; - } - - public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool - { - if (!$token->getUser() instanceof User) { - return false; - } - - // Retrieve the related accompanying course document - $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject); - - // Determine the attribute to pass to AccompanyingCourseDocumentVoter - $voterAttribute = match($attribute) { - self::SEE_AND_EDIT => AccompanyingCourseDocumentVoter::UPDATE, - default => AccompanyingCourseDocumentVoter::SEE_DETAILS, - }; - - // Check access using AccompanyingCourseDocumentVoter - if (false === $this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { - return false; - } - - // Check if entity is related to a workflow, if so, check if user can apply transition - $relatedWorkflow = $this->workflowDocumentService->getRelatedWorkflow($accompanyingCourseDocument); - - if ($relatedWorkflow instanceof EntityWorkflow){ - return $this->workflowDocumentService->canApplyTransition($relatedWorkflow); - } - - return true; - } -} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 7e2becab4..3bb5fa396 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -48,15 +48,19 @@ class StoredObjectVoter extends Voter return false; } + $attributeAsEnum = StoredObjectRoleEnum::from($attribute); + // Loop through context-specific voters foreach ($this->storedObjectVoters as $storedObjectVoter) { - if ($storedObjectVoter->supports($attribute, $subject)) { - return $storedObjectVoter->voteOnAttribute($attribute, $subject, $token); + if ($storedObjectVoter->supports($attributeAsEnum, $subject)) { + return $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token); } } // 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; } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index c0fd7e09f..516722654 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -12,12 +12,13 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; interface StoredObjectVoterInterface { - public function supports(string $attribute, StoredObject $subject): bool; + public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool; - public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool; + public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool; } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php new file mode 100644 index 000000000..7ca73f206 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php @@ -0,0 +1,67 @@ +getClass(); + return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class; + } + + public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool + { + if (!$token->getUser() instanceof User) { + return false; + } + + // 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 ($this->canBeAssociatedWithWorkflow()) { + if (null === $this->workflowDocumentService) { + throw new \LogicException("Provide a workflow document service"); + } + + return $this->workflowDocumentService->notBlockedByWorkflow($entity); + } + + return true; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php new file mode 100644 index 000000000..a41ea3067 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php @@ -0,0 +1,46 @@ +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; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php new file mode 100644 index 000000000..423730767 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository; + } + + /** + * @inheritDoc + */ + protected function getClass(): string + { + return AccompanyingPeriodWorkEvaluationDocument::class; + } + + protected function attributeToRole(StoredObjectRoleEnum $attribute): string + { + //Question: there is no update/edit check in AccompanyingPeriodWorkEvaluationDocumentVoter, so for both SEE and EDIT of the + // stored object I check with SEE right in AccompanyingPeriodWorkEvaluationDocumentVoter, correct? + return match ($attribute) { + StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE, + }; + } + + protected function canBeAssociatedWithWorkflow(): bool + { + return true; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php new file mode 100644 index 000000000..a36535005 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository; + } + + /** + * @inheritDoc + */ + 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; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php new file mode 100644 index 000000000..218ce1980 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php @@ -0,0 +1,49 @@ +repository; + } + + /** + * @inheritDoc + */ + protected function getClass(): string + { + return Event::class; + } + + protected function attributeToRole(StoredObjectRoleEnum $attribute): string + { + return match ($attribute) { + StoredObjectRoleEnum::EDIT => EventVoter::UPDATE, + StoredObjectRoleEnum::SEE => EventVoter::SEE_DETAILS, + }; + } + + protected function canBeAssociatedWithWorkflow(): bool + { + return false; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php new file mode 100644 index 000000000..6c3b0b807 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php @@ -0,0 +1,49 @@ +repository; + } + + /** + * @inheritDoc + */ + 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; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index 498348b96..f796fe56e 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -13,19 +13,19 @@ class WorkflowDocumentService { } - public function getRelatedWorkflow($entity): ?EntityWorkflow + public function notBlockedByWorkflow($entity): bool { - return $this->repository->findByRelatedEntity(get_class($entity), $entity->getId()); - } + /** + * @var EntityWorkflow + */ + $workflow = $this->repository->findByRelatedEntity(get_class($entity), $entity->getId()); - public function canApplyTransition(EntityWorkflow $entityWorkflow): bool - { - if ($entityWorkflow->isFinal()) { + if ($workflow->isFinal()) { return false; } $currentUser = $this->security->getUser(); - if ($entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { return true; } diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php index 6f7ee7ef0..be5969363 100644 --- a/src/Bundle/ChillEventBundle/Entity/Event.php +++ b/src/Bundle/ChillEventBundle/Entity/Event.php @@ -31,7 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert; /** * Class Event. */ -#[ORM\Entity(repositoryClass: \Chill\EventBundle\Repository\EventRepository::class)] +#[ORM\Entity] #[ORM\HasLifecycleCallbacks] #[ORM\Table(name: 'chill_event_event')] class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index 7406748b4..24eb095ec 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -11,17 +11,66 @@ declare(strict_types=1); namespace Chill\EventBundle\Repository; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\EventBundle\Entity\Event; -use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; +use Doctrine\Persistence\ObjectRepository; /** * Class EventRepository. */ -class EventRepository extends ServiceEntityRepository +class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { - public function __construct(ManagerRegistry $registry) + private readonly EntityRepository $repository; + + public function __construct(EntityManagerInterface $entityManager) { - parent::__construct($registry, Event::class); + $this->repository = $entityManager->getRepository(Event::class); + } + + public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder + { + return $this->repository->createQueryBuilder($alias, $indexBy); + } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->createQueryBuilder('e'); + $query = $qb + ->join('e.documents', 'ed') + ->join('ed.storedObject', 'so') + ->where('so.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObject->getId()) + ->getQuery(); + + return $query->getResult(); + } + + public function find($id) + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria) + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return Event::class; } } diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php index 66b3ab379..7d2e047d3 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -99,6 +99,22 @@ class EntityWorkflowRepository implements ObjectRepository return $this->repository->findAll(); } + public function findByRelatedEntity($entityClass, $relatedEntityId): ?EntityWorkflow + { + $qb = $this->repository->createQueryBuilder('w'); + + $query = $qb->where( + $qb->expr()->andX( + $qb->expr()->eq('w.relatedEntityClass', ':entity_class'), + $qb->expr()->eq('w.relatedEntityId', ':entity_id'), + ) + )->setParameter('entity_class', $entityClass) + ->setParameter('entity_id', $relatedEntityId); + + return $query->getQuery()->getResult(); + + } + /** * @param mixed|null $limit * @param mixed|null $offset diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 59bb3f915..60dcdf1b1 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -11,12 +11,14 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\AccompanyingPeriod; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\Persistence\ObjectRepository; -class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository +class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { private readonly EntityRepository $repository; @@ -58,4 +60,14 @@ class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectReposi { return AccompanyingPeriodWorkEvaluationDocument::class; } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->repository->createQueryBuilder('ed'); + $query = $qb->where('ed.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getResult(); + } } From bd36735cb16be8e7cd640aaa598383fa4d562a56 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 14:06:02 +0200 Subject: [PATCH 024/375] Ensure single result when retrieving activity and event linked to stored object Although a many-to-many relationship exists between these entities and stored object, only one activity or event will ever be linked to a single stored object. For extra safety measure we return a single result in the repository to ensure our voters will keep working. --- .../ChillActivityBundle/Repository/ActivityRepository.php | 8 +++++++- .../ChillEventBundle/Repository/EventRepository.php | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index 54672c6b7..4d621d56e 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -17,6 +17,8 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; /** @@ -100,6 +102,10 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn return $qb->getQuery()->getResult(); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->createQueryBuilder('a'); @@ -110,6 +116,6 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getResult(); + return $query->getSingleResult(); } } diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index 24eb095ec..b720866f2 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -16,6 +16,8 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\EventBundle\Entity\Event; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; @@ -36,6 +38,10 @@ class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjec return $this->repository->createQueryBuilder($alias, $indexBy); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->createQueryBuilder('e'); @@ -46,7 +52,7 @@ class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjec ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getResult(); + return $query->getSingleResult(); } public function find($id) From d3956319caf28f98e5512f568a4ad138aa7dd223 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 14:56:25 +0200 Subject: [PATCH 025/375] Add test for AccompayingCourseStoredObjectVoter Mainly to check the voteOnAttribute method, by mocking a scenario where a person is allowed to see/edit an AccompanyingCourseDocument and not. --- ...ccompanyingCourseStoredObjectVoterTest.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php new file mode 100644 index 000000000..f7ad25987 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php @@ -0,0 +1,106 @@ +repository = $this->createMock(AccompanyingCourseDocumentRepository::class); + $this->security = $this->createMock(Security::class); + $this->workflowDocumentService = $this->createMock(WorkflowDocumentService::class); + + $this->voter = new AccompanyingCourseStoredObjectVoter( + $this->repository, + $this->security, + $this->workflowDocumentService + ); + } + + private function setupMockObjects(): array + { + $user = $this->createMock(User::class); + $token = $this->createMock(TokenInterface::class); + $subject = $this->createMock(StoredObject::class); + $entity = $this->createMock(AccompanyingCourseDocument::class); + + return [$user, $token, $subject, $entity]; + } + + private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForAccCourseDocument, AccompanyingCourseDocument $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 attributeToRole to return appropriate role + $this->voter->method('attributeToRole')->willReturn(AccompanyingCourseDocumentVoter::SEE_DETAILS); + + // Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument + $this->security->method('isGranted')->willReturnMap([ + [[AccompanyingCourseDocumentVoter::SEE_DETAILS, $entity], $isGrantedForAccCourseDocument], + ]); + + // Mock case where user is blocked or not by workflow + $this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed); + } + + public function testVoteOnAttributeAllowed(): void + { + list($user, $token, $subject, $entity) = $this->setupMockObjects(); + + // Setup mocks for voteOnAttribute method + $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true); + + // The voteOnAttribute method should return True when workflow is allowed + $attributeSee = StoredObjectRoleEnum::SEE; + $attributeEdit = StoredObjectRoleEnum::EDIT; + $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $subject, $token)); + } + + public function testVoteOnAttributeNotAllowed(): void + { + list($user, $token, $subject, $entity) = $this->setupMockObjects(); + + // Setup mocks for voteOnAttribute method where isGranted() returns false + $this->setupMocksForVoteOnAttribute($user, $token, false, $entity, true); + + // The voteOnAttribute method should return True when workflow is allowed + $attributeSee = StoredObjectRoleEnum::SEE; + $attributeEdit = StoredObjectRoleEnum::EDIT; + $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $subject, $token)); + } + + public function testVoteOnAttributeWhenBlockedByWorkflow(): void + { + list($user, $token, $subject, $entity) = $this->setupMockObjects(); + + // Setup mocks for voteOnAttribute method + $this->setupMocksForVoteOnAttribute($user, $token, $subject, $entity, false); + + // Test voteOnAttribute method + $attribute = StoredObjectRoleEnum::SEE; + $result = $this->voter->voteOnAttribute($attribute, $subject, $token); + + // Assert that access is denied when workflow is not allowed + $this->assertFalse($result); + } +} From 3a87513a114ded794c6e0302fe5e34851bf3b687 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 15:35:50 +0200 Subject: [PATCH 026/375] initial commit --- .changes/unreleased/Feature-20240614-153537.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changes/unreleased/Feature-20240614-153537.yaml diff --git a/.changes/unreleased/Feature-20240614-153537.yaml b/.changes/unreleased/Feature-20240614-153537.yaml new file mode 100644 index 000000000..c16d49b2d --- /dev/null +++ b/.changes/unreleased/Feature-20240614-153537.yaml @@ -0,0 +1,7 @@ +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" From 2d09efb2e0b88a2ed43b19deba58ca52446f971c Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 16:48:09 +0200 Subject: [PATCH 027/375] Add StoredObjectVoterInterface An interface is defined that can be implemented by each context-specific voter in the future. --- .../StoredObjectVoterInterface.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php new file mode 100644 index 000000000..47b93eabb --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -0,0 +1,22 @@ + Date: Fri, 14 Jun 2024 17:22:27 +0200 Subject: [PATCH 028/375] Refactorize StoredObjectVoter.php The StoredObjectVoter.php has been refactorized to handle context-specific voters.\ This way we can check if the context-specific voter should handle the authorization or not.\ If not, there is a simple fallback to check on the USER_ROLE. --- .../Authorization/StoredObjectVoter.php | 28 ++++++++++++++----- .../config/services/voter.yaml | 14 ++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/config/services/voter.yaml diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 2e253cf3c..ecfc56615 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\Security; /** * Voter for the content of a stored object. @@ -23,6 +24,14 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; */ class StoredObjectVoter extends Voter { + private $security; + private $storedObjectVoters; + + public function __construct(Security $security, iterable $storedObjectVoters) { + $this->security = $security; + $this->storedObjectVoters = $storedObjectVoters; + } + protected function supports($attribute, $subject): bool { return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum @@ -43,13 +52,18 @@ class StoredObjectVoter extends Voter return false; } - $askedRole = StoredObjectRoleEnum::from($attribute); - $tokenRoleAuthorization = - $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS); + // Loop through context-specific voters + foreach ($this->storedObjectVoters as $storedObjectVoter) { + if ($storedObjectVoter->supports($attribute, $subject)) { + return $storedObjectVoter->voteOnAttribute($attribute, $subject, $token); + } + } - return match ($askedRole) { - StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization, - StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization - }; + // User role-based fallback + if ($this->security->isGranted('ROLE_USER')) { + return true; + } + + return false; } } diff --git a/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml b/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml new file mode 100644 index 000000000..922d29cba --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml @@ -0,0 +1,14 @@ +services: + _defaults: + autowire: true + autoconfigure: true + Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter: + arguments: + $storedObjectVoters: + # context specific voters + - '@accompanying_course_document_voter' + tags: + - { name: security.voter } + + accompanying_course_document_voter: + class: Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter From aaac80be8439fcbbbd788bd5840bc9f7664bec99 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 17:27:22 +0200 Subject: [PATCH 029/375] Add config voter.yaml The voter.yaml was not configured to be taken into account. Now added\ to ChillDocStoreExtension.php --- .../DependencyInjection/ChillDocStoreExtension.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index fe9aeecfa..5f87199a3 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -42,6 +42,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf $loader->load('services/fixtures.yaml'); $loader->load('services/form.yaml'); $loader->load('services/templating.yaml'); + $loader->load('services/voter.yaml'); } public function prepend(ContainerBuilder $container) From 30078db841bc9402f464278cd45280c159d98886 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 09:51:21 +0200 Subject: [PATCH 030/375] Type-hint $subject in StoredObjectVoterInterface.php Since the subject passed to these voters should\ always be of the type StoredObject, type-hinting\ added. --- .../Security/Authorization/StoredObjectVoterInterface.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index 47b93eabb..a3a76e79a 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -11,12 +11,13 @@ declare(strict_types=1); namespace ChillDocStoreBundle\Security\Authorization; +use Chill\DocStoreBundle\Entity\StoredObject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; interface StoredObjectVoterInterface { - public function supports(string $attribute, $subject): bool; + public function supports(string $attribute, StoredObject $subject): bool; - public function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool; + public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool; } From 26b3d84d624e39ffe21b8d8c394aa64d1a1c9a12 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 09:52:59 +0200 Subject: [PATCH 031/375] Add fall-back right for ROLE_ADMIN Within the StoredObjectVoter.php also the admin\ user should be able to edit documents as a fall-back --- .../Security/Authorization/StoredObjectVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index ecfc56615..781c2c542 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -60,7 +60,7 @@ class StoredObjectVoter extends Voter } // User role-based fallback - if ($this->security->isGranted('ROLE_USER')) { + if ($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN')) { return true; } From 2ce9810243f07f3bcf7b663096825db1ab752cbe Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:00:10 +0200 Subject: [PATCH 032/375] Use constructor property promotion In accordance with php8.1 use property promotion\ within the constructor method. Less clutter. --- .../Security/Authorization/StoredObjectVoter.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 781c2c542..7e2becab4 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -24,12 +24,8 @@ use Symfony\Component\Security\Core\Security; */ class StoredObjectVoter extends Voter { - private $security; - private $storedObjectVoters; - public function __construct(Security $security, iterable $storedObjectVoters) { - $this->security = $security; - $this->storedObjectVoters = $storedObjectVoters; + public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters) { } protected function supports($attribute, $subject): bool From 830dace1ba2a021434ca4bd94126dff838f839bd Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:02:25 +0200 Subject: [PATCH 033/375] Rename voter.yaml file to security.yaml For consistency with other bundles voters are\ registered under the security.yaml file. --- .../DependencyInjection/ChillDocStoreExtension.php | 2 +- .../config/services/{voter.yaml => security.yaml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Bundle/ChillDocStoreBundle/config/services/{voter.yaml => security.yaml} (100%) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index 5f87199a3..a0fcc2d5d 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -42,7 +42,7 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf $loader->load('services/fixtures.yaml'); $loader->load('services/form.yaml'); $loader->load('services/templating.yaml'); - $loader->load('services/voter.yaml'); + $loader->load('services/security.yaml'); } public function prepend(ContainerBuilder $container) diff --git a/src/Bundle/ChillDocStoreBundle/config/services/voter.yaml b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml similarity index 100% rename from src/Bundle/ChillDocStoreBundle/config/services/voter.yaml rename to src/Bundle/ChillDocStoreBundle/config/services/security.yaml From 062afd669596f5fb8cbfb5383bce8b2ed7c36c69 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:16:39 +0200 Subject: [PATCH 034/375] Use service tags to inject all voters into StoredObjectVoter.php Instead of manually injecting services into StoredObjectVoter\ config is added to automatically tag each service that implements\ StoredObjectVoterInterface.php --- .../DependencyInjection/ChillDocStoreExtension.php | 3 +++ .../ChillDocStoreBundle/config/services/security.yaml | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index a0fcc2d5d..9f277e716 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\DependencyInjection; use Chill\DocStoreBundle\Controller\StoredObjectApiController; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; +use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -35,6 +36,8 @@ class ChillDocStoreExtension extends Extension implements PrependExtensionInterf $container->setParameter('chill_doc_store', $config); + $container->registerForAutoconfiguration(StoredObjectVoterInterface::class)->addTag('stored_object_voter'); + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader->load('services.yaml'); $loader->load('services/controller.yaml'); diff --git a/src/Bundle/ChillDocStoreBundle/config/services/security.yaml b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml index 922d29cba..c57eb63c5 100644 --- a/src/Bundle/ChillDocStoreBundle/config/services/security.yaml +++ b/src/Bundle/ChillDocStoreBundle/config/services/security.yaml @@ -4,11 +4,10 @@ services: autoconfigure: true Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter: arguments: - $storedObjectVoters: - # context specific voters - - '@accompanying_course_document_voter' + $storedObjectVoters: !tagged_iterator stored_object_voter tags: - { name: security.voter } - accompanying_course_document_voter: - class: Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter + Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter: + tags: + - { name: security.voter } From f75c7a0232dda4b410c96a95652aa0e8594c675f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jun 2024 10:21:24 +0200 Subject: [PATCH 035/375] Implement StoredObjectVoterInterface An interface was created to be implemented by Stored Doc voters\ these will automatically be tagged and injected into DocStoreVoter. --- .../Authorization/AccompanyingCourseDocumentVoter.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index 5febc7e42..93243b1f4 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -19,11 +19,12 @@ use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; +use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; -class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface +class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface, StoredObjectVoterInterface { final public const CREATE = 'CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE'; @@ -70,12 +71,12 @@ class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements Prov return []; } - protected function supports($attribute, $subject): bool + public function supports($attribute, $subject): bool { return $this->voterHelper->supports($attribute, $subject); } - protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool + public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool { if (!$token->getUser() instanceof User) { return false; From d9892f6822fff3fbc3526fd988e6ad2f73bc2a61 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 10:53:33 +0200 Subject: [PATCH 036/375] Correct namespace and use statement for StoredObjectVoterInterface.php The namespace was formed wrong and needed adjustment --- .../Security/Authorization/AccompanyingCourseDocumentVoter.php | 1 - .../Security/Authorization/StoredObjectVoterInterface.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index 93243b1f4..dac08b05b 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -19,7 +19,6 @@ use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; -use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index a3a76e79a..c0fd7e09f 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace ChillDocStoreBundle\Security\Authorization; +namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; From e54633d14dd75ac3e596a5a78334a4277bee478f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 15:17:56 +0200 Subject: [PATCH 037/375] Implement voting logic: separation of concerns A separate AccompanyingCourseDocumentStoredObjectVoter was\ created to handle the specific access to a Stored object\ related to an Accompanying Course Document. --- .../AccompanyingCourseDocumentRepository.php | 13 +++++ ...panyingCourseDocumentStoredObjectVoter.php | 50 +++++++++++++++++++ ...nyingPeriodWorkEvaluationDocumentVoter.php | 7 +-- 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 2679993c4..239b88b90 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Repository; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; +use Chill\DocStoreBundle\Entity\StoredObject; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; @@ -45,6 +46,17 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } + public function findLinkedCourseDocument(int $storedObjectId): ?AccompanyingCourseDocument { + + $qb = $this->repository->createQueryBuilder('d'); + $query = $qb->leftJoin('d.storedObject', 'do') + ->where('do.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObjectId) + ->getQuery(); + + return $query->getResult(); + } + public function find($id): ?AccompanyingCourseDocument { return $this->repository->find($id); @@ -69,4 +81,5 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository { return AccompanyingCourseDocument::class; } + } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php new file mode 100644 index 000000000..db1499a99 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository->findLinkedCourseDocument($subject->getId())); + } + + public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool + { + if (!$token->getUser() instanceof User) { + return false; + } + + // Retrieve the related accompanying course document + $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject->getId()); + + // Determine the attribute to pass to AccompanyingCourseDocumentVoter + $voterAttribute = ($attribute === self::SEE_AND_EDIT) ? AccompanyingCourseDocumentVoter::UPDATE : AccompanyingCourseDocumentVoter::SEE_DETAILS; + + // Check access using AccompanyingCourseDocumentVoter + if ($this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { + // TODO implement logic to check for associated workflow + return true; + } else { + return false; + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php index 77c131cd9..8f4a1b995 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Security\Authorization; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; @@ -21,13 +22,13 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; * * Delegates to the sames authorization than for Evalution */ -class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter +class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter implements StoredObjectVoterInterface { final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW'; public function __construct(private readonly AccessDecisionManagerInterface $accessDecisionManager) {} - protected function supports($attribute, $subject) + public function supports($attribute, $subject): bool { return $subject instanceof AccompanyingPeriodWorkEvaluationDocument && self::SEE === $attribute; @@ -39,7 +40,7 @@ class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter * * @return bool|void */ - protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool + public function voteOnAttribute($attribute, $subject, TokenInterface $token): bool { return match ($attribute) { self::SEE => $this->accessDecisionManager->decide( From cce04ee4900636bd9b92f3c63d713d0238854390 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:27:21 +0200 Subject: [PATCH 038/375] Remove implementation of StoredObjectVoterInterface in AccompanyingCourseDocumentVoter.php This implementation has been moved to the voter\ AccompanyingCourseDocumentStoredObjectVoter.php --- .../Security/Authorization/AccompanyingCourseDocumentVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php index dac08b05b..7a46184cb 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentVoter.php @@ -23,7 +23,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; -class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface, StoredObjectVoterInterface +class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface { final public const CREATE = 'CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE'; From 7c03a25f1a8fb37cdddf1986bb0271af23fd5b52 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:28:19 +0200 Subject: [PATCH 039/375] Refactor AccompanyingCourseDocumentRepository.php Build where clause using StoredObject directly instead\ of based on it's id. --- .../Repository/AccompanyingCourseDocumentRepository.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 239b88b90..93cf00ecb 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -46,12 +46,11 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } - public function findLinkedCourseDocument(int $storedObjectId): ?AccompanyingCourseDocument { + public function findLinkedCourseDocument(StoredObject $storedObject): ?AccompanyingCourseDocument { $qb = $this->repository->createQueryBuilder('d'); - $query = $qb->leftJoin('d.storedObject', 'do') - ->where('do.id = :storedObjectId') - ->setParameter('storedObjectId', $storedObjectId) + $query = $qb->where('d.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) ->getQuery(); return $query->getResult(); From 4607c36b574916aa148e7973e15c4f51074cb315 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 20 Jun 2024 17:32:09 +0200 Subject: [PATCH 040/375] Add WorkflowDocumentService and use in StoredObject voters A WorkflowDocumentService was created that can be injected\ in context-specific StoredObject voters that need to check whether\ the document in question is attached to a workflow. --- ...panyingCourseDocumentStoredObjectVoter.php | 31 ++++++++++------ .../Service/WorkflowDocumentService.php | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php index db1499a99..931378a61 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php @@ -2,13 +2,15 @@ namespace ChillDocStoreBundle\Security\Authorization; +use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; +use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Security; class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterInterface { @@ -16,15 +18,15 @@ class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterIn public function __construct( private readonly AccompanyingCourseDocumentRepository $repository, - private readonly Security $security, - private readonly AccompanyingCourseDocumentVoter $accompanyingCourseDocumentVoter + private readonly AccompanyingCourseDocumentVoter $accompanyingCourseDocumentVoter, + private readonly WorkflowDocumentService $workflowDocumentService ){ } public function supports(string $attribute, StoredObject $subject): bool { // check if the stored object is linked to an AccompanyingCourseDocument - return !empty($this->repository->findLinkedCourseDocument($subject->getId())); + return $this->repository->findLinkedCourseDocument($subject) instanceof AccompanyingCourseDocument; } public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool @@ -34,17 +36,26 @@ class AccompanyingCourseDocumentStoredObjectVoter implements StoredObjectVoterIn } // Retrieve the related accompanying course document - $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject->getId()); + $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject); // Determine the attribute to pass to AccompanyingCourseDocumentVoter - $voterAttribute = ($attribute === self::SEE_AND_EDIT) ? AccompanyingCourseDocumentVoter::UPDATE : AccompanyingCourseDocumentVoter::SEE_DETAILS; + $voterAttribute = match($attribute) { + self::SEE_AND_EDIT => AccompanyingCourseDocumentVoter::UPDATE, + default => AccompanyingCourseDocumentVoter::SEE_DETAILS, + }; // Check access using AccompanyingCourseDocumentVoter - if ($this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { - // TODO implement logic to check for associated workflow - return true; - } else { + if (false === $this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { return false; } + + // Check if entity is related to a workflow, if so, check if user can apply transition + $relatedWorkflow = $this->workflowDocumentService->getRelatedWorkflow($accompanyingCourseDocument); + + if ($relatedWorkflow instanceof EntityWorkflow){ + return $this->workflowDocumentService->canApplyTransition($relatedWorkflow); + } + + return true; } } diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php new file mode 100644 index 000000000..498348b96 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -0,0 +1,35 @@ +repository->findByRelatedEntity(get_class($entity), $entity->getId()); + } + + public function canApplyTransition(EntityWorkflow $entityWorkflow): bool + { + if ($entityWorkflow->isFinal()) { + return false; + } + + $currentUser = $this->security->getUser(); + if ($entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + return true; + } + + return false; + } + +} From c06e76a0ee8e2812f9efa9746e168b6b5239c95f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 13:45:15 +0200 Subject: [PATCH 041/375] Implement context-specific voters for all current entities that can be linked to a document For reusability an AbstractStoredObjectVoter was created and a StoredObjectVoterInterface. A WorkflowDocumentService checks whether the StoredObject is involved in a workflow. --- .../Repository/ActivityRepository.php | 17 ++++- .../AccompanyingCourseDocumentRepository.php | 14 ++-- ...ssociatedEntityToStoredObjectInterface.php | 10 +++ .../Repository/PersonDocumentRepository.php | 13 +++- ...panyingCourseDocumentStoredObjectVoter.php | 61 ----------------- .../Authorization/StoredObjectVoter.php | 8 ++- .../StoredObjectVoterInterface.php | 5 +- .../AbstractStoredObjectVoter.php | 67 +++++++++++++++++++ .../AccompanyingCourseStoredObjectVoter.php | 46 +++++++++++++ ...gPeriodWorkEvaluationStoredObjectVoter.php | 50 ++++++++++++++ .../ActivityStoredObjectVoter.php | 50 ++++++++++++++ .../EventStoredObjectVoter.php | 49 ++++++++++++++ .../PersonStoredObjectVoter.php | 49 ++++++++++++++ .../Service/WorkflowDocumentService.php | 14 ++-- src/Bundle/ChillEventBundle/Entity/Event.php | 2 +- .../Repository/EventRepository.php | 59 ++++++++++++++-- .../Workflow/EntityWorkflowRepository.php | 16 +++++ ...PeriodWorkEvaluationDocumentRepository.php | 14 +++- 18 files changed, 456 insertions(+), 88 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php create mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index a7025d4a9..54672c6b7 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Repository; use Chill\ActivityBundle\Entity\Activity; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; @@ -23,7 +25,7 @@ use Doctrine\Persistence\ManagerRegistry; * @method Activity[] findAll() * @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class ActivityRepository extends ServiceEntityRepository +class ActivityRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface { public function __construct(ManagerRegistry $registry) { @@ -97,4 +99,17 @@ class ActivityRepository extends ServiceEntityRepository return $qb->getQuery()->getResult(); } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->createQueryBuilder('a'); + $query = $qb + ->join('a.documents', 'ad') + ->join('ad.storedObject', 'so') + ->where('so.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObject->getId()) + ->getQuery(); + + return $query->getResult(); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 93cf00ecb..246c7f2d9 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -19,7 +19,7 @@ use Doctrine\ORM\EntityRepository; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; -class AccompanyingCourseDocumentRepository implements ObjectRepository +class AccompanyingCourseDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { private readonly EntityRepository $repository; @@ -46,12 +46,12 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } - public function findLinkedCourseDocument(StoredObject $storedObject): ?AccompanyingCourseDocument { - + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { $qb = $this->repository->createQueryBuilder('d'); $query = $qb->where('d.storedObject = :storedObject') - ->setParameter('storedObject', $storedObject) - ->getQuery(); + ->setParameter('storedObject', $storedObject) + ->getQuery(); return $query->getResult(); } @@ -66,7 +66,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $this->repository->findAll(); } - public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null) + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array { return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } @@ -76,7 +76,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository return $this->findOneBy($criteria); } - public function getClassName() + public function getClassName(): string { return AccompanyingCourseDocument::class; } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php new file mode 100644 index 000000000..5349251c5 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php @@ -0,0 +1,10 @@ + */ -readonly class PersonDocumentRepository implements ObjectRepository +readonly class PersonDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { private EntityRepository $repository; @@ -53,4 +54,14 @@ readonly class PersonDocumentRepository implements ObjectRepository { return PersonDocument::class; } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->repository->createQueryBuilder('d'); + $query = $qb->where('d.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getResult(); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php deleted file mode 100644 index 931378a61..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AccompanyingCourseDocumentStoredObjectVoter.php +++ /dev/null @@ -1,61 +0,0 @@ -repository->findLinkedCourseDocument($subject) instanceof AccompanyingCourseDocument; - } - - public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool - { - if (!$token->getUser() instanceof User) { - return false; - } - - // Retrieve the related accompanying course document - $accompanyingCourseDocument = $this->repository->findLinkedCourseDocument($subject); - - // Determine the attribute to pass to AccompanyingCourseDocumentVoter - $voterAttribute = match($attribute) { - self::SEE_AND_EDIT => AccompanyingCourseDocumentVoter::UPDATE, - default => AccompanyingCourseDocumentVoter::SEE_DETAILS, - }; - - // Check access using AccompanyingCourseDocumentVoter - if (false === $this->accompanyingCourseDocumentVoter->voteOnAttribute($voterAttribute, $accompanyingCourseDocument, $token)) { - return false; - } - - // Check if entity is related to a workflow, if so, check if user can apply transition - $relatedWorkflow = $this->workflowDocumentService->getRelatedWorkflow($accompanyingCourseDocument); - - if ($relatedWorkflow instanceof EntityWorkflow){ - return $this->workflowDocumentService->canApplyTransition($relatedWorkflow); - } - - return true; - } -} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 7e2becab4..3bb5fa396 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -48,15 +48,19 @@ class StoredObjectVoter extends Voter return false; } + $attributeAsEnum = StoredObjectRoleEnum::from($attribute); + // Loop through context-specific voters foreach ($this->storedObjectVoters as $storedObjectVoter) { - if ($storedObjectVoter->supports($attribute, $subject)) { - return $storedObjectVoter->voteOnAttribute($attribute, $subject, $token); + if ($storedObjectVoter->supports($attributeAsEnum, $subject)) { + return $storedObjectVoter->voteOnAttribute($attributeAsEnum, $subject, $token); } } // 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; } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index c0fd7e09f..516722654 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -12,12 +12,13 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; interface StoredObjectVoterInterface { - public function supports(string $attribute, StoredObject $subject): bool; + public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool; - public function voteOnAttribute(string $attribute, StoredObject $subject, TokenInterface $token): bool; + public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool; } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php new file mode 100644 index 000000000..7ca73f206 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php @@ -0,0 +1,67 @@ +getClass(); + return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class; + } + + public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool + { + if (!$token->getUser() instanceof User) { + return false; + } + + // 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 ($this->canBeAssociatedWithWorkflow()) { + if (null === $this->workflowDocumentService) { + throw new \LogicException("Provide a workflow document service"); + } + + return $this->workflowDocumentService->notBlockedByWorkflow($entity); + } + + return true; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php new file mode 100644 index 000000000..a41ea3067 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php @@ -0,0 +1,46 @@ +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; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php new file mode 100644 index 000000000..423730767 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository; + } + + /** + * @inheritDoc + */ + protected function getClass(): string + { + return AccompanyingPeriodWorkEvaluationDocument::class; + } + + protected function attributeToRole(StoredObjectRoleEnum $attribute): string + { + //Question: there is no update/edit check in AccompanyingPeriodWorkEvaluationDocumentVoter, so for both SEE and EDIT of the + // stored object I check with SEE right in AccompanyingPeriodWorkEvaluationDocumentVoter, correct? + return match ($attribute) { + StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE, + }; + } + + protected function canBeAssociatedWithWorkflow(): bool + { + return true; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php new file mode 100644 index 000000000..a36535005 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php @@ -0,0 +1,50 @@ +repository; + } + + /** + * @inheritDoc + */ + 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; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php new file mode 100644 index 000000000..218ce1980 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php @@ -0,0 +1,49 @@ +repository; + } + + /** + * @inheritDoc + */ + protected function getClass(): string + { + return Event::class; + } + + protected function attributeToRole(StoredObjectRoleEnum $attribute): string + { + return match ($attribute) { + StoredObjectRoleEnum::EDIT => EventVoter::UPDATE, + StoredObjectRoleEnum::SEE => EventVoter::SEE_DETAILS, + }; + } + + protected function canBeAssociatedWithWorkflow(): bool + { + return false; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php new file mode 100644 index 000000000..6c3b0b807 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php @@ -0,0 +1,49 @@ +repository; + } + + /** + * @inheritDoc + */ + 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; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index 498348b96..f796fe56e 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -13,19 +13,19 @@ class WorkflowDocumentService { } - public function getRelatedWorkflow($entity): ?EntityWorkflow + public function notBlockedByWorkflow($entity): bool { - return $this->repository->findByRelatedEntity(get_class($entity), $entity->getId()); - } + /** + * @var EntityWorkflow + */ + $workflow = $this->repository->findByRelatedEntity(get_class($entity), $entity->getId()); - public function canApplyTransition(EntityWorkflow $entityWorkflow): bool - { - if ($entityWorkflow->isFinal()) { + if ($workflow->isFinal()) { return false; } $currentUser = $this->security->getUser(); - if ($entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { return true; } diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php index 6f7ee7ef0..be5969363 100644 --- a/src/Bundle/ChillEventBundle/Entity/Event.php +++ b/src/Bundle/ChillEventBundle/Entity/Event.php @@ -31,7 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert; /** * Class Event. */ -#[ORM\Entity(repositoryClass: \Chill\EventBundle\Repository\EventRepository::class)] +#[ORM\Entity] #[ORM\HasLifecycleCallbacks] #[ORM\Table(name: 'chill_event_event')] class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index 7406748b4..24eb095ec 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -11,17 +11,66 @@ declare(strict_types=1); namespace Chill\EventBundle\Repository; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\EventBundle\Entity\Event; -use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; +use Doctrine\Persistence\ObjectRepository; /** * Class EventRepository. */ -class EventRepository extends ServiceEntityRepository +class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { - public function __construct(ManagerRegistry $registry) + private readonly EntityRepository $repository; + + public function __construct(EntityManagerInterface $entityManager) { - parent::__construct($registry, Event::class); + $this->repository = $entityManager->getRepository(Event::class); + } + + public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder + { + return $this->repository->createQueryBuilder($alias, $indexBy); + } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->createQueryBuilder('e'); + $query = $qb + ->join('e.documents', 'ed') + ->join('ed.storedObject', 'so') + ->where('so.id = :storedObjectId') + ->setParameter('storedObjectId', $storedObject->getId()) + ->getQuery(); + + return $query->getResult(); + } + + public function find($id) + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria) + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return Event::class; } } diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php index 66b3ab379..7d2e047d3 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -99,6 +99,22 @@ class EntityWorkflowRepository implements ObjectRepository return $this->repository->findAll(); } + public function findByRelatedEntity($entityClass, $relatedEntityId): ?EntityWorkflow + { + $qb = $this->repository->createQueryBuilder('w'); + + $query = $qb->where( + $qb->expr()->andX( + $qb->expr()->eq('w.relatedEntityClass', ':entity_class'), + $qb->expr()->eq('w.relatedEntityId', ':entity_id'), + ) + )->setParameter('entity_class', $entityClass) + ->setParameter('entity_id', $relatedEntityId); + + return $query->getQuery()->getResult(); + + } + /** * @param mixed|null $limit * @param mixed|null $offset diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 59bb3f915..60dcdf1b1 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -11,12 +11,14 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\AccompanyingPeriod; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\Persistence\ObjectRepository; -class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository +class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { private readonly EntityRepository $repository; @@ -58,4 +60,14 @@ class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectReposi { return AccompanyingPeriodWorkEvaluationDocument::class; } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + $qb = $this->repository->createQueryBuilder('ed'); + $query = $qb->where('ed.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getResult(); + } } From a25f2c7539aeb1f9261ff2f22f75c1f5e591e60b Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 14:06:02 +0200 Subject: [PATCH 042/375] Ensure single result when retrieving activity and event linked to stored object Although a many-to-many relationship exists between these entities and stored object, only one activity or event will ever be linked to a single stored object. For extra safety measure we return a single result in the repository to ensure our voters will keep working. --- .../ChillActivityBundle/Repository/ActivityRepository.php | 8 +++++++- .../ChillEventBundle/Repository/EventRepository.php | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index 54672c6b7..4d621d56e 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -17,6 +17,8 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; /** @@ -100,6 +102,10 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn return $qb->getQuery()->getResult(); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->createQueryBuilder('a'); @@ -110,6 +116,6 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getResult(); + return $query->getSingleResult(); } } diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index 24eb095ec..b720866f2 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -16,6 +16,8 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\EventBundle\Entity\Event; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; @@ -36,6 +38,10 @@ class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjec return $this->repository->createQueryBuilder($alias, $indexBy); } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->createQueryBuilder('e'); @@ -46,7 +52,7 @@ class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjec ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getResult(); + return $query->getSingleResult(); } public function find($id) From bab6528ed6e7f85471cead28f639ee55088ac359 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 26 Jun 2024 14:56:25 +0200 Subject: [PATCH 043/375] Add test for AccompayingCourseStoredObjectVoter Mainly to check the voteOnAttribute method, by mocking a scenario where a person is allowed to see/edit an AccompanyingCourseDocument and not. --- ...ccompanyingCourseStoredObjectVoterTest.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php new file mode 100644 index 000000000..f7ad25987 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php @@ -0,0 +1,106 @@ +repository = $this->createMock(AccompanyingCourseDocumentRepository::class); + $this->security = $this->createMock(Security::class); + $this->workflowDocumentService = $this->createMock(WorkflowDocumentService::class); + + $this->voter = new AccompanyingCourseStoredObjectVoter( + $this->repository, + $this->security, + $this->workflowDocumentService + ); + } + + private function setupMockObjects(): array + { + $user = $this->createMock(User::class); + $token = $this->createMock(TokenInterface::class); + $subject = $this->createMock(StoredObject::class); + $entity = $this->createMock(AccompanyingCourseDocument::class); + + return [$user, $token, $subject, $entity]; + } + + private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForAccCourseDocument, AccompanyingCourseDocument $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 attributeToRole to return appropriate role + $this->voter->method('attributeToRole')->willReturn(AccompanyingCourseDocumentVoter::SEE_DETAILS); + + // Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument + $this->security->method('isGranted')->willReturnMap([ + [[AccompanyingCourseDocumentVoter::SEE_DETAILS, $entity], $isGrantedForAccCourseDocument], + ]); + + // Mock case where user is blocked or not by workflow + $this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed); + } + + public function testVoteOnAttributeAllowed(): void + { + list($user, $token, $subject, $entity) = $this->setupMockObjects(); + + // Setup mocks for voteOnAttribute method + $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true); + + // The voteOnAttribute method should return True when workflow is allowed + $attributeSee = StoredObjectRoleEnum::SEE; + $attributeEdit = StoredObjectRoleEnum::EDIT; + $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $subject, $token)); + } + + public function testVoteOnAttributeNotAllowed(): void + { + list($user, $token, $subject, $entity) = $this->setupMockObjects(); + + // Setup mocks for voteOnAttribute method where isGranted() returns false + $this->setupMocksForVoteOnAttribute($user, $token, false, $entity, true); + + // The voteOnAttribute method should return True when workflow is allowed + $attributeSee = StoredObjectRoleEnum::SEE; + $attributeEdit = StoredObjectRoleEnum::EDIT; + $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $subject, $token)); + } + + public function testVoteOnAttributeWhenBlockedByWorkflow(): void + { + list($user, $token, $subject, $entity) = $this->setupMockObjects(); + + // Setup mocks for voteOnAttribute method + $this->setupMocksForVoteOnAttribute($user, $token, $subject, $entity, false); + + // Test voteOnAttribute method + $attribute = StoredObjectRoleEnum::SEE; + $result = $this->voter->voteOnAttribute($attribute, $subject, $token); + + // Assert that access is denied when workflow is not allowed + $this->assertFalse($result); + } +} From 742f2540f69685aaa4ce1c967bc668e3e397570d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 27 Jun 2024 11:59:31 +0200 Subject: [PATCH 044/375] Setup AccompanyingPeriodWorkEvaluationStoredObjectVoter.php to use AccompanyingPeriodWorkRepository.php The voter was not checking the correct permissions to\ establish whether a user can see/edit a storedObject\ The right to see/edit an AccompanyingPeriodWork has to\ be checked. --- ...ingPeriodWorkEvaluationStoredObjectVoter.php | 15 +++++++-------- ...ngPeriodWorkEvaluationDocumentRepository.php | 11 +---------- .../AccompanyingPeriodWorkRepository.php | 17 ++++++++++++++++- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php index 423730767..72e422776 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php @@ -6,15 +6,15 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; -use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; -use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; -use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter; +use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; +use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter; use Symfony\Component\Security\Core\Security; class AccompanyingPeriodWorkEvaluationStoredObjectVoter extends AbstractStoredObjectVoter { public function __construct( - private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, + private readonly AccompanyingPeriodWorkRepository $repository, Security $security, WorkflowDocumentService $workflowDocumentService ){ @@ -31,15 +31,14 @@ class AccompanyingPeriodWorkEvaluationStoredObjectVoter extends AbstractStoredOb */ protected function getClass(): string { - return AccompanyingPeriodWorkEvaluationDocument::class; + return AccompanyingPeriodWork::class; } protected function attributeToRole(StoredObjectRoleEnum $attribute): string { - //Question: there is no update/edit check in AccompanyingPeriodWorkEvaluationDocumentVoter, so for both SEE and EDIT of the - // stored object I check with SEE right in AccompanyingPeriodWorkEvaluationDocumentVoter, correct? return match ($attribute) { - StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE, + StoredObjectRoleEnum::SEE => AccompanyingPeriodWorkVoter::SEE, + StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkVoter::UPDATE, }; } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 60dcdf1b1..36de5ce3e 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -18,7 +18,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\Persistence\ObjectRepository; -class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface +class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository { private readonly EntityRepository $repository; @@ -61,13 +61,4 @@ class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectReposi return AccompanyingPeriodWorkEvaluationDocument::class; } - public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object - { - $qb = $this->repository->createQueryBuilder('ed'); - $query = $qb->where('ed.storedObject = :storedObject') - ->setParameter('storedObject', $storedObject) - ->getQuery(); - - return $query->getResult(); - } } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index 95b995e74..43ecd5337 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -11,6 +11,8 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\AccompanyingPeriod; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; @@ -22,7 +24,7 @@ use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; -final readonly class AccompanyingPeriodWorkRepository implements ObjectRepository +final readonly class AccompanyingPeriodWorkRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { private EntityRepository $repository; @@ -251,4 +253,17 @@ final readonly class AccompanyingPeriodWorkRepository implements ObjectRepositor return $qb; } + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?AccompanyingPeriodWork + { + $qb = $this->repository->createQueryBuilder('acpw'); + $query = $qb + ->join('acpw.evaluations', 'acpwe') + ->join('acpwe.documents', 'acpwed') + ->where('acpwed.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getResult(); + } } From efaad1981d2742c8e99fc27f9d51a6e641e4e498 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 27 Jun 2024 12:44:36 +0200 Subject: [PATCH 045/375] Fix namespaces and move voters to corresponding bundles --- .../ChillActivityBundle/Repository/ActivityRepository.php | 2 +- .../Security/Authorization}/ActivityStoredObjectVoter.php | 2 +- ...er.php => AccompanyingCourseDocumentStoredObjectVoter.php} | 4 ++-- .../AccompanyingPeriodWorkEvaluationStoredObjectVoter.php | 3 +-- ...redObjectVoter.php => PersonDocumentStoredObjectVoter.php} | 4 ++-- .../Security/Authorization}/EventStoredObjectVoter.php | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) rename src/Bundle/{ChillDocStoreBundle/Security/Authorization/StoredObjectVoters => ChillActivityBundle/Security/Authorization}/ActivityStoredObjectVoter.php (94%) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/{AccompanyingCourseStoredObjectVoter.php => AccompanyingCourseDocumentStoredObjectVoter.php} (90%) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/{PersonStoredObjectVoter.php => PersonDocumentStoredObjectVoter.php} (90%) rename src/Bundle/{ChillDocStoreBundle/Security/Authorization/StoredObjectVoters => ChillEventBundle/Security/Authorization}/EventStoredObjectVoter.php (94%) diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index 4d621d56e..47ecb66dc 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -106,7 +106,7 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn * @throws NonUniqueResultException * @throws NoResultException */ - public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?Activity { $qb = $this->createQueryBuilder('a'); $query = $qb diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php similarity index 94% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php rename to src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index a36535005..01ec68bd9 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -1,6 +1,6 @@ Date: Wed, 26 Jun 2024 10:05:38 +0200 Subject: [PATCH 046/375] Add signature messenger request serialization and processing This update introduces a new serializer class for request messages (from messenger component). New features-includes encoding and decoding of request messages and handling unexpected value exceptions. A new test class for the serializer and it also adds functionality to process signature requests in the controller. --- .../Controller/SignatureRequestController.php | 46 ++++++ .../BaseSigner/RequestPdfSignMessage.php | 26 ++++ .../RequestPdfSignMessageSerializer.php | 102 +++++++++++++ .../RequestPdfSignMessageSerializerTest.php | 137 ++++++++++++++++++ 4 files changed, 311 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessage.php create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessageSerializer.php create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/RequestPdfSignMessageSerializerTest.php diff --git a/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php b/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php new file mode 100644 index 000000000..d76e9e5db --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php @@ -0,0 +1,46 @@ +storedObjectManager->read($storedObject); + + $this->messageBus->dispatch(new RequestPdfSignMessage( + 0, + new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)), + 0, + 'test signature', + 'Mme Caroline Diallo', + $content + )); + + return new Response('test

ok

'); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessage.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessage.php new file mode 100644 index 000000000..2598bf4d2 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessage.php @@ -0,0 +1,26 @@ +denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [ + AbstractNormalizer::GROUPS => ['write'], + ]); + + $content = base64_decode($data['content'], true); + + if (false === $content) { + throw new MessageDecodingFailedException('the content could not be converted from base64 encoding'); + } + + $message = new RequestPdfSignMessage( + $data['signatureId'], + $zoneSignature, + $data['signatureZoneIndex'], + $data['reason'], + $data['signerText'], + $content, + ); + + // in case of redelivery, unserialize any stamps + $stamps = []; + if (isset($headers['stamps'])) { + $stamps = unserialize($headers['stamps']); + } + + return new Envelope($message, $stamps); + } + + public function encode(Envelope $envelope): array + { + $message = $envelope->getMessage(); + + if (!$message instanceof RequestPdfSignMessage) { + throw new MessageDecodingFailedException('Message is not a RequestPdfSignMessage'); + } + + $data = [ + 'signatureId' => $message->signatureId, + 'signatureZoneIndex' => $message->signatureZoneIndex, + 'signatureZone' => $this->normalizer->normalize($message->PDFSignatureZone, 'json', [AbstractNormalizer::GROUPS => ['read']]), + 'reason' => $message->reason, + 'signerText' => $message->signerText, + 'content' => base64_encode($message->content), + ]; + + $allStamps = []; + foreach ($envelope->all() as $stamp) { + if ($stamp instanceof NonSendableStampInterface) { + continue; + } + $allStamps = [...$allStamps, ...$stamp]; + } + + return [ + 'body' => json_encode($data, JSON_THROW_ON_ERROR, 512), + 'headers' => [ + 'stamps' => serialize($allStamps), + 'Message' => RequestPdfSignMessage::class, + ], + ]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/RequestPdfSignMessageSerializerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/RequestPdfSignMessageSerializerTest.php new file mode 100644 index 000000000..6fda535df --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/RequestPdfSignMessageSerializerTest.php @@ -0,0 +1,137 @@ +buildSerializer(); + + $envelope = new Envelope( + $request = new RequestPdfSignMessage( + 0, + new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)), + 0, + 'metadata to add to the signature', + 'Mme Caroline Diallo', + 'abc' + ), + ); + + $actual = $serializer->encode($envelope); + $expectedBody = json_encode([ + 'signatureId' => $request->signatureId, + 'signatureZoneIndex' => $request->signatureZoneIndex, + 'signatureZone' => ['x' => 10.0], + 'reason' => $request->reason, + 'signerText' => $request->signerText, + 'content' => base64_encode($request->content), + ]); + + self::assertIsArray($actual); + self::assertArrayHasKey('body', $actual); + self::assertArrayHasKey('headers', $actual); + self::assertEquals($expectedBody, $actual['body']); + } + + public function testDecode(): void + { + $serializer = $this->buildSerializer(); + + $request = new RequestPdfSignMessage( + 0, + new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)), + 0, + 'metadata to add to the signature', + 'Mme Caroline Diallo', + 'abc' + ); + + $bodyAsString = json_encode([ + 'signatureId' => $request->signatureId, + 'signatureZoneIndex' => $request->signatureZoneIndex, + 'signatureZone' => ['x' => 10.0], + 'reason' => $request->reason, + 'signerText' => $request->signerText, + 'content' => base64_encode($request->content), + ], JSON_THROW_ON_ERROR); + + $actual = $serializer->decode([ + 'body' => $bodyAsString, + 'headers' => [ + 'Message' => RequestPdfSignMessage::class, + ], + ]); + + self::assertInstanceOf(RequestPdfSignMessage::class, $actual->getMessage()); + self::assertEquals($request->signatureId, $actual->getMessage()->signatureId); + self::assertEquals($request->signatureZoneIndex, $actual->getMessage()->signatureZoneIndex); + self::assertEquals($request->reason, $actual->getMessage()->reason); + self::assertEquals($request->signerText, $actual->getMessage()->signerText); + self::assertEquals($request->content, $actual->getMessage()->content); + self::assertNotNull($actual->getMessage()->PDFSignatureZone); + } + + private function buildSerializer(): RequestPdfSignMessageSerializer + { + $normalizer = + new class () implements NormalizerInterface { + public function normalize($object, ?string $format = null, array $context = []): array + { + if (!$object instanceof PDFSignatureZone) { + throw new UnexpectedValueException('expected RequestPdfSignMessage'); + } + + return [ + 'x' => $object->x, + ]; + } + + public function supportsNormalization($data, ?string $format = null): bool + { + return $data instanceof PDFSignatureZone; + } + }; + $denormalizer = new class () implements DenormalizerInterface { + public function denormalize($data, string $type, ?string $format = null, array $context = []) + { + 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) + { + return PDFSignatureZone::class === $type; + } + }; + + $serializer = new Serializer([$normalizer, $denormalizer]); + + return new RequestPdfSignMessageSerializer($serializer, $serializer); + } +} From 9bc6fe6aff1504e7ccf84cab0aa2cc8f9021fc2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 27 Jun 2024 17:17:09 +0200 Subject: [PATCH 047/375] Add PdfSignedMessage and its serializer Added a new class, PdfSignedMessage, to handle received signed PDF messages. Also, added a serializer for this class, PdfSignedMessageSerializer, for use with messaging. Furthermore, comment documentation has been added to RequestPdfSignMessage and its serializer for better clarity. Updated unit tests are also included. --- .../Driver/BaseSigner/PdfSignedMessage.php | 23 +++++++ .../BaseSigner/PdfSignedMessageHandler.php | 25 +++++++ .../BaseSigner/PdfSignedMessageSerializer.php | 66 +++++++++++++++++++ .../BaseSigner/RequestPdfSignMessage.php | 3 + .../RequestPdfSignMessageSerializer.php | 3 + .../PdfSignedMessageSerializerTest.php | 63 ++++++++++++++++++ 6 files changed, 183 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessage.php create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php create mode 100644 src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageSerializer.php create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageSerializerTest.php diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessage.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessage.php new file mode 100644 index 000000000..64ada47c7 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessage.php @@ -0,0 +1,23 @@ +logger->info(self::P."a message is received", ['signaturedId' => $message->signatureId]); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageSerializer.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageSerializer.php new file mode 100644 index 000000000..082b9b83c --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageSerializer.php @@ -0,0 +1,66 @@ +getMessage(); + + if (!$message instanceof PdfSignedMessage) { + throw new MessageDecodingFailedException('Expected a PdfSignedMessage'); + } + + $data = [ + 'signatureId' => $message->signatureId, + 'content' => base64_encode($message->content), + ]; + + return [ + 'body' => json_encode($data, JSON_THROW_ON_ERROR), + 'headers' => [], + ]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessage.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessage.php index 2598bf4d2..144a738e1 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessage.php +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessage.php @@ -13,6 +13,9 @@ namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner; use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone; +/** + * Message which is sent when we request a signature on a pdf. + */ final readonly class RequestPdfSignMessage { public function __construct( diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessageSerializer.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessageSerializer.php index 1241bc0bb..57314a109 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessageSerializer.php +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/RequestPdfSignMessageSerializer.php @@ -20,6 +20,9 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +/** + * Serialize a RequestPdfSignMessage, for external consumer. + */ final readonly class RequestPdfSignMessageSerializer implements SerializerInterface { public function __construct( diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageSerializerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageSerializerTest.php new file mode 100644 index 000000000..d9d600254 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageSerializerTest.php @@ -0,0 +1,63 @@ +buildSerializer()->decode(['body' => $asString]); + + self::assertInstanceOf(Envelope::class, $actual); + $message = $actual->getMessage(); + self::assertInstanceOf(PdfSignedMessage::class, $message); + self::assertEquals("test\n", $message->content); + self::assertEquals(0, $message->signatureId); + } + + public function testEncode(): void + { + $envelope = new Envelope( + new PdfSignedMessage(0, "test\n") + ); + + $actual = $this->buildSerializer()->encode($envelope); + + self::assertIsArray($actual); + self::assertArrayHasKey('body', $actual); + self::assertArrayHasKey('headers', $actual); + self::assertEquals([], $actual['headers']); + + self::assertEquals(<<<'JSON' + {"signatureId":0,"content":"dGVzdAo="} + JSON, $actual['body']); + } + + private function buildSerializer(): PdfSignedMessageSerializer + { + return new PdfSignedMessageSerializer(); + } +} From c9d54a5fea14bac1035a712d142b022c071687e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 28 Jun 2024 10:47:12 +0200 Subject: [PATCH 048/375] fix cs --- .../BaseSigner/PdfSignedMessageHandler.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php index 37fb3de6c..4002b107b 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php @@ -1,5 +1,14 @@ logger->info(self::P."a message is received", ['signaturedId' => $message->signatureId]); + $this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]); } } From c9d2e37cee3533cc2eb0203ca4843ad470601760 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 12:14:03 +0200 Subject: [PATCH 049/375] Implement logic to check if editing of document is blocked by workflow Using the workflow handlers we return the workflow that is attached to an object so that within the workflowDocumentService we can then check whether this workflow blocks the edition of a document. --- .../Service/WorkflowDocumentService.php | 26 +++++++++++-------- ...ompanyingCourseDocumentWorkflowHandler.php | 20 +++++++++----- .../EntityWorkflowHandlerInterface.php | 2 ++ .../Workflow/EntityWorkflowManager.php | 9 +++++++ ...dWorkEvaluationDocumentWorkflowHandler.php | 16 +++++++++++- ...ingPeriodWorkEvaluationWorkflowHandler.php | 17 +++++++++++- .../AccompanyingPeriodWorkWorkflowHandler.php | 16 +++++++++++- 7 files changed, 86 insertions(+), 20 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index f796fe56e..17591b2d0 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -4,32 +4,36 @@ namespace Chill\DocStoreBundle\Service; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; +use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\Security\Core\Security; class WorkflowDocumentService { - public function __construct(private readonly Security $security, private readonly EntityWorkflowRepository $repository) + public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) { } - public function notBlockedByWorkflow($entity): bool + public function notBlockedByWorkflow(object $entity): bool { /** * @var EntityWorkflow */ - $workflow = $this->repository->findByRelatedEntity(get_class($entity), $entity->getId()); - - if ($workflow->isFinal()) { - return false; - } - + $workflow = $this->entityWorkflowManager->findByRelatedEntity($entity); $currentUser = $this->security->getUser(); - if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { - return true; + + + if (null !== $workflow) { + if ($workflow->isFinal()) { + return false; + } + + if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + return true; + } } - return false; + return true; } } diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index 9d1b619f9..e3f201914 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -12,8 +12,10 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Workflow; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; +use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Doctrine\ORM\EntityManagerInterface; @@ -22,16 +24,13 @@ use Symfony\Contracts\Translation\TranslatorInterface; class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface { - private readonly EntityRepository $repository; - /** - * TODO: injecter le repository directement. - */ public function __construct( EntityManagerInterface $em, - private readonly TranslatorInterface $translator + private readonly TranslatorInterface $translator, + private readonly EntityWorkflowRepository $workflowRepository, + private readonly AccompanyingCourseDocumentRepository $repository ) { - $this->repository = $em->getRepository(AccompanyingCourseDocument::class); } public function getDeletionRoles(): array @@ -126,4 +125,13 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler { return false; } + + public function findByRelatedEntity(object $object): ?EntityWorkflow + { + if(!$object instanceof AccompanyingCourseDocument) { + return null; + } + + return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId()); + } } diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php index 1d185ba0e..bff1057c8 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php @@ -51,4 +51,6 @@ interface EntityWorkflowHandlerInterface public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool; public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool; + + public function findByRelatedEntity(object $object): ?EntityWorkflow; } diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php index 9a1f52280..3f2ff0439 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php @@ -37,4 +37,13 @@ class EntityWorkflowManager { return $this->registry->all($entityWorkflow); } + + public function findByRelatedEntity(object $object): ?EntityWorkflow + { + foreach ($this->handlers as $handler) { + return $handler->findByRelatedEntity($object); + } + return null; + } + } diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index 0fc13224e..0fd3af0af 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Workflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; @@ -21,7 +22,12 @@ use Symfony\Contracts\Translation\TranslatorInterface; class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityWorkflowHandlerInterface { - public function __construct(private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatorInterface $translator) {} + public function __construct( + private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, + private readonly EntityWorkflowRepository $workflowRepository, + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly TranslatorInterface $translator + ) {} public function getDeletionRoles(): array { @@ -128,4 +134,12 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW { return false; } + + public function findByRelatedEntity(object $object): ?EntityWorkflow + { + if (!$object instanceof AccompanyingPeriodWorkEvaluationDocument) { + return null; + } + return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWorkEvaluationDocument::class, $object->getId()); + } } diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php index 63b34d1dc..e007f03f7 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Workflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation; @@ -22,7 +23,12 @@ use Symfony\Contracts\Translation\TranslatorInterface; class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowHandlerInterface { - public function __construct(private readonly AccompanyingPeriodWorkEvaluationRepository $repository, private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatorInterface $translator) {} + public function __construct( + private readonly AccompanyingPeriodWorkEvaluationRepository $repository, + private readonly EntityWorkflowRepository $workflowRepository, + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly TranslatorInterface $translator + ) {} public function getDeletionRoles(): array { @@ -114,4 +120,13 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH { return false; } + + public function findByRelatedEntity(object $object): ?EntityWorkflow + { + if (!$object instanceof AccompanyingPeriodWorkEvaluation) { + return null; + } + + return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWorkEvaluation::class, $object->getId()); + } } diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php index 5c74e5b17..81116b7b2 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Workflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; @@ -23,7 +24,12 @@ use Symfony\Contracts\Translation\TranslatorInterface; class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInterface { - public function __construct(private readonly AccompanyingPeriodWorkRepository $repository, private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatorInterface $translator) {} + public function __construct( + private readonly AccompanyingPeriodWorkRepository $repository, + private readonly EntityWorkflowRepository $workflowRepository, + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly TranslatorInterface $translator + ) {} public function getDeletionRoles(): array { @@ -121,4 +127,12 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte { return false; } + + public function findByRelatedEntity(object $object): ?EntityWorkflow + { + if (!$object instanceof AccompanyingPeriodWork) { + return null; + } + return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWork::class, $object->getId()); + } } From 254122d125151cf16634de0a858bac6bd2f10c65 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 12:20:21 +0200 Subject: [PATCH 050/375] Remove check to see if user is instance of User The admin user would not be identified as a User. --- .../StoredObjectVoters/AbstractStoredObjectVoter.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php index 7ca73f206..659d7d28a 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php @@ -40,10 +40,6 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool { - if (!$token->getUser() instanceof User) { - return false; - } - // Retrieve the related accompanying course document $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject); From d1653a074bb3595bed9e828f16b8f90325e3272f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 12:21:25 +0200 Subject: [PATCH 051/375] Implement test on AbstractStoredObjectVoter To avoid having to duplicate tests, a test is written\ for the abstract voter. --- ....php => AbstractStoredObjectVoterTest.php} | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) rename src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/{AccompanyingCourseStoredObjectVoterTest.php => AbstractStoredObjectVoterTest.php} (73%) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php similarity index 73% rename from src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php rename to src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index f7ad25987..d5ab471ab 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AccompanyingCourseStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -5,15 +5,17 @@ namespace Chill\DocStoreBundle\Tests\Security\Authorization; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\StoredObject; 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\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\MainBundle\Entity\User; use ChillDocStoreBundle\Security\Authorization\StoredObjectVoters\AccompanyingCourseStoredObjectVoter; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -class AccompanyingCourseStoredObjectVoterTest extends PHPUnit\Framework\TestCase +class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase { private $repository; private $security; @@ -26,11 +28,28 @@ class AccompanyingCourseStoredObjectVoterTest extends PHPUnit\Framework\TestCase $this->security = $this->createMock(Security::class); $this->workflowDocumentService = $this->createMock(WorkflowDocumentService::class); - $this->voter = new AccompanyingCourseStoredObjectVoter( - $this->repository, - $this->security, - $this->workflowDocumentService - ); + // Anonymous class extending the abstract class + $this->voter = new class($this->repository, $this->security, $this->workflowDocumentService) extends AbstractStoredObjectVoter { + protected function attributeToRole($attribute): string + { + return AccompanyingCourseDocumentVoter::SEE_DETAILS; + } + + protected function getRepository(): AssociatedEntityToStoredObjectInterface + { + // TODO: Implement getRepository() method. + } + + protected function getClass(): string + { + // TODO: Implement getClass() method. + } + + protected function canBeAssociatedWithWorkflow(): bool + { + // TODO: Implement canBeAssociatedWithWorkflow() method. + } + }; } private function setupMockObjects(): array @@ -94,7 +113,7 @@ class AccompanyingCourseStoredObjectVoterTest extends PHPUnit\Framework\TestCase list($user, $token, $subject, $entity) = $this->setupMockObjects(); // Setup mocks for voteOnAttribute method - $this->setupMocksForVoteOnAttribute($user, $token, $subject, $entity, false); + $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false); // Test voteOnAttribute method $attribute = StoredObjectRoleEnum::SEE; @@ -103,4 +122,16 @@ class AccompanyingCourseStoredObjectVoterTest extends PHPUnit\Framework\TestCase // Assert that access is denied when workflow is not allowed $this->assertFalse($result); } + + public function testAbstractStoredObjectVoter(): void + { + $voter = new class extends AbstractStoredObjectVoter { + // Implement abstract methods here + public function someMethod() { + // method implementation + } + }; + // Run tests on $voter + } + } From fb743b522d35aea4fd82ed6fa2ef59fe1b2fc688 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 12:23:31 +0200 Subject: [PATCH 052/375] Remove implementation of StoredObjectInterface --- .../AccompanyingPeriodWorkEvaluationDocumentVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php index 8f4a1b995..ecf3cd802 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php @@ -22,7 +22,7 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; * * Delegates to the sames authorization than for Evalution */ -class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter implements StoredObjectVoterInterface +class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter { final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW'; From 3db4fff80df6ce82c97b8c6b00b1cd037a3ccb66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 28 Jun 2024 11:58:39 +0200 Subject: [PATCH 053/375] Add signature functionality to workflow entities Created new files to add signature functionality to the workflow entities, including signature state enums and signature metadata. Added these changes to the migration script as well. Updated EntityWorkflowStep to include a collection for signatures. --- .../EntityWorkflowSignatureStateEnum.php | 20 ++ .../Entity/Workflow/EntityWorkflowStep.php | 42 ++- .../Workflow/EntityWorkflowStepSignature.php | 156 ++++++++++ .../EntityWorkflowStepSignatureRepository.php | 54 ++++ .../EntityWorkflowStepSignatureTest.php | 69 ++++ .../migrations/Version20240628095159.php | 51 +++ tests/app/config/packages/workflow_chill.yaml | 294 ++++++++++++++++++ 7 files changed, 684 insertions(+), 2 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php create mode 100644 src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php create mode 100644 src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20240628095159.php create mode 100644 tests/app/config/packages/workflow_chill.yaml diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php new file mode 100644 index 000000000..2d3e4269f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSignatureStateEnum.php @@ -0,0 +1,20 @@ + + * @var Collection */ #[ORM\ManyToMany(targetEntity: User::class)] #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')] private Collection $destUser; /** - * @var Collection + * @var Collection */ #[ORM\ManyToMany(targetEntity: User::class)] #[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')] private Collection $destUserByAccessKey; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)] + private Collection $signatures; + #[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')] private ?EntityWorkflow $entityWorkflow = null; @@ -97,6 +103,7 @@ class EntityWorkflowStep $this->ccUser = new ArrayCollection(); $this->destUser = new ArrayCollection(); $this->destUserByAccessKey = new ArrayCollection(); + $this->signatures = new ArrayCollection(); $this->accessKey = bin2hex(openssl_random_pseudo_bytes(32)); } @@ -136,6 +143,29 @@ class EntityWorkflowStep return $this; } + /** + * @internal use @see{EntityWorkflowStepSignature}'s constructor instead + */ + public function addSignature(EntityWorkflowStepSignature $signature): self + { + if (!$this->signatures->contains($signature)) { + $this->signatures[] = $signature; + } + + return $this; + } + + public function removeSignature(EntityWorkflowStepSignature $signature): self + { + if ($this->signatures->contains($signature)) { + $this->signatures->removeElement($signature); + } + + $signature->detachEntityWorkflowStep(); + + return $this; + } + public function getAccessKey(): string { return $this->accessKey; @@ -198,6 +228,14 @@ class EntityWorkflowStep return $this->entityWorkflow; } + /** + * @return Collection + */ + public function getSignatures(): Collection + { + return $this->signatures; + } + public function getId(): ?int { return $this->id; diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php new file mode 100644 index 000000000..25babc3c6 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php @@ -0,0 +1,156 @@ + null])] + private ?\DateTimeImmutable $stateDate = null; + + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])] + private array $signatureMetadata = []; + + #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])] + private ?int $zoneSignatureIndex = null; + + #[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')] + #[ORM\JoinColumn(nullable: false)] + private ?EntityWorkflowStep $step = null; + + public function __construct( + EntityWorkflowStep $step, + User|Person $signer, + ) { + $this->step = $step; + $step->addSignature($this); + $this->setSigner($signer); + } + + private function setSigner(User|Person $signer): void + { + if ($signer instanceof User) { + $this->userSigner = $signer; + } else { + $this->personSigner = $signer; + } + } + + public function getId(): ?int + { + return $this->id; + } + + public function getStep(): EntityWorkflowStep + { + return $this->step; + } + + public function getSigner(): User|Person + { + if (null !== $this->userSigner) { + return $this->userSigner; + } + + return $this->personSigner; + } + + public function getSignatureMetadata(): array + { + return $this->signatureMetadata; + } + + public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature + { + $this->signatureMetadata = $signatureMetadata; + + return $this; + } + + public function getState(): EntityWorkflowSignatureStateEnum + { + return $this->state; + } + + public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature + { + $this->state = $state; + + return $this; + } + + public function getStateDate(): ?\DateTimeImmutable + { + return $this->stateDate; + } + + public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature + { + $this->stateDate = $stateDate; + + return $this; + } + + public function getZoneSignatureIndex(): ?int + { + return $this->zoneSignatureIndex; + } + + public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature + { + $this->zoneSignatureIndex = $zoneSignatureIndex; + + return $this; + } + + /** + * Detach from the @see{EntityWorkflowStep}. + * + * @internal used internally to remove the current signature + * + * @return $this + */ + public function detachEntityWorkflowStep(): self + { + $this->step = null; + + return $this; + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php new file mode 100644 index 000000000..0e1393242 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowStepSignatureRepository.php @@ -0,0 +1,54 @@ + + */ +class EntityWorkflowStepSignatureRepository implements ObjectRepository +{ + private \Doctrine\ORM\EntityRepository $repository; + + public function __construct(EntityManagerInterface $entityManager) + { + $this->repository = $entityManager->getRepository(EntityWorkflowStepSignature::class); + } + + public function find($id): ?EntityWorkflowStepSignature + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?EntityWorkflowStepSignature + { + return $this->findOneBy($criteria); + } + + public function getClassName(): string + { + return EntityWorkflowStepSignature::class; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php new file mode 100644 index 000000000..62b6a7d6a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowStepSignatureTest.php @@ -0,0 +1,69 @@ +entityManager = self::getContainer()->get(EntityManagerInterface::class); + } + + public function testConstruct() + { + $workflow = new EntityWorkflow(); + $workflow->setWorkflowName('vendee_internal') + ->setRelatedEntityId(0) + ->setRelatedEntityClass(AccompanyingPeriodWorkEvaluationDocument::class); + + $step = $workflow->getCurrentStep(); + + $person = $this->entityManager->createQuery('SELECT p FROM '.Person::class.' p') + ->setMaxResults(1) + ->getSingleResult(); + + $signature = new EntityWorkflowStepSignature($step, $person); + + self::assertCount(1, $step->getSignatures()); + self::assertSame($signature, $step->getSignatures()->first()); + + $this->entityManager->getConnection()->beginTransaction(); + $this->entityManager->persist($workflow); + $this->entityManager->persist($step); + $this->entityManager->persist($signature); + + $this->entityManager->flush(); + $this->entityManager->getConnection()->commit(); + + $this->entityManager->clear(); + + $signatureBis = $this->entityManager->find(EntityWorkflowStepSignature::class, $signature->getId()); + + self::assertEquals($signature->getId(), $signatureBis->getId()); + self::assertEquals($step->getId(), $signatureBis->getStep()->getId()); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20240628095159.php b/src/Bundle/ChillMainBundle/migrations/Version20240628095159.php new file mode 100644 index 000000000..4452530c6 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20240628095159.php @@ -0,0 +1,51 @@ +addSql('CREATE SEQUENCE chill_main_workflow_entity_step_signature_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_workflow_entity_step_signature (id INT NOT NULL, step_id INT NOT NULL, '. + 'state VARCHAR(50) NOT NULL, stateDate TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, signatureMetadata JSON DEFAULT \'[]\' NOT NULL,'. + ' zoneSignatureIndex INT DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,'. + ' userSigner_id INT DEFAULT NULL, personSigner_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_C47D4BA3D934E3A4 ON chill_main_workflow_entity_step_signature (userSigner_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA3ADFFA293 ON chill_main_workflow_entity_step_signature (personSigner_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA373B21E9C ON chill_main_workflow_entity_step_signature (step_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA33174800F ON chill_main_workflow_entity_step_signature (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_C47D4BA365FF1AEC ON chill_main_workflow_entity_step_signature (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.stateDate IS \'(DC2Type:datetimetz_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3D934E3A4 FOREIGN KEY (userSigner_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3ADFFA293 FOREIGN KEY (personSigner_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA373B21E9C FOREIGN KEY (step_id) REFERENCES chill_main_workflow_entity_step (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA33174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA365FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE chill_main_workflow_entity_step_signature_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_main_workflow_entity_step_signature'); + } +} diff --git a/tests/app/config/packages/workflow_chill.yaml b/tests/app/config/packages/workflow_chill.yaml new file mode 100644 index 000000000..82662461b --- /dev/null +++ b/tests/app/config/packages/workflow_chill.yaml @@ -0,0 +1,294 @@ +framework: + workflows: + vendee_internal: + type: state_machine + metadata: + related_entity: + - Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument + - Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork + - Chill\DocStoreBundle\Entity\AccompanyingCourseDocument + label: + fr: 'Suivi' + support_strategy: Chill\MainBundle\Workflow\RelatedEntityWorkflowSupportsStrategy + initial_marking: 'initial' + marking_store: + property: step + type: method + places: + initial: + metadata: + label: + fr: Étape initiale + attenteModification: + metadata: + label: + fr: En attente de modification du document + validationFilterInputLabels: + forward: {fr: Modification effectuée} + backward: {fr: Pas de modification effectuée} + neutral: {fr: Autre} + attenteMiseEnForme: + metadata: + label: + fr: En attente de mise en forme + validationFilterInputLabels: + forward: {fr: Mise en forme terminée} + backward: {fr: Pas de mise en forme effectuée} + neutral: {fr: Autre} + attenteVisa: + metadata: + label: + fr: En attente de visa + validationFilterInputLabels: + forward: {fr: Visa accordé} + backward: {fr: Visa refusé} + neutral: {fr: Autre} + attenteSignature: + metadata: + label: + fr: En attente de signature + validationFilterInputLabels: + forward: {fr: Signature accordée} + backward: {fr: Signature refusée} + neutral: {fr: Autre} + attenteTraitement: + metadata: + label: + fr: En attente de traitement + validationFilterInputLabels: + forward: {fr: Traitement terminé favorablement} + backward: {fr: Traitement terminé défavorablement} + neutral: {fr: Autre} + attenteEnvoi: + metadata: + label: + fr: En attente d'envoi + validationFilterInputLabels: + forward: {fr: Document envoyé} + backward: {fr: Document non envoyé} + neutral: {fr: Autre} + attenteValidationMiseEnForme: + metadata: + label: + fr: En attente de validation de la mise en forme + validationFilterInputLabels: + forward: {fr: Validation de la mise en forme} + backward: {fr: Refus de validation de la mise en forme} + neutral: {fr: Autre} + annule: + metadata: + isFinal: true + isFinalPositive: false + label: + fr: Annulé + final: + metadata: + isFinal: true + isFinalPositive: true + label: + fr: Finalisé + transitions: + # transition qui avancent + demandeModificationDocument: + from: + - initial + to: attenteModification + metadata: + label: + fr: Demande de modification du document + isForward: true + demandeMiseEnForme: + from: + - initial + - attenteModification + to: attenteMiseEnForme + metadata: + label: + fr: Demande de mise en forme + isForward: true + demandeValidationMiseEnForme: + from: + - attenteMiseEnForme + to: attenteValidationMiseEnForme + metadata: + label: + fr: Demande de validation de la mise en forme + isForward: true + demandeVisa: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + to: attenteVisa + metadata: + label: + fr: Demande de visa + isForward: true + demandeSignature: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + to: attenteSignature + metadata: + label: {fr: Demande de signature} + isForward: true + demandeTraitement: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + - attenteSignature + to: attenteTraitement + metadata: + label: {fr: Demande de traitement} + isForward: true + demandeEnvoi: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + - attenteSignature + - attenteTraitement + to: attenteEnvoi + metadata: + label: {fr: Demande d'envoi} + isForward: true + annulation: + from: + - initial + - attenteModification + - attenteMiseEnForme + - attenteValidationMiseEnForme + - attenteVisa + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: annule + metadata: + label: {fr: Annulation} + isForward: false + # transitions qui répètent l'étape + demandeMiseEnFormeSupplementaire: + from: + - attenteMiseEnForme + - attenteValidationMiseEnForme + to: attenteMiseEnForme + metadata: + label: {fr: Demande de mise en forme supplémentaire} + demandeVisaSupplementaire: + from: + - attenteVisa + to: attenteVisa + metadata: + label: {fr: Demande de visa supplémentaire} + isForward: true + demandeSignatureSupplementaire: + from: + - attenteSignature + to: attenteSignature + metadata: + label: {fr: Demande de signature supplémentaire} + demandeTraitementSupplementaire: + from: + - attenteTraitement + to: attenteTraitement + metadata: + label: {fr: Demande de traitement supplémentaire} + # transitions qui renvoient vers une étape précédente + refusEtModificationDocument: + from: + - attenteVisa + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: attenteModification + metadata: + label: + fr: Refus et demande de modification du document + isForward: false + refusEtDemandeMiseEnForme: + from: + - attenteVisa + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: attenteMiseEnForme + metadata: + label: {fr: Refus et demande de mise en forme} + isForward: false + refusEtDemandeVisa: + from: + - attenteSignature + - attenteTraitement + - attenteEnvoi + to: attenteVisa + metadata: + label: {fr: Refus et demande de visa} + isForward: false + refusEtDemandeSignature: + from: + - attenteTraitement + - attenteEnvoi + to: attenteSignature + metadata: + label: {fr: Refus et demande de signature} + isForward: false + refusEtDemandeTraitement: + from: + - attenteEnvoi + to: attenteTraitement + metadata: + label: {fr: Refus et demande de traitement} + isForward: false + # transition vers final + initialToFinal: + from: + - initial + to: final + metadata: + label: {fr: Clotûre immédiate et cloture positive} + isForward: true + attenteMiseEnFormeToFinal: + from: + - attenteMiseEnForme + - attenteValidationMiseEnForme + to: final + metadata: + label: {fr: Mise en forme terminée et cloture positive} + isForward: true + attenteVisaToFinal: + from: + - attenteVisa + to: final + metadata: + label: {fr: Accorde le visa et cloture positive} + isForward: true + attenteSignatureToFinal: + from: + - attenteSignature + to: final + metadata: + label: {fr: Accorde la signature et cloture positive} + isForward: true + attenteTraitementToFinal: + from: + - attenteTraitement + to: final + metadata: + label: {fr: Traitement terminé et cloture postive} + isForward: true + attenteEnvoiToFinal: + from: + - attenteEnvoi + to: final + metadata: + label: {fr: Envoyé et cloture postive} + isForward: true From 8c92d117225b77a9cd5951ddd721311ad3ebd022 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 15:23:07 +0200 Subject: [PATCH 054/375] Implement permissions for WOPI --- .../src/Service/Wopi/AuthorizationManager.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php index c54f187ca..9ed421461 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php @@ -65,7 +65,11 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanRead(string $accessToken, Document $document, RequestInterface $request): bool { - return $this->isTokenValid($accessToken, $document, $request); + if ($this->security->isGranted('SEE', $document)) { + return $this->isTokenValid($accessToken, $document, $request); + } + + return false; } public function userCanRename(string $accessToken, Document $document, RequestInterface $request): bool @@ -75,6 +79,11 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanWrite(string $accessToken, Document $document, RequestInterface $request): bool { - return $this->isTokenValid($accessToken, $document, $request); + if ($this->security->isGranted('EDIT', $document)) { + return $this->isTokenValid($accessToken, $document, $request); + } + + return false; } + } From ac2f314395514328d8ddb5bdba7fbec9f291e581 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 15:23:32 +0200 Subject: [PATCH 055/375] Implement permissions for download button group --- .../Templating/WopiEditTwigExtensionRuntime.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php index 716cb422e..be45bd2d4 100644 --- a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php +++ b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php @@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Twig\Environment; @@ -128,6 +129,7 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt private NormalizerInterface $normalizer, private JWTDavTokenProviderInterface $davTokenProvider, private UrlGeneratorInterface $urlGenerator, + private Security $security, ) {} /** @@ -150,6 +152,8 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt */ public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string { + $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT, $document); + $accessToken = $this->davTokenProvider->createToken( $document, $canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE From 5b0babb9b08d260bfc0c64be37b6b43feb4ba270 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 15:37:47 +0200 Subject: [PATCH 056/375] Implement permissions in AsyncUploadVoter.php --- .../Security/Authorization/AsyncUploadVoter.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php index 1d7ad759a..d458bae08 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php @@ -36,6 +36,13 @@ final class AsyncUploadVoter extends Voter return false; } + //TODO get the StoredObject from the SignedUrl +/* match($subject->method) { + 'GET' => $this->security->isGranted('SEE', $storedObject), + 'PUT' => $this->security->isGranted('EDIT', $storedObject), + 'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') + };*/ + return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN'); } } From a309cc077480c1b2889f630098925c9b90476d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 1 Jul 2024 20:47:15 +0200 Subject: [PATCH 057/375] Refactor workflow classes and forms - the workflow controller add a context to each transition; - the state of the entity workflow is applyied using a dedicated marking store - the method EntityWorkflow::step use the context to associate the new step with the future destination user, cc users and email. This makes the step consistent at every step. - this allow to remove some logic which was processed in eventSubscribers, - as counterpart, each workflow must specify a dedicated marking_store: ```yaml framework: workflows: vendee_internal: # ... marking_store: service: Chill\MainBundle\Workflow\EntityWorkflowMarkingStore ``` --- ...ompanyingCourseDocumentWorkflowHandler.php | 5 - .../Controller/WorkflowController.php | 13 +- .../Entity/Workflow/EntityWorkflow.php | 45 +-- .../ChillMainBundle/Form/WorkflowStepType.php | 266 ++++++++---------- .../views/Workflow/_decision.html.twig | 14 +- .../Entity/Workflow/EntityWorkflowTest.php | 17 +- .../EntityWorkflowMarkingStoreTest.php | 61 ++++ .../EntityWorkflowHandlerInterface.php | 2 - .../Workflow/EntityWorkflowMarkingStore.php | 49 ++++ ...ntityWorkflowTransitionEventSubscriber.php | 35 +-- .../NotificationOnTransition.php | 13 +- .../SendAccessKeyEventSubscriber.php | 10 +- .../Workflow/WorkflowTransitionContextDTO.php | 74 +++++ ...dWorkEvaluationDocumentWorkflowHandler.php | 5 - ...ingPeriodWorkEvaluationWorkflowHandler.php | 5 - .../AccompanyingPeriodWorkWorkflowHandler.php | 5 - 16 files changed, 362 insertions(+), 257 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index 9d1b619f9..55b823dcd 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -121,9 +121,4 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler { return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass(); } - - public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool - { - return false; - } } diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php index d75e42012..3a8626f26 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php @@ -22,6 +22,7 @@ use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter; use Chill\MainBundle\Security\ChillSecurity; use Chill\MainBundle\Workflow\EntityWorkflowManager; +use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\FormType; @@ -279,7 +280,7 @@ class WorkflowController extends AbstractController if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) { // possible transition - + $stepDTO = new WorkflowTransitionContextDTO($entityWorkflow); $usersInvolved = $entityWorkflow->getUsersInvolved(); $currentUserFound = array_search($this->security->getUser(), $usersInvolved, true); @@ -289,9 +290,8 @@ class WorkflowController extends AbstractController $transitionForm = $this->createForm( WorkflowStepType::class, - $entityWorkflow->getCurrentStep(), + $stepDTO, [ - 'transition' => true, 'entity_workflow' => $entityWorkflow, 'suggested_users' => $usersInvolved, ] @@ -310,12 +310,7 @@ class WorkflowController extends AbstractController throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs))); } - // TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context) - $entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? []; - $entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? []; - $entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? []; - - $workflow->apply($entityWorkflow, $transition); + $workflow->apply($entityWorkflow, $transition, ['context' => $stepDTO]); $this->entityManager->flush(); diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index 6df054b61..9c314115e 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -17,9 +17,9 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation; +use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; -use Doctrine\Common\Collections\Order; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Validator\Constraints as Assert; @@ -34,35 +34,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface use TrackUpdateTrait; - /** - * a list of future cc users for the next steps. - * - * @var array|User[] - */ - public array $futureCcUsers = []; - - /** - * a list of future dest emails for the next steps. - * - * This is in used in order to let controller inform who will be the future emails which will validate - * the next step. This is necessary to perform some computation about the next emails, before they are - * associated to the entity EntityWorkflowStep. - * - * @var array|string[] - */ - public array $futureDestEmails = []; - - /** - * a list of future dest users for the next steps. - * - * This is in used in order to let controller inform who will be the future users which will validate - * the next step. This is necessary to perform some computation about the next users, before they are - * associated to the entity EntityWorkflowStep. - * - * @var array|User[] - */ - public array $futureDestUsers = []; - /** * @var Collection */ @@ -442,11 +413,23 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface * * @return $this */ - public function setStep(string $step): self + public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO): self { $newStep = new EntityWorkflowStep(); $newStep->setCurrentStep($step); + foreach ($transitionContextDTO->futureCcUsers as $user) { + $newStep->addCcUser($user); + } + + foreach ($transitionContextDTO->futureDestUsers as $user) { + $newStep->addDestUser($user); + } + + foreach ($transitionContextDTO->futureDestEmails as $email) { + $newStep->addDestEmail($email); + } + // copy the freeze if ($this->isFreeze()) { $newStep->setFreezeAfter(true); diff --git a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php index 3a3f1b8d3..284781d54 100644 --- a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php +++ b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php @@ -12,14 +12,12 @@ declare(strict_types=1); namespace Chill\MainBundle\Form; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; -use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; -use Chill\MainBundle\Workflow\EntityWorkflowManager; +use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\FormBuilderInterface; @@ -34,169 +32,151 @@ use Symfony\Component\Workflow\Transition; class WorkflowStepType extends AbstractType { - public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {} + public function __construct( + private readonly Registry $registry, + private readonly TranslatableStringHelperInterface $translatableStringHelper + ) {} public function buildForm(FormBuilderInterface $builder, array $options) { /** @var EntityWorkflow $entityWorkflow */ $entityWorkflow = $options['entity_workflow']; - $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $place = $workflow->getMarking($entityWorkflow); $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]); - if (true === $options['transition']) { - if (null === $options['entity_workflow']) { - throw new \LogicException('if transition is true, entity_workflow should be defined'); - } + if (null === $options['entity_workflow']) { + throw new \LogicException('if transition is true, entity_workflow should be defined'); + } - $transitions = $this->registry - ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName()) - ->getEnabledTransitions($entityWorkflow); + $transitions = $this->registry + ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName()) + ->getEnabledTransitions($entityWorkflow); - $choices = array_combine( - array_map( - static fn (Transition $transition) => $transition->getName(), - $transitions - ), + $choices = array_combine( + array_map( + static fn (Transition $transition) => $transition->getName(), $transitions - ); + ), + $transitions + ); - if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) { - $inputLabels = $placeMetadata['validationFilterInputLabels']; + if (\array_key_exists('validationFilterInputLabels', $placeMetadata)) { + $inputLabels = $placeMetadata['validationFilterInputLabels']; - $builder->add('transitionFilter', ChoiceType::class, [ - 'multiple' => false, - 'label' => 'workflow.My decision', - 'choices' => [ - 'forward' => 'forward', - 'backward' => 'backward', - 'neutral' => 'neutral', - ], - 'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]), - 'choice_attr' => static fn (string $key) => [ - $key => $key, - ], - 'mapped' => false, - 'expanded' => true, - 'data' => 'forward', - ]); - } - - $builder - ->add('transition', ChoiceType::class, [ - 'label' => 'workflow.Next step', - 'mapped' => false, - 'multiple' => false, - 'expanded' => true, - 'choices' => $choices, - 'constraints' => [new NotNull()], - 'choice_label' => function (Transition $transition) use ($workflow) { - $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition); - - if (\array_key_exists('label', $meta)) { - return $this->translatableStringHelper->localize($meta['label']); - } - - return $transition->getName(); - }, - 'choice_attr' => static function (Transition $transition) use ($workflow) { - $toFinal = true; - $isForward = 'neutral'; - - $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition); - - if (\array_key_exists('isForward', $metadata)) { - if ($metadata['isForward']) { - $isForward = 'forward'; - } else { - $isForward = 'backward'; - } - } - - foreach ($transition->getTos() as $to) { - $meta = $workflow->getMetadataStore()->getPlaceMetadata($to); - - if ( - !\array_key_exists('isFinal', $meta) || false === $meta['isFinal'] - ) { - $toFinal = false; - } - } - - return [ - 'data-is-transition' => 'data-is-transition', - 'data-to-final' => $toFinal ? '1' : '0', - 'data-is-forward' => $isForward, - ]; - }, - ]) - ->add('future_dest_users', PickUserDynamicType::class, [ - 'label' => 'workflow.dest for next steps', - 'multiple' => true, - 'mapped' => false, - 'suggested' => $options['suggested_users'], - ]) - ->add('future_cc_users', PickUserDynamicType::class, [ - 'label' => 'workflow.cc for next steps', - 'multiple' => true, - 'mapped' => false, - 'required' => false, - 'suggested' => $options['suggested_users'], - ]) - ->add('future_dest_emails', ChillCollectionType::class, [ - 'label' => 'workflow.dest by email', - 'help' => 'workflow.dest by email help', - 'mapped' => false, - 'allow_add' => true, - 'entry_type' => EmailType::class, - 'button_add_label' => 'workflow.Add an email', - 'button_remove_label' => 'workflow.Remove an email', - 'empty_collection_explain' => 'workflow.Any email', - 'entry_options' => [ - 'constraints' => [ - new NotNull(), new NotBlank(), new Email(), - ], - 'label' => 'Email', - ], - ]); + $builder->add('transitionFilter', ChoiceType::class, [ + 'multiple' => false, + 'label' => 'workflow.My decision', + 'choices' => [ + 'forward' => 'forward', + 'backward' => 'backward', + 'neutral' => 'neutral', + ], + 'choice_label' => fn (string $key) => $this->translatableStringHelper->localize($inputLabels[$key]), + 'choice_attr' => static fn (string $key) => [ + $key => $key, + ], + 'mapped' => false, + 'expanded' => true, + 'data' => 'forward', + ]); } - if ( - $handler->supportsFreeze($entityWorkflow) - && !$entityWorkflow->isFreeze() - ) { - $builder - ->add('freezeAfter', CheckboxType::class, [ - 'required' => false, - 'label' => 'workflow.Freeze', - 'help' => 'workflow.The associated element will be freezed', - ]); - } + $builder + ->add('transition', ChoiceType::class, [ + 'label' => 'workflow.Next step', + 'mapped' => false, + 'multiple' => false, + 'expanded' => true, + 'choices' => $choices, + 'constraints' => [new NotNull()], + 'choice_label' => function (Transition $transition) use ($workflow) { + $meta = $workflow->getMetadataStore()->getTransitionMetadata($transition); + + if (\array_key_exists('label', $meta)) { + return $this->translatableStringHelper->localize($meta['label']); + } + + return $transition->getName(); + }, + 'choice_attr' => static function (Transition $transition) use ($workflow) { + $toFinal = true; + $isForward = 'neutral'; + + $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition); + + if (\array_key_exists('isForward', $metadata)) { + if ($metadata['isForward']) { + $isForward = 'forward'; + } else { + $isForward = 'backward'; + } + } + + foreach ($transition->getTos() as $to) { + $meta = $workflow->getMetadataStore()->getPlaceMetadata($to); + + if ( + !\array_key_exists('isFinal', $meta) || false === $meta['isFinal'] + ) { + $toFinal = false; + } + } + + return [ + 'data-is-transition' => 'data-is-transition', + 'data-to-final' => $toFinal ? '1' : '0', + 'data-is-forward' => $isForward, + ]; + }, + ]) + ->add('futureDestUsers', PickUserDynamicType::class, [ + 'label' => 'workflow.dest for next steps', + 'multiple' => true, + 'suggested' => $options['suggested_users'], + ]) + ->add('futureCcUsers', PickUserDynamicType::class, [ + 'label' => 'workflow.cc for next steps', + 'multiple' => true, + 'required' => false, + 'suggested' => $options['suggested_users'], + ]) + ->add('futureDestEmails', ChillCollectionType::class, [ + 'label' => 'workflow.dest by email', + 'help' => 'workflow.dest by email help', + 'allow_add' => true, + 'entry_type' => EmailType::class, + 'button_add_label' => 'workflow.Add an email', + 'button_remove_label' => 'workflow.Remove an email', + 'empty_collection_explain' => 'workflow.Any email', + 'entry_options' => [ + 'constraints' => [ + new NotNull(), new NotBlank(), new Email(), + ], + 'label' => 'Email', + ], + ]); $builder ->add('comment', ChillTextareaType::class, [ 'required' => false, 'label' => 'Comment', + 'empty_data' => '', ]); } public function configureOptions(OptionsResolver $resolver) { $resolver - ->setDefined('class') - ->setRequired('transition') - ->setAllowedTypes('transition', 'bool') + ->setDefault('data_class', WorkflowTransitionContextDTO::class) ->setRequired('entity_workflow') ->setAllowedTypes('entity_workflow', EntityWorkflow::class) ->setDefault('suggested_users', []) ->setDefault('constraints', [ new Callback( - function ($step, ExecutionContextInterface $context, $payload) { - /** @var EntityWorkflowStep $step */ - $form = $context->getObject(); - $workflow = $this->registry->get($step->getEntityWorkflow(), $step->getEntityWorkflow()->getWorkflowName()); - $transition = $form['transition']->getData(); + function (WorkflowTransitionContextDTO $step, ExecutionContextInterface $context, $payload) { + $workflow = $this->registry->get($step->entityWorkflow, $step->entityWorkflow->getWorkflowName()); + $transition = $step->transition; $toFinal = true; if (null === $transition) { @@ -212,8 +192,8 @@ class WorkflowStepType extends AbstractType $toFinal = false; } } - $destUsers = $form['future_dest_users']->getData(); - $destEmails = $form['future_dest_emails']->getData(); + $destUsers = $step->futureDestUsers; + $destEmails = $step->futureDestEmails; if (!$toFinal && [] === $destUsers && [] === $destEmails) { $context @@ -224,20 +204,6 @@ class WorkflowStepType extends AbstractType } } ), - new Callback( - function ($step, ExecutionContextInterface $context, $payload) { - $form = $context->getObject(); - - foreach ($form->get('future_dest_users')->getData() as $u) { - if (in_array($u, $form->get('future_cc_users')->getData(), true)) { - $context - ->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step') - ->atPath('ccUsers') - ->addViolation(); - } - } - } - ), ]); } } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig index 0056c503d..35f587e38 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig @@ -58,17 +58,15 @@ {{ form_row(transition_form.transition) }} - {% if transition_form.freezeAfter is defined %} - {{ form_row(transition_form.freezeAfter) }} - {% endif %} -
- {{ form_row(transition_form.future_dest_users) }} + {{ form_row(transition_form.futureDestUsers) }} + {{ form_errors(transition_form.futureDestUsers) }} - {{ form_row(transition_form.future_cc_users) }} + {{ form_row(transition_form.futureCcUsers) }} + {{ form_errors(transition_form.futureCcUsers) }} - {{ form_row(transition_form.future_dest_emails) }} - {{ form_errors(transition_form.future_dest_users) }} + {{ form_row(transition_form.futureDestEmails) }} + {{ form_errors(transition_form.futureDestEmails) }}

{{ form_label(transition_form.comment) }}

diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php index f515490b8..ec5fa6ae2 100644 --- a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Tests\Entity\Workflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use PHPUnit\Framework\TestCase; /** @@ -25,7 +26,7 @@ final class EntityWorkflowTest extends TestCase { $entityWorkflow = new EntityWorkflow(); - $entityWorkflow->setStep('final'); + $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow)); $entityWorkflow->getCurrentStep()->setIsFinal(true); $this->assertTrue($entityWorkflow->isFinal()); @@ -37,16 +38,16 @@ final class EntityWorkflowTest extends TestCase $this->assertFalse($entityWorkflow->isFinal()); - $entityWorkflow->setStep('two'); + $entityWorkflow->setStep('two', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertFalse($entityWorkflow->isFinal()); - $entityWorkflow->setStep('previous_final'); + $entityWorkflow->setStep('previous_final', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertFalse($entityWorkflow->isFinal()); $entityWorkflow->getCurrentStep()->setIsFinal(true); - $entityWorkflow->setStep('final'); + $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertTrue($entityWorkflow->isFinal()); } @@ -57,20 +58,20 @@ final class EntityWorkflowTest extends TestCase $this->assertFalse($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('step_one'); + $entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertFalse($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('step_three'); + $entityWorkflow->setStep('step_three', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertFalse($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('freezed'); + $entityWorkflow->setStep('freezed', new WorkflowTransitionContextDTO($entityWorkflow)); $entityWorkflow->getCurrentStep()->setFreezeAfter(true); $this->assertTrue($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('after_freeze'); + $entityWorkflow->setStep('after_freeze', new WorkflowTransitionContextDTO($entityWorkflow)); $this->assertTrue($entityWorkflow->isFreeze()); diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php new file mode 100644 index 000000000..a32cefb09 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php @@ -0,0 +1,61 @@ +buildMarkingStore(); + $workflow = new EntityWorkflow(); + + $marking = $markingStore->getMarking($workflow); + + self::assertEquals(['initial' => 1], $marking->getPlaces()); + } + + public function testSetMarking(): void + { + $markingStore = $this->buildMarkingStore(); + $workflow = new EntityWorkflow(); + + $dto = new WorkflowTransitionContextDTO($workflow); + $dto->futureCcUsers[] = $user1 = new User(); + $dto->futureDestUsers[] = $user2 = new User(); + $dto->futureDestEmails[] = $email = 'test@example.com'; + + $markingStore->setMarking($workflow, new Marking(['foo' => 1]), ['context' => $dto]); + + $currentStep = $workflow->getCurrentStep(); + self::assertEquals('foo', $currentStep->getCurrentStep()); + self::assertContains($email, $currentStep->getDestEmail()); + self::assertContains($user1, $currentStep->getCcUser()); + self::assertContains($user2, $currentStep->getDestUser()); + } + + private function buildMarkingStore(): EntityWorkflowMarkingStore + { + return new EntityWorkflowMarkingStore(); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php index 1d185ba0e..e79982e1c 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php @@ -49,6 +49,4 @@ interface EntityWorkflowHandlerInterface public function isObjectSupported(object $object): bool; public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool; - - public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool; } diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php new file mode 100644 index 000000000..dca929e86 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php @@ -0,0 +1,49 @@ +getCurrentStep(); + + return new Marking([$step->getCurrentStep() => 1]); + } + + public function setMarking(object $subject, Marking $marking, array $context = []): void + { + if (!$subject instanceof EntityWorkflow) { + throw new \UnexpectedValueException('Expected instance of EntityWorkflow'); + } + + $places = $marking->getPlaces(); + if (1 < count($places)) { + throw new \LogicException('Expected maximum one place'); + } + $next = array_keys($places)[0]; + + $transitionDTO = $context['context'] ?? null; + if (!$transitionDTO instanceof WorkflowTransitionContextDTO) { + throw new \UnexpectedValueException(sprintf('Expected instance of %s', WorkflowTransitionContextDTO::class)); + } + + $subject->setStep($next, $transitionDTO); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php index b1fa80458..ce33ea6d0 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php @@ -21,31 +21,13 @@ use Symfony\Component\Workflow\Event\Event; use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Component\Workflow\TransitionBlocker; -class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface +final readonly class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface { - public function __construct(private readonly LoggerInterface $chillLogger, private readonly Security $security, private readonly UserRender $userRender) {} - - public function addDests(Event $event): void - { - if (!$event->getSubject() instanceof EntityWorkflow) { - return; - } - - /** @var EntityWorkflow $entityWorkflow */ - $entityWorkflow = $event->getSubject(); - - foreach ($entityWorkflow->futureCcUsers as $user) { - $entityWorkflow->getCurrentStep()->addCcUser($user); - } - - foreach ($entityWorkflow->futureDestUsers as $user) { - $entityWorkflow->getCurrentStep()->addDestUser($user); - } - - foreach ($entityWorkflow->futureDestEmails as $email) { - $entityWorkflow->getCurrentStep()->addDestEmail($email); - } - } + public function __construct( + private LoggerInterface $chillLogger, + private Security $security, + private UserRender $userRender + ) {} public static function getSubscribedEvents(): array { @@ -53,7 +35,6 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac 'workflow.transition' => 'onTransition', 'workflow.completed' => [ ['markAsFinal', 2048], - ['addDests', 2048], ], 'workflow.guard' => [ ['guardEntityWorkflow', 0], @@ -99,6 +80,10 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac public function markAsFinal(Event $event): void { + // NOTE: it is not possible to move this method to the marking store, because + // there is dependency between the Workflow definition and the MarkingStoreInterface (the workflow + // constructor need a MarkingStoreInterface) + if (!$event->getSubject() instanceof EntityWorkflow) { return; } diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php index e290a567e..386b2eff6 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php @@ -23,7 +23,13 @@ use Symfony\Component\Workflow\Registry; class NotificationOnTransition implements EventSubscriberInterface { - public function __construct(private readonly EntityManagerInterface $entityManager, private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Security $security, private readonly Registry $registry) {} + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly \Twig\Environment $engine, + private readonly MetadataExtractor $metadataExtractor, + private readonly Security $security, + private readonly Registry $registry + ) {} public static function getSubscribedEvents(): array { @@ -85,7 +91,10 @@ class NotificationOnTransition implements EventSubscriberInterface 'dest' => $subscriber, 'place' => $place, 'workflow' => $workflow, - 'is_dest' => \in_array($subscriber->getId(), array_map(static fn (User $u) => $u->getId(), $entityWorkflow->futureDestUsers), true), + 'is_dest' => \in_array($subscriber->getId(), array_map( + static fn (User $u) => $u->getId(), + $entityWorkflow->getCurrentStep()->getDestUser()->toArray() + ), true), ]; $notification = new Notification(); diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php index 90a6e19db..1a4b573d5 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/SendAccessKeyEventSubscriber.php @@ -20,7 +20,13 @@ use Symfony\Component\Workflow\Registry; class SendAccessKeyEventSubscriber { - public function __construct(private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry, private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MailerInterface $mailer) {} + public function __construct( + private readonly \Twig\Environment $engine, + private readonly MetadataExtractor $metadataExtractor, + private readonly Registry $registry, + private readonly EntityWorkflowManager $entityWorkflowManager, + private readonly MailerInterface $mailer + ) {} public function postPersist(EntityWorkflowStep $step): void { @@ -32,7 +38,7 @@ class SendAccessKeyEventSubscriber ); $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); - foreach ($entityWorkflow->futureDestEmails as $emailAddress) { + foreach ($step->getDestEmail() as $emailAddress) { $context = [ 'entity_workflow' => $entityWorkflow, 'dest' => $emailAddress, diff --git a/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php new file mode 100644 index 000000000..2a8253523 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php @@ -0,0 +1,74 @@ +futureDestUsers as $u) { + if (in_array($u, $this->futureCcUsers, true)) { + $context + ->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step') + ->atPath('ccUsers') + ->addViolation(); + } + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index 0fc13224e..2912d1c01 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php @@ -123,9 +123,4 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW { return AccompanyingPeriodWorkEvaluationDocument::class === $entityWorkflow->getRelatedEntityClass(); } - - public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool - { - return false; - } } diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php index 63b34d1dc..a041b3c37 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php @@ -109,9 +109,4 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH { return AccompanyingPeriodWorkEvaluation::class === $entityWorkflow->getRelatedEntityClass(); } - - public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool - { - return false; - } } diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php index 5c74e5b17..ce146a887 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php @@ -116,9 +116,4 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte { return AccompanyingPeriodWork::class === $entityWorkflow->getRelatedEntityClass(); } - - public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool - { - return false; - } } From 064dfc5a5655b619e5e0472644fb929769674a8c Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 12:48:18 +0200 Subject: [PATCH 058/375] Fix repositories to fetch entity linked to stored object getSingleResult() replaced by getOneOrNullResult() to\ avoid error being thrown. Fix naming of properties. --- .../Repository/ActivityRepository.php | 11 +++-------- .../AccompanyingCourseDocumentRepository.php | 4 ++-- .../Repository/PersonDocumentRepository.php | 4 ++-- .../ChillEventBundle/Repository/EventRepository.php | 9 ++------- ...mpanyingPeriodWorkEvaluationDocumentRepository.php | 2 -- .../AccompanyingPeriodWorkRepository.php | 4 ++-- 6 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index 47ecb66dc..4942eef16 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -102,20 +102,15 @@ class ActivityRepository extends ServiceEntityRepository implements AssociatedEn return $qb->getQuery()->getResult(); } - /** - * @throws NonUniqueResultException - * @throws NoResultException - */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?Activity { $qb = $this->createQueryBuilder('a'); $query = $qb - ->join('a.documents', 'ad') - ->join('ad.storedObject', 'so') - ->where('so.id = :storedObjectId') + ->leftJoin('a.documents', 'ad') + ->where('ad.id = :storedObjectId') ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getSingleResult(); + return $query->getOneOrNullResult(); } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 246c7f2d9..744afd424 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -49,11 +49,11 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->repository->createQueryBuilder('d'); - $query = $qb->where('d.storedObject = :storedObject') + $query = $qb->where('d.object = :storedObject') ->setParameter('storedObject', $storedObject) ->getQuery(); - return $query->getResult(); + return $query->getOneOrNullResult(); } public function find($id): ?AccompanyingCourseDocument diff --git a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php index 57a964867..d0ef4c9d9 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentRepository.php @@ -58,10 +58,10 @@ readonly class PersonDocumentRepository implements ObjectRepository, AssociatedE public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->repository->createQueryBuilder('d'); - $query = $qb->where('d.storedObject = :storedObject') + $query = $qb->where('d.object = :storedObject') ->setParameter('storedObject', $storedObject) ->getQuery(); - return $query->getResult(); + return $query->getOneOrNullResult(); } } diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index b720866f2..03ee366ac 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -38,21 +38,16 @@ class EventRepository implements ObjectRepository, AssociatedEntityToStoredObjec return $this->repository->createQueryBuilder($alias, $indexBy); } - /** - * @throws NonUniqueResultException - * @throws NoResultException - */ public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object { $qb = $this->createQueryBuilder('e'); $query = $qb ->join('e.documents', 'ed') - ->join('ed.storedObject', 'so') - ->where('so.id = :storedObjectId') + ->where('ed.id = :storedObjectId') ->setParameter('storedObjectId', $storedObject->getId()) ->getQuery(); - return $query->getSingleResult(); + return $query->getOneOrNullResult(); } public function find($id) diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 95e0075e2..59bb3f915 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -11,8 +11,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\AccompanyingPeriod; -use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index 43ecd5337..324c3c176 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -258,12 +258,12 @@ final readonly class AccompanyingPeriodWorkRepository implements ObjectRepositor { $qb = $this->repository->createQueryBuilder('acpw'); $query = $qb - ->join('acpw.evaluations', 'acpwe') + ->join('acpw.accompanyingPeriodWorkEvaluations', 'acpwe') ->join('acpwe.documents', 'acpwed') ->where('acpwed.storedObject = :storedObject') ->setParameter('storedObject', $storedObject) ->getQuery(); - return $query->getResult(); + return $query->getOneOrNullResult(); } } From 03800029c90c030b38e5e7c6c1b87ca7c7c295d1 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 12:49:29 +0200 Subject: [PATCH 059/375] Fix the import of StoredObjectVoterInterface --- .../DependencyInjection/ChillDocStoreExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php index 9f277e716..b156af1ea 100644 --- a/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php +++ b/src/Bundle/ChillDocStoreBundle/DependencyInjection/ChillDocStoreExtension.php @@ -14,7 +14,7 @@ namespace Chill\DocStoreBundle\DependencyInjection; use Chill\DocStoreBundle\Controller\StoredObjectApiController; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; -use ChillDocStoreBundle\Security\Authorization\StoredObjectVoterInterface; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; From c19c597ba06e6874eb681e94c7960a8bbb09e686 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 12:50:44 +0200 Subject: [PATCH 060/375] Fix checking of permissions within document_button_group --- .../Resources/views/List/list_item.html.twig | 4 ++-- .../Security/Authorization/StoredObjectVoter.php | 2 ++ .../Templating/WopiEditTwigExtensionRuntime.php | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig index 58504b095..2a3592d73 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig @@ -71,7 +71,7 @@ {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
  • - {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }} + {{ document.object|chill_document_button_group(document.title) }}
  • @@ -90,7 +90,7 @@ {% else %} {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
  • - {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }} + {{ document.object|chill_document_button_group(document.title) }}
  • diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 3bb5fa396..57688e05b 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -37,6 +37,7 @@ class StoredObjectVoter extends Voter protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool { /** @var StoredObject $subject */ + /* if ( !$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT) || $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT) @@ -47,6 +48,7 @@ class StoredObjectVoter extends Voter if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) { return false; } + */ $attributeAsEnum = StoredObjectRoleEnum::from($attribute); diff --git a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php index be45bd2d4..b6290049c 100644 --- a/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php +++ b/src/Bundle/ChillDocStoreBundle/Templating/WopiEditTwigExtensionRuntime.php @@ -150,9 +150,9 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt * @throws \Twig\Error\RuntimeError * @throws \Twig\Error\SyntaxError */ - public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string + public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $showEditButtons = true, array $options = []): string { - $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT, $document); + $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $document) && $showEditButtons; $accessToken = $this->davTokenProvider->createToken( $document, From a9f4f8c973cc9d4ef797c7108955e03eefb0a13d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 14:17:05 +0200 Subject: [PATCH 061/375] Resolve phpstan erorrs --- .../Service/WorkflowDocumentService.php | 2 +- .../AccompanyingCourseDocumentWorkflowHandler.php | 9 ++++----- .../Form/ChoiceLoader/EventChoiceLoader.php | 6 ++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index 17591b2d0..a279912a9 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -23,7 +23,7 @@ class WorkflowDocumentService $currentUser = $this->security->getUser(); - if (null !== $workflow) { + if (null != $workflow) { if ($workflow->isFinal()) { return false; } diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index e3f201914..d1a384ef3 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -22,14 +22,13 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Symfony\Contracts\Translation\TranslatorInterface; -class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface +readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface { public function __construct( - EntityManagerInterface $em, - private readonly TranslatorInterface $translator, - private readonly EntityWorkflowRepository $workflowRepository, - private readonly AccompanyingCourseDocumentRepository $repository + private TranslatorInterface $translator, + private EntityWorkflowRepository $workflowRepository, + private AccompanyingCourseDocumentRepository $repository ) { } diff --git a/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php b/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php index 9aa001170..19829a369 100644 --- a/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php +++ b/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\EventBundle\Form\ChoiceLoader; use Chill\EventBundle\Entity\Event; +use Chill\EventBundle\Repository\EventRepository; use Doctrine\ORM\EntityRepository; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; @@ -26,9 +27,6 @@ class EventChoiceLoader implements ChoiceLoaderInterface */ protected $centers = []; - /** - * @var EntityRepository - */ protected $eventRepository; /** @@ -40,7 +38,7 @@ class EventChoiceLoader implements ChoiceLoaderInterface * EventChoiceLoader constructor. */ public function __construct( - EntityRepository $eventRepository, + EventRepository $eventRepository, ?array $centers = null ) { $this->eventRepository = $eventRepository; From 3262a1dd02df05c5f5f5763599f5cfd83dbdc831 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 15:35:41 +0200 Subject: [PATCH 062/375] Implement StoredObject permissions in AsyncUploadVoter.php --- .../TempUrlOpenstackGenerator.php | 2 +- .../AsyncUpload/SignedUrl.php | 1 + .../Security/Authorization/AsyncUploadVoter.php | 16 +++++++++------- .../TempUrlOpenstackGeneratorTest.php | 3 ++- .../Templating/AsyncUploadExtensionTest.php | 2 +- .../Controller/AsyncUploadControllerTest.php | 3 ++- .../Normalizer/SignedUrlNormalizerTest.php | 3 ++- .../Tests/Service/StoredObjectManagerTest.php | 3 ++- .../Constraints/AsyncFileExistsValidatorTest.php | 2 +- 9 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php index e410529b4..06f660fbe 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php @@ -127,7 +127,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf ]; $url = $url.'?'.\http_build_query($args); - $signature = new SignedUrl(strtoupper($method), $url, $expires); + $signature = new SignedUrl(strtoupper($method), $url, $expires, $object_name); $this->event_dispatcher->dispatch( new TempUrlGenerateEvent($signature) diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php index aba289652..1047e1344 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php @@ -21,6 +21,7 @@ readonly class SignedUrl #[Serializer\Groups(['read'])] public string $url, public \DateTimeImmutable $expires, + public string $object_name, ) {} #[Serializer\Groups(['read'])] diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php index d458bae08..c92dffcd5 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\AsyncUpload\SignedUrl; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\StoredObjectRepository; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Security; @@ -22,6 +24,7 @@ final class AsyncUploadVoter extends Voter public function __construct( private readonly Security $security, + private readonly StoredObjectRepository $storedObjectRepository ) {} protected function supports($attribute, $subject): bool @@ -36,13 +39,12 @@ final class AsyncUploadVoter extends Voter return false; } - //TODO get the StoredObject from the SignedUrl -/* match($subject->method) { - 'GET' => $this->security->isGranted('SEE', $storedObject), - 'PUT' => $this->security->isGranted('EDIT', $storedObject), - 'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') - };*/ + $storedObject = $this->storedObjectRepository->findOneBy(['filename' => $subject->object_name]); - return $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN'); + return match($subject->method) { + 'GET' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject), + 'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject), + default => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') + }; } } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php index e6250202f..f9cc49fa6 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php @@ -122,7 +122,8 @@ class TempUrlOpenstackGeneratorTest extends TestCase $signedUrl = new SignedUrl( 'GET', '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) { diff --git a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Templating/AsyncUploadExtensionTest.php b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Templating/AsyncUploadExtensionTest.php index e309c02ec..d39809a12 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Templating/AsyncUploadExtensionTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Templating/AsyncUploadExtensionTest.php @@ -35,7 +35,7 @@ class AsyncUploadExtensionTest extends KernelTestCase { $generator = $this->prophesize(TempUrlGeneratorInterface::class); $generator->generate(Argument::in(['GET', 'POST']), Argument::type('string'), Argument::any()) - ->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'))); + ->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'), $args[1])); $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); $urlGenerator->generate('async_upload.generate_url', Argument::type('array')) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php index c378ba989..da223cea6 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php @@ -87,7 +87,8 @@ class AsyncUploadControllerTest extends TestCase return new SignedUrl( $method, 'https://object.store.example', - new \DateTimeImmutable('1 hour') + new \DateTimeImmutable('1 hour'), + $object_name ); } }; diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php index cb5294fff..2029fac74 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php @@ -38,7 +38,8 @@ class SignedUrlNormalizerTest extends KernelTestCase $signedUrl = new SignedUrl( 'GET', 'https://object.store.example/container/object', - \DateTimeImmutable::createFromFormat('U', '1700000') + \DateTimeImmutable::createFromFormat('U', '1700000'), + 'object' ); $actual = self::$normalizer->normalize($signedUrl, 'json', [AbstractNormalizer::GROUPS => ['read']]); diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php index 531d19592..c5cc8185e 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php @@ -202,7 +202,8 @@ final class StoredObjectManagerTest extends TestCase $response = new SignedUrl( 'PUT', 'https://example.com/'.$storedObject->getFilename(), - new \DateTimeImmutable('1 hours') + new \DateTimeImmutable('1 hours'), + $storedObject->getFilename() ); $tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class); diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Validator/Constraints/AsyncFileExistsValidatorTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Validator/Constraints/AsyncFileExistsValidatorTest.php index 6e46ee370..ac0f4a6e7 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Validator/Constraints/AsyncFileExistsValidatorTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Validator/Constraints/AsyncFileExistsValidatorTest.php @@ -43,7 +43,7 @@ class AsyncFileExistsValidatorTest extends ConstraintValidatorTestCase $generator = $this->prophesize(TempUrlGeneratorInterface::class); $generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any()) - ->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'))); + ->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours'), $args[1])); return new AsyncFileExistsValidator($generator->reveal(), $client); } From 345f379650a6ae7ab813d387d432de118ab88faf Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 15:39:31 +0200 Subject: [PATCH 063/375] Implement StoredObject permissions WOPI AuthorizationManager.php --- .../src/Service/Wopi/AuthorizationManager.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php index 9ed421461..9b70ea049 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\WopiBundle\Service\Wopi; use ChampsLibres\WopiLib\Contract\Entity\Document; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\MainBundle\Entity\User; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Psr\Http\Message\RequestInterface; @@ -60,12 +61,17 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanPresent(string $accessToken, Document $document, RequestInterface $request): bool { - return $this->isTokenValid($accessToken, $document, $request); + if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $document)) { + + return $this->isTokenValid($accessToken, $document, $request); + } + + return false; } public function userCanRead(string $accessToken, Document $document, RequestInterface $request): bool { - if ($this->security->isGranted('SEE', $document)) { + if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $document)) { return $this->isTokenValid($accessToken, $document, $request); } @@ -79,7 +85,7 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanWrite(string $accessToken, Document $document, RequestInterface $request): bool { - if ($this->security->isGranted('EDIT', $document)) { + if ($this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $document)) { return $this->isTokenValid($accessToken, $document, $request); } From 3d7c8596eeb0bd32c92873c6381978462863f5f9 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 2 Jul 2024 15:49:53 +0200 Subject: [PATCH 064/375] Pass StoredObject instead of Document to check permission in AuthorizationManager.php --- .../src/Service/Wopi/AuthorizationManager.php | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php index 9ed421461..af78eb73c 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\WopiBundle\Service\Wopi; use ChampsLibres\WopiLib\Contract\Entity\Document; +use Chill\DocStoreBundle\Repository\StoredObjectRepository; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\MainBundle\Entity\User; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Psr\Http\Message\RequestInterface; @@ -19,13 +21,18 @@ use Symfony\Component\Security\Core\Security; class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\AuthorizationManagerInterface { - public function __construct(private readonly JWTTokenManagerInterface $tokenManager, private readonly Security $security) {} + public function __construct(private readonly JWTTokenManagerInterface $tokenManager, private readonly Security $security, private readonly StoredObjectRepository $storedObjectRepository) {} public function isRestrictedWebViewOnly(string $accessToken, Document $document, RequestInterface $request): bool { return false; } + public function getRelatedStoredObject(Document $document) + { + return $this->storedObjectRepository->findOneBy(['uuid' => $document->getWopiDocId()]); + } + public function isTokenValid(string $accessToken, Document $document, RequestInterface $request): bool { $metadata = $this->tokenManager->parse($accessToken); @@ -60,12 +67,21 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanPresent(string $accessToken, Document $document, RequestInterface $request): bool { - return $this->isTokenValid($accessToken, $document, $request); + $storedObject = $this->getRelatedStoredObject($document); + + if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { + + return $this->isTokenValid($accessToken, $document, $request); + } + + return false; } public function userCanRead(string $accessToken, Document $document, RequestInterface $request): bool { - if ($this->security->isGranted('SEE', $document)) { + $storedObject = $this->getRelatedStoredObject($document); + + if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { return $this->isTokenValid($accessToken, $document, $request); } @@ -79,7 +95,9 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori public function userCanWrite(string $accessToken, Document $document, RequestInterface $request): bool { - if ($this->security->isGranted('EDIT', $document)) { + $storedObject = $this->getRelatedStoredObject($document); + + if ($this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) { return $this->isTokenValid($accessToken, $document, $request); } From e9a9a3430f86c95f73cd5b4de9f2c77f007b50ae Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:27:16 +0200 Subject: [PATCH 065/375] Complete AbstractStoredObjectVoterTest.php --- .../AbstractStoredObjectVoterTest.php | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index d5ab471ab..c384e48b9 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -17,10 +17,9 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase { - private $repository; - private $security; - private $workflowDocumentService; - private $voter; + private AssociatedEntityToStoredObjectInterface $repository; + private Security $security; + private WorkflowDocumentService $workflowDocumentService; protected function setUp(): void { @@ -54,10 +53,10 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase private function setupMockObjects(): array { - $user = $this->createMock(User::class); + $user = new User(); $token = $this->createMock(TokenInterface::class); - $subject = $this->createMock(StoredObject::class); - $entity = $this->createMock(AccompanyingCourseDocument::class); + $subject = new StoredObject(); + $entity = new \stdClass(); return [$user, $token, $subject, $entity]; } @@ -90,9 +89,7 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true); // The voteOnAttribute method should return True when workflow is allowed - $attributeSee = StoredObjectRoleEnum::SEE; - $attributeEdit = StoredObjectRoleEnum::EDIT; - $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $subject, $token)); + self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token)); } public function testVoteOnAttributeNotAllowed(): void @@ -101,11 +98,10 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase // 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 - $attributeSee = StoredObjectRoleEnum::SEE; - $attributeEdit = StoredObjectRoleEnum::EDIT; - $this->assertTrue($this->voter->voteOnAttribute($attributeSee, $subject, $token)); + self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token)); } public function testVoteOnAttributeWhenBlockedByWorkflow(): void @@ -114,10 +110,11 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase // 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 = $this->voter->voteOnAttribute($attribute, $subject, $token); + $result = $voter->voteOnAttribute($attribute, $subject, $token); // Assert that access is denied when workflow is not allowed $this->assertFalse($result); From 719fabc87895554ad6dcd88a69155286efae027d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:27:54 +0200 Subject: [PATCH 066/375] Check permissions within StoredObjectNormalizer.php --- .../Serializer/Normalizer/StoredObjectNormalizer.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php index f6eb5a1cb..4a0ba6dab 100644 --- a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php +++ b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php @@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -32,7 +33,8 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa public function __construct( 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 = []) From 5d57ec8a3b533e1c07bc060544997784b8047ad4 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:38:13 +0200 Subject: [PATCH 067/375] Complete AbstractStoredObjectVoterTest.php --- .../AbstractStoredObjectVoterTest.php | 81 ++++++++++++------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index c384e48b9..24370c2db 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -1,21 +1,33 @@ repository = $this->createMock(AccompanyingCourseDocumentRepository::class); + $this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class); $this->security = $this->createMock(Security::class); $this->workflowDocumentService = $this->createMock(WorkflowDocumentService::class); + } + private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowDocumentService $workflowDocumentService = null): AbstractStoredObjectVoter + { // Anonymous class extending the abstract class - $this->voter = new class($this->repository, $this->security, $this->workflowDocumentService) extends AbstractStoredObjectVoter { + return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter { + public function __construct( + private bool $canBeAssociatedWithWorkflow, + private AssociatedEntityToStoredObjectInterface $repository, + Security $security, + ?WorkflowDocumentService $workflowDocumentService = null + ) { + parent::__construct($security, $workflowDocumentService); + } + protected function attributeToRole($attribute): string { - return AccompanyingCourseDocumentVoter::SEE_DETAILS; + return 'SOME_ROLE'; } protected function getRepository(): AssociatedEntityToStoredObjectInterface { - // TODO: Implement getRepository() method. + return $this->repository; } protected function getClass(): string { - // TODO: Implement getClass() method. + return \stdClass::class; } protected function canBeAssociatedWithWorkflow(): bool { - // TODO: Implement canBeAssociatedWithWorkflow() method. + return $this->canBeAssociatedWithWorkflow; } }; } @@ -61,7 +85,7 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase return [$user, $token, $subject, $entity]; } - private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForAccCourseDocument, AccompanyingCourseDocument $entity, bool $workflowAllowed): void + 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); @@ -69,24 +93,31 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase // Mock the return of an AccompanyingCourseDocument by the repository $this->repository->method('findAssociatedEntityToStoredObject')->willReturn($entity); - // Mock attributeToRole to return appropriate role - $this->voter->method('attributeToRole')->willReturn(AccompanyingCourseDocumentVoter::SEE_DETAILS); - // Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument - $this->security->method('isGranted')->willReturnMap([ - [[AccompanyingCourseDocumentVoter::SEE_DETAILS, $entity], $isGrantedForAccCourseDocument], - ]); + $this->security->method('isGranted')->willReturn($isGrantedForEntity); // Mock case where user is blocked or not by workflow $this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed); } - public function testVoteOnAttributeAllowed(): void + public function testSupportsOnAttribute(): void { list($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 + { + list($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)); @@ -104,7 +135,7 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token)); } - public function testVoteOnAttributeWhenBlockedByWorkflow(): void + public function testVoteOnAttributeAllowedWorkflowNotAllowed(): void { list($user, $token, $subject, $entity) = $this->setupMockObjects(); @@ -119,16 +150,4 @@ class AbstractStoredObjectVoterTest extends PHPUnit\Framework\TestCase // Assert that access is denied when workflow is not allowed $this->assertFalse($result); } - - public function testAbstractStoredObjectVoter(): void - { - $voter = new class extends AbstractStoredObjectVoter { - // Implement abstract methods here - public function someMethod() { - // method implementation - } - }; - // Run tests on $voter - } - } From 428494ca1fe46cde4be97ad238b64447b41cc7cd Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:38:41 +0200 Subject: [PATCH 068/375] Implement stored object permissions in serialization --- .../Serializer/Normalizer/StoredObjectNormalizer.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php index 4a0ba6dab..17597901d 100644 --- a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php +++ b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php @@ -57,13 +57,13 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa // deprecated property $datas['creationDate'] = $datas['createdAt']; - $canDavSee = in_array(self::ADD_DAV_SEE_LINK_CONTEXT, $context['groups'] ?? [], true); - $canDavEdit = in_array(self::ADD_DAV_EDIT_LINK_CONTEXT, $context['groups'] ?? [], true); + $canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE, $object); + $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT, $object); - if ($canDavSee || $canDavEdit) { + if ($canSee || $canEdit) { $accessToken = $this->JWTDavTokenProvider->createToken( $object, - $canDavEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE + $canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE ); $datas['_links'] = [ From 21b79c198106aedf3a542d005c58acbda5a7764e Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:39:02 +0200 Subject: [PATCH 069/375] Php cs fixes --- .../Repository/ActivityRepository.php | 2 -- .../ActivityStoredObjectVoter.php | 16 +++++++++------ .../AccompanyingCourseDocumentRepository.php | 1 - ...ssociatedEntityToStoredObjectInterface.php | 9 +++++++++ .../Authorization/AsyncUploadVoter.php | 3 +-- .../Authorization/StoredObjectVoter.php | 4 +--- .../StoredObjectVoterInterface.php | 6 ++---- .../AbstractStoredObjectVoter.php | 20 ++++++++++++------- ...panyingCourseDocumentStoredObjectVoter.php | 12 +++++++++-- .../AccompanyingCourseStoredObjectVoter.php | 11 +++++++++- ...gPeriodWorkEvaluationStoredObjectVoter.php | 15 +++++++++----- .../ActivityStoredObjectVoter.php | 15 +++++++++----- .../EventStoredObjectVoter.php | 14 +++++++++---- .../PersonDocumentStoredObjectVoter.php | 15 +++++++++----- .../PersonStoredObjectVoter.php | 14 +++++++++---- .../Service/WorkflowDocumentService.php | 17 +++++++++------- ...ompanyingCourseDocumentWorkflowHandler.php | 12 ++++------- .../Form/ChoiceLoader/EventChoiceLoader.php | 1 - .../Repository/EventRepository.php | 2 -- .../Authorization/EventStoredObjectVoter.php | 15 +++++++++----- .../Workflow/EntityWorkflowRepository.php | 1 - .../Workflow/EntityWorkflowManager.php | 2 +- ...nyingPeriodWorkEvaluationDocumentVoter.php | 1 - ...dWorkEvaluationDocumentWorkflowHandler.php | 1 + .../AccompanyingPeriodWorkWorkflowHandler.php | 1 + .../src/Service/Wopi/AuthorizationManager.php | 3 --- 26 files changed, 133 insertions(+), 80 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php index 4942eef16..ff4904f9e 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityRepository.php @@ -17,8 +17,6 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\ORM\NonUniqueResultException; -use Doctrine\ORM\NoResultException; use Doctrine\Persistence\ManagerRegistry; /** diff --git a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index 01ec68bd9..be3013a08 100644 --- a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -1,11 +1,18 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return Activity::class; diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 744afd424..7033a53c9 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -80,5 +80,4 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat { return AccompanyingCourseDocument::class; } - } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php index 5349251c5..81d230e67 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AssociatedEntityToStoredObjectInterface.php @@ -1,5 +1,14 @@ storedObjectRepository->findOneBy(['filename' => $subject->object_name]); - return match($subject->method) { + return match ($subject->method) { 'GET' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject), 'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject), default => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index 57688e05b..c0851144a 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -24,9 +24,7 @@ use Symfony\Component\Security\Core\Security; */ class StoredObjectVoter extends Voter { - - public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters) { - } + public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters) {} protected function supports($attribute, $subject): bool { diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php index 516722654..97d45eec3 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoterInterface.php @@ -12,13 +12,11 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -interface StoredObjectVoterInterface { - +interface StoredObjectVoterInterface +{ public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool; public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool; - } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php index 659d7d28a..24def7ca9 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php @@ -1,14 +1,21 @@ getClass(); + return $this->getRepository()->findAssociatedEntityToStoredObject($subject) instanceof $class; } @@ -52,7 +58,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface if ($this->canBeAssociatedWithWorkflow()) { if (null === $this->workflowDocumentService) { - throw new \LogicException("Provide a workflow document service"); + throw new \LogicException('Provide a workflow document service'); } return $this->workflowDocumentService->notBlockedByWorkflow($entity); diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php index 86e7bd9de..2665553e8 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php @@ -1,5 +1,14 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return AccompanyingPeriodWork::class; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php index a36535005..4f897b36c 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php @@ -1,9 +1,17 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return Activity::class; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php index 218ce1980..d258b3ae3 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php @@ -1,5 +1,14 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return Event::class; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php index a40d08849..5791d724b 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php @@ -1,5 +1,14 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return PersonDocument::class; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php index 6c3b0b807..4d8a1220f 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php @@ -1,5 +1,14 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return PersonDocument::class; diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index a279912a9..78c7b7b7d 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -1,18 +1,23 @@ entityWorkflowManager->findByRelatedEntity($entity); $currentUser = $this->security->getUser(); - if (null != $workflow) { if ($workflow->isFinal()) { return false; @@ -35,5 +39,4 @@ class WorkflowDocumentService return true; } - } diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index d1a384ef3..74e51b19e 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -18,19 +18,15 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityRepository; use Symfony\Contracts\Translation\TranslatorInterface; readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface { - public function __construct( - private TranslatorInterface $translator, - private EntityWorkflowRepository $workflowRepository, + private TranslatorInterface $translator, + private EntityWorkflowRepository $workflowRepository, private AccompanyingCourseDocumentRepository $repository - ) { - } + ) {} public function getDeletionRoles(): array { @@ -127,7 +123,7 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl public function findByRelatedEntity(object $object): ?EntityWorkflow { - if(!$object instanceof AccompanyingCourseDocument) { + if (!$object instanceof AccompanyingCourseDocument) { return null; } diff --git a/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php b/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php index 19829a369..3fe305ff7 100644 --- a/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php +++ b/src/Bundle/ChillEventBundle/Form/ChoiceLoader/EventChoiceLoader.php @@ -13,7 +13,6 @@ namespace Chill\EventBundle\Form\ChoiceLoader; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Repository\EventRepository; -use Doctrine\ORM\EntityRepository; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; diff --git a/src/Bundle/ChillEventBundle/Repository/EventRepository.php b/src/Bundle/ChillEventBundle/Repository/EventRepository.php index 03ee366ac..89723b770 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventRepository.php @@ -16,8 +16,6 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\EventBundle\Entity\Event; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\ORM\NonUniqueResultException; -use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php index 9db6177c8..8dda94aad 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php +++ b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php @@ -1,5 +1,14 @@ repository; } - /** - * @inheritDoc - */ protected function getClass(): string { return Event::class; diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php index 7d2e047d3..dba68ee41 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -112,7 +112,6 @@ class EntityWorkflowRepository implements ObjectRepository ->setParameter('entity_id', $relatedEntityId); return $query->getQuery()->getResult(); - } /** diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php index 3f2ff0439..26c31506f 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php @@ -43,7 +43,7 @@ class EntityWorkflowManager foreach ($this->handlers as $handler) { return $handler->findByRelatedEntity($object); } + return null; } - } diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php index ecf3cd802..97ca84a13 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Security\Authorization; -use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index 0fd3af0af..34f8db4a7 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php @@ -140,6 +140,7 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW if (!$object instanceof AccompanyingPeriodWorkEvaluationDocument) { return null; } + return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWorkEvaluationDocument::class, $object->getId()); } } diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php index 81116b7b2..d40080e33 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php @@ -133,6 +133,7 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte if (!$object instanceof AccompanyingPeriodWork) { return null; } + return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWork::class, $object->getId()); } } diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php index d773a8eb4..4c9cb4a2f 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\WopiBundle\Service\Wopi; use ChampsLibres\WopiLib\Contract\Entity\Document; - use Chill\DocStoreBundle\Repository\StoredObjectRepository; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\MainBundle\Entity\User; @@ -70,7 +69,6 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori { $storedObject = $this->getRelatedStoredObject($document); if ($this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) { - return $this->isTokenValid($accessToken, $document, $request); } @@ -103,5 +101,4 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori return false; } - } From 2adc8b3bf6cdffad987d3c58a80d74f59bc7286a Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 11:58:48 +0200 Subject: [PATCH 070/375] Fix construct of SignedUrlPost --- .../Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php | 1 + src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php | 3 ++- .../OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php | 1 + .../Tests/Controller/AsyncUploadControllerTest.php | 1 + .../Serializer/Normalizer/SignedUrlPostNormalizerTest.php | 2 ++ 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php index 06f660fbe..ae74f9d0e 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGenerator.php @@ -89,6 +89,7 @@ final readonly class TempUrlOpenstackGenerator implements TempUrlGeneratorInterf $g = new SignedUrlPost( $url = $this->generateUrl($object_name), $expires, + $object_name, $this->max_post_file_size, $max_file_count, $submit_delay, diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php index 9d37771d1..e9aecf6b2 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrlPost.php @@ -18,6 +18,7 @@ readonly class SignedUrlPost extends SignedUrl public function __construct( string $url, \DateTimeImmutable $expires, + string $object_name, #[Serializer\Groups(['read'])] public int $max_file_size, #[Serializer\Groups(['read'])] @@ -31,6 +32,6 @@ readonly class SignedUrlPost extends SignedUrl #[Serializer\Groups(['read'])] public string $signature, ) { - parent::__construct('POST', $url, $expires); + parent::__construct('POST', $url, $expires, $object_name); } } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php index f9cc49fa6..bb83c698a 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/OpenstackObjectStore/TempUrlOpenstackGeneratorTest.php @@ -154,6 +154,7 @@ class TempUrlOpenstackGeneratorTest extends TestCase $signedUrl = new SignedUrlPost( 'https://objectstore.example/v1/my_account/container/object?temp_url_sig=0aeef353a5f6e22d125c76c6ad8c644a59b222ba1b13eaeb56bf3d04e28b081d11dfcb36601ab3aa7b623d79e1ef03017071bbc842fb7b34afec2baff895bf80&temp_url_expires=1702043543', \DateTimeImmutable::createFromFormat('U', '1702043543'), + $objectName, 150, 1, 1800, diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php index da223cea6..4c7dc92d0 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php @@ -73,6 +73,7 @@ class AsyncUploadControllerTest extends TestCase return new SignedUrlPost( 'https://object.store.example', new \DateTimeImmutable('1 hour'), + 'abc' 150, 1, 1800, diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php index ceb777f23..3bfa279e5 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php @@ -38,6 +38,7 @@ class SignedUrlPostNormalizerTest extends KernelTestCase $signedUrl = new SignedUrlPost( 'https://object.store.example/container/object', \DateTimeImmutable::createFromFormat('U', '1700000'), + 'abc', 15000, 1, 180, @@ -59,6 +60,7 @@ class SignedUrlPostNormalizerTest extends KernelTestCase 'method' => 'POST', 'expires' => 1_700_000, 'url' => 'https://object.store.example/container/object', + 'object_name' => 'abc' ], $actual ); From af4db2218404704b76d453dc502e40b94488a169 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 13:58:10 +0200 Subject: [PATCH 071/375] php cs fixer and rector: add missing comma in AsyncUploadControllerTest --- .../Tests/Controller/AsyncUploadControllerTest.php | 2 +- .../Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php index 4c7dc92d0..6b49b2919 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Controller/AsyncUploadControllerTest.php @@ -73,7 +73,7 @@ class AsyncUploadControllerTest extends TestCase return new SignedUrlPost( 'https://object.store.example', new \DateTimeImmutable('1 hour'), - 'abc' + 'abc', 150, 1, 1800, diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php index 3bfa279e5..69d7958b6 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlPostNormalizerTest.php @@ -60,7 +60,7 @@ class SignedUrlPostNormalizerTest extends KernelTestCase 'method' => 'POST', 'expires' => 1_700_000, 'url' => 'https://object.store.example/container/object', - 'object_name' => 'abc' + 'object_name' => 'abc', ], $actual ); From 435836c7d1ba38b853c151efe87f4f4b554d5514 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 15:46:18 +0200 Subject: [PATCH 072/375] Delete unused storedobject voter --- .../AccompanyingCourseStoredObjectVoter.php | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php deleted file mode 100644 index 040f4dc32..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseStoredObjectVoter.php +++ /dev/null @@ -1,55 +0,0 @@ -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; - } -} From 25e89571f7be08802c6e1aa4604dc5ce848b65ae Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 15:48:08 +0200 Subject: [PATCH 073/375] Change usage of match function in AsyncUploadVoter --- .../Security/Authorization/AsyncUploadVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php index ccda803e4..25981db1e 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php @@ -43,7 +43,7 @@ final class AsyncUploadVoter extends Voter return match ($subject->method) { 'GET' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject), 'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject), - default => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') + 'POST', 'HEAD' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') }; } } From 3b80d9a93b15d9981641ad7b80d7e301aeba1b6e Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 16:24:53 +0200 Subject: [PATCH 074/375] Delete voters that are not in use anymore --- .../ActivityStoredObjectVoter.php | 55 ------------------- .../EventStoredObjectVoter.php | 55 ------------------- .../PersonStoredObjectVoter.php | 55 ------------------- 3 files changed, 165 deletions(-) delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php delete mode 100644 src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php deleted file mode 100644 index 4f897b36c..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/ActivityStoredObjectVoter.php +++ /dev/null @@ -1,55 +0,0 @@ -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; - } -} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php deleted file mode 100644 index d258b3ae3..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/EventStoredObjectVoter.php +++ /dev/null @@ -1,55 +0,0 @@ -repository; - } - - protected function getClass(): string - { - return Event::class; - } - - protected function attributeToRole(StoredObjectRoleEnum $attribute): string - { - return match ($attribute) { - StoredObjectRoleEnum::EDIT => EventVoter::UPDATE, - StoredObjectRoleEnum::SEE => EventVoter::SEE_DETAILS, - }; - } - - protected function canBeAssociatedWithWorkflow(): bool - { - return false; - } -} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php deleted file mode 100644 index 4d8a1220f..000000000 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonStoredObjectVoter.php +++ /dev/null @@ -1,55 +0,0 @@ -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; - } -} From 7dd5f542a6296687ba99e9d922bc68718ef7cc37 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 4 Jul 2024 16:28:09 +0200 Subject: [PATCH 075/375] Fix serialization of SignedUrl An annotation was missing to include the object_name in the serialization. --- src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php index 1047e1344..ec0debca9 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/SignedUrl.php @@ -21,6 +21,7 @@ readonly class SignedUrl #[Serializer\Groups(['read'])] public string $url, public \DateTimeImmutable $expires, + #[Serializer\Groups(['read'])] public string $object_name, ) {} From f9122341d17ea2e300487ccffe84a79c3e07c9d2 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 9 Jul 2024 13:30:58 +0200 Subject: [PATCH 076/375] Fix phpstan error in match() function --- .../Security/Authorization/AsyncUploadVoter.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php index 25981db1e..708df0467 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/AsyncUploadVoter.php @@ -34,16 +34,16 @@ final class AsyncUploadVoter extends Voter protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool { /** @var SignedUrl $subject */ - if (!in_array($subject->method, ['POST', 'GET', 'HEAD'], true)) { + if (!in_array($subject->method, ['POST', 'GET', 'HEAD', 'PUT'], true)) { return false; } $storedObject = $this->storedObjectRepository->findOneBy(['filename' => $subject->object_name]); return match ($subject->method) { - 'GET' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject), + 'GET', 'HEAD' => $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject), 'PUT' => $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject), - 'POST', 'HEAD' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') + 'POST' => $this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN') }; } } From facc4affed1bbc66fef0cdf6bdc99645bf353e88 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 9 Jul 2024 14:57:14 +0200 Subject: [PATCH 077/375] Fix testNormalizerSignedUrl method fixed --- .../Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php index 2029fac74..253eac2ca 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php @@ -49,6 +49,7 @@ class SignedUrlNormalizerTest extends KernelTestCase 'method' => 'GET', 'expires' => 1_700_000, 'url' => 'https://object.store.example/container/object', + 'object_name' => 'object' ], $actual ); From cfa51cd6590f550206c3b810f52622f731cd920e Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 9 Jul 2024 15:43:22 +0200 Subject: [PATCH 078/375] php cs fixer --- .../Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php index 253eac2ca..7f0c342bb 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/SignedUrlNormalizerTest.php @@ -49,7 +49,7 @@ class SignedUrlNormalizerTest extends KernelTestCase 'method' => 'GET', 'expires' => 1_700_000, 'url' => 'https://object.store.example/container/object', - 'object_name' => 'object' + 'object_name' => 'object', ], $actual ); From 7f3de62b2c042deeae6df2e3da9a77f1fb78328a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 9 Jul 2024 23:36:12 +0200 Subject: [PATCH 079/375] Move the metadata on each workflow transition from the event subscriber to the entity EntityWorkflow::setStep method The main update is in the setStep method of EntityWorkflow, where parameters are added to capture the transition details. These include the exact transition, the user who made the transition and the time of transition. The WorkflowController extracts this information and put it into the transition's context. The MarkingStore transfer it to the EntityWorkflow::setStep method, and all metadata are recorded within the entities themselve. --- .../Controller/WorkflowController.php | 23 +++++++++- .../Entity/Workflow/EntityWorkflow.php | 9 +++- .../Entity/Workflow/EntityWorkflowTest.php | 44 +++++++++++++++---- .../EntityWorkflowMarkingStoreTest.php | 12 ++++- .../Workflow/EntityWorkflowMarkingStore.php | 6 ++- ...ntityWorkflowTransitionEventSubscriber.php | 6 --- 6 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php index 3a8626f26..6898d87e4 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php @@ -25,6 +25,7 @@ use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\HttpFoundation\Request; @@ -39,7 +40,18 @@ use Symfony\Contracts\Translation\TranslatorInterface; class WorkflowController extends AbstractController { - public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly ValidatorInterface $validator, private readonly PaginatorFactory $paginatorFactory, private readonly Registry $registry, private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, private readonly ChillSecurity $security, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry) {} + public function __construct( + private readonly EntityWorkflowManager $entityWorkflowManager, + private readonly EntityWorkflowRepository $entityWorkflowRepository, + private readonly ValidatorInterface $validator, + private readonly PaginatorFactory $paginatorFactory, + private readonly Registry $registry, + private readonly EntityManagerInterface $entityManager, + private readonly TranslatorInterface $translator, + private readonly ChillSecurity $security, + private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry, + private readonly ClockInterface $clock, + ) {} #[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')] public function create(Request $request): Response @@ -310,7 +322,14 @@ class WorkflowController extends AbstractController throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs))); } - $workflow->apply($entityWorkflow, $transition, ['context' => $stepDTO]); + $byUser = $this->security->getUser(); + + $workflow->apply($entityWorkflow, $transition, [ + 'context' => $stepDTO, + 'byUser' => $byUser, + 'transition' => $transition, + 'transitionAt' => $this->clock->now(), + ]); $this->entityManager->flush(); diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index 9c314115e..b1a93534d 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -413,8 +413,15 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface * * @return $this */ - public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO): self + public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO, string $transition, \DateTimeImmutable $transitionAt, ?User $byUser = null): self { + $previousStep = $this->getCurrentStep(); + + $previousStep + ->setTransitionAfter($transition) + ->setTransitionAt($transitionAt) + ->setTransitionBy($byUser); + $newStep = new EntityWorkflowStep(); $newStep->setCurrentStep($step); diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php index ec5fa6ae2..30003144f 100644 --- a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Tests\Entity\Workflow; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use PHPUnit\Framework\TestCase; @@ -26,7 +27,7 @@ final class EntityWorkflowTest extends TestCase { $entityWorkflow = new EntityWorkflow(); - $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow)); + $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow), 'finalize', new \DateTimeImmutable()); $entityWorkflow->getCurrentStep()->setIsFinal(true); $this->assertTrue($entityWorkflow->isFinal()); @@ -38,16 +39,16 @@ final class EntityWorkflowTest extends TestCase $this->assertFalse($entityWorkflow->isFinal()); - $entityWorkflow->setStep('two', new WorkflowTransitionContextDTO($entityWorkflow)); + $entityWorkflow->setStep('two', new WorkflowTransitionContextDTO($entityWorkflow), 'two', new \DateTimeImmutable()); $this->assertFalse($entityWorkflow->isFinal()); - $entityWorkflow->setStep('previous_final', new WorkflowTransitionContextDTO($entityWorkflow)); + $entityWorkflow->setStep('previous_final', new WorkflowTransitionContextDTO($entityWorkflow), 'three', new \DateTimeImmutable()); $this->assertFalse($entityWorkflow->isFinal()); $entityWorkflow->getCurrentStep()->setIsFinal(true); - $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow)); + $entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow), 'four', new \DateTimeImmutable()); $this->assertTrue($entityWorkflow->isFinal()); } @@ -58,23 +59,50 @@ final class EntityWorkflowTest extends TestCase $this->assertFalse($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow)); + $entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow), 'to_step_one', new \DateTimeImmutable()); $this->assertFalse($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('step_three', new WorkflowTransitionContextDTO($entityWorkflow)); + $entityWorkflow->setStep('step_three', new WorkflowTransitionContextDTO($entityWorkflow), 'to_step_three', new \DateTimeImmutable()); $this->assertFalse($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('freezed', new WorkflowTransitionContextDTO($entityWorkflow)); + $entityWorkflow->setStep('freezed', new WorkflowTransitionContextDTO($entityWorkflow), 'to_freezed', new \DateTimeImmutable()); $entityWorkflow->getCurrentStep()->setFreezeAfter(true); $this->assertTrue($entityWorkflow->isFreeze()); - $entityWorkflow->setStep('after_freeze', new WorkflowTransitionContextDTO($entityWorkflow)); + $entityWorkflow->setStep('after_freeze', new WorkflowTransitionContextDTO($entityWorkflow), 'to_after_freeze', new \DateTimeImmutable()); $this->assertTrue($entityWorkflow->isFreeze()); $this->assertTrue($entityWorkflow->getCurrentStep()->isFreezeAfter()); } + + public function testPreviousStepMetadataAreFilled() + { + $entityWorkflow = new EntityWorkflow(); + $initialStep = $entityWorkflow->getCurrentStep(); + + $entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow), 'to_step_one', new \DateTimeImmutable('2024-01-01'), $user1 = new User()); + + $previous = $entityWorkflow->getCurrentStep(); + + $entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow), 'to_step_two', new \DateTimeImmutable('2024-01-02'), $user2 = new User()); + + $final = $entityWorkflow->getCurrentStep(); + + $stepsChained = $entityWorkflow->getStepsChained(); + + self::assertCount(3, $stepsChained); + self::assertSame($initialStep, $stepsChained[0]); + self::assertSame($previous, $stepsChained[1]); + self::assertSame($final, $stepsChained[2]); + self::assertEquals($user1, $initialStep->getTransitionBy()); + self::assertEquals('2024-01-01', $initialStep->getTransitionAt()?->format('Y-m-d')); + self::assertEquals('to_step_one', $initialStep->getTransitionAfter()); + self::assertEquals($user2, $previous->getTransitionBy()); + self::assertEquals('2024-01-02', $previous->getTransitionAt()?->format('Y-m-d')); + self::assertEquals('to_step_two', $previous->getTransitionAfter()); + } } diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php index a32cefb09..4922cab17 100644 --- a/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/EntityWorkflowMarkingStoreTest.php @@ -39,19 +39,29 @@ class EntityWorkflowMarkingStoreTest extends TestCase { $markingStore = $this->buildMarkingStore(); $workflow = new EntityWorkflow(); + $previousStep = $workflow->getCurrentStep(); $dto = new WorkflowTransitionContextDTO($workflow); $dto->futureCcUsers[] = $user1 = new User(); $dto->futureDestUsers[] = $user2 = new User(); $dto->futureDestEmails[] = $email = 'test@example.com'; - $markingStore->setMarking($workflow, new Marking(['foo' => 1]), ['context' => $dto]); + $markingStore->setMarking($workflow, new Marking(['foo' => 1]), [ + 'context' => $dto, + 'transition' => 'bar_transition', + 'byUser' => $user3 = new User(), + 'transitionAt' => $at = new \DateTimeImmutable(), + ]); $currentStep = $workflow->getCurrentStep(); self::assertEquals('foo', $currentStep->getCurrentStep()); self::assertContains($email, $currentStep->getDestEmail()); self::assertContains($user1, $currentStep->getCcUser()); self::assertContains($user2, $currentStep->getDestUser()); + + self::assertSame($user3, $previousStep->getTransitionBy()); + self::assertSame($at, $previousStep->getTransitionAt()); + self::assertEquals('bar_transition', $previousStep->getTransitionAfter()); } private function buildMarkingStore(): EntityWorkflowMarkingStore diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php index dca929e86..ee07d7a62 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowMarkingStore.php @@ -40,10 +40,14 @@ final readonly class EntityWorkflowMarkingStore implements MarkingStoreInterface $next = array_keys($places)[0]; $transitionDTO = $context['context'] ?? null; + $transition = $context['transition']; + $byUser = $context['byUser'] ?? null; + $at = $context['transitionAt']; + if (!$transitionDTO instanceof WorkflowTransitionContextDTO) { throw new \UnexpectedValueException(sprintf('Expected instance of %s', WorkflowTransitionContextDTO::class)); } - $subject->setStep($next, $transitionDTO); + $subject->setStep($next, $transitionDTO, $transition, $at, $byUser); } } diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php index ce33ea6d0..9c74c861c 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php @@ -108,12 +108,6 @@ final readonly class EntityWorkflowTransitionEventSubscriber implements EventSub /** @var EntityWorkflow $entityWorkflow */ $entityWorkflow = $event->getSubject(); - $step = $entityWorkflow->getCurrentStep(); - - $step - ->setTransitionAfter($event->getTransition()->getName()) - ->setTransitionAt(new \DateTimeImmutable('now')) - ->setTransitionBy($this->security->getUser()); $this->chillLogger->info('[workflow] apply transition on entityWorkflow', [ 'relatedEntityClass' => $entityWorkflow->getRelatedEntityClass(), From b5af9f7b63ef42f31f2cc1a656034788a3dd4ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 9 Jul 2024 23:36:39 +0200 Subject: [PATCH 080/375] Add futurePersonSignatures property to WorkflowTransitionContextDTO A new property named futurePersonSignatures has been added to the WorkflowTransitionContextDTO class. This will hold a list of Person objects expected to sign the next step, improving the scope of information available within the workflow context. --- .../ChillMainBundle/Entity/Workflow/EntityWorkflow.php | 4 ++++ .../Workflow/WorkflowTransitionContextDTO.php | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index b1a93534d..4b5da09db 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -437,6 +437,10 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface $newStep->addDestEmail($email); } + foreach ($transitionContextDTO->futurePersonSignatures as $personSignature) { + new EntityWorkflowStepSignature($newStep, $personSignature); + } + // copy the freeze if ($this->isFreeze()) { $newStep->setFreezeAfter(true); diff --git a/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php index 2a8253523..ba4cde51e 100644 --- a/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php +++ b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php @@ -51,6 +51,11 @@ class WorkflowTransitionContextDTO */ public array $futureDestEmails = []; + /** + * a list of future @see{Person} with will sign the next step. + */ + public array $futurePersonSignatures = []; + public ?Transition $transition = null; public string $comment = ''; From fe6b4848e60deb3ce4f62001fba7d5f8415dc483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 9 Jul 2024 23:50:28 +0200 Subject: [PATCH 081/375] Implement workflow handlers for stored objects Added new interface, EntityWorkflowWithStoredObjectHandlerInterface, which provides methods to handle workflows associated with stored objects. Two classes, AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler and AccompanyingCourseDocumentWorkflowHandler, have been updated to implement this new interface. The EntityWorkflowManager class has also been updated to handle workflows associated with stored objects. --- ...ompanyingCourseDocumentWorkflowHandler.php | 15 ++++++++--- .../Workflow/EntityWorkflowManager.php | 12 +++++++++ ...rkflowWithStoredObjectHandlerInterface.php | 27 +++++++++++++++++++ ...dWorkEvaluationDocumentWorkflowHandler.php | 15 ++++++++--- 4 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Workflow/EntityWorkflowWithStoredObjectHandlerInterface.php diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index 55b823dcd..ad16ae722 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -12,15 +12,19 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Workflow; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; +use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; -use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; +use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Symfony\Contracts\Translation\TranslatorInterface; -class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface +/** + * @implements EntityWorkflowWithStoredObjectHandlerInterface + */ +class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface { private readonly EntityRepository $repository; @@ -73,8 +77,6 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler } /** - * @param AccompanyingCourseDocument $object - * * @return array[] */ public function getRelatedObjects(object $object): array @@ -121,4 +123,9 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler { return AccompanyingCourseDocument::class === $entityWorkflow->getRelatedEntityClass(); } + + public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject + { + return $this->getRelatedEntity($entityWorkflow)?->getObject(); + } } diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php index 9a1f52280..a45abe081 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Workflow; +use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Workflow\Exception\HandlerNotFoundException; use Symfony\Component\Workflow\Registry; @@ -37,4 +38,15 @@ class EntityWorkflowManager { return $this->registry->all($entityWorkflow); } + + public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject + { + foreach ($this->handlers as $handler) { + if ($handler instanceof EntityWorkflowWithStoredObjectHandlerInterface && $handler->supports($entityWorkflow)) { + return $handler->getAssociatedStoredObject($entityWorkflow); + } + } + + return null; + } } diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowWithStoredObjectHandlerInterface.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowWithStoredObjectHandlerInterface.php new file mode 100644 index 000000000..a1c7561cd --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowWithStoredObjectHandlerInterface.php @@ -0,0 +1,27 @@ + + */ +interface EntityWorkflowWithStoredObjectHandlerInterface extends EntityWorkflowHandlerInterface +{ + public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject; +} diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index 2912d1c01..97d48e5b0 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php @@ -11,15 +11,19 @@ declare(strict_types=1); namespace Chill\PersonBundle\Workflow; +use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; -use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; +use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter; use Symfony\Contracts\Translation\TranslatorInterface; -class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityWorkflowHandlerInterface +/** + * @implements EntityWorkflowWithStoredObjectHandlerInterface + */ +class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface { public function __construct(private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatorInterface $translator) {} @@ -67,8 +71,6 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW } /** - * @param AccompanyingPeriodWorkEvaluationDocument $object - * * @return array[] */ public function getRelatedObjects(object $object): array @@ -123,4 +125,9 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW { return AccompanyingPeriodWorkEvaluationDocument::class === $entityWorkflow->getRelatedEntityClass(); } + + public function getAssociatedStoredObject(EntityWorkflow $entityWorkflow): ?StoredObject + { + return $this->getRelatedEntity($entityWorkflow)?->getStoredObject(); + } } From c1cf27c42dbfd9fd76c4119b5035475e930c34d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 9 Jul 2024 23:51:00 +0200 Subject: [PATCH 082/375] Refactor workflow handlers and update comments Changes include class refactoring for Workflow handlers, using `readonly` and better indentation in constructors for better readability. In addition, outdated comments are removed. Also, entity workflow handlers now implement the EntityWorkflowHandlerInterface type for better type safety. --- .../Authorization/WorkflowEntityDeletionVoter.php | 5 +++-- .../Workflow/EntityWorkflowHandlerInterface.php | 6 ++++++ ...mpanyingPeriodWorkEvaluationWorkflowHandler.php | 14 +++++++++----- .../AccompanyingPeriodWorkWorkflowHandler.php | 14 +++++++++----- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/WorkflowEntityDeletionVoter.php b/src/Bundle/ChillMainBundle/Security/Authorization/WorkflowEntityDeletionVoter.php index 9718cf013..3444a57a6 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/WorkflowEntityDeletionVoter.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/WorkflowEntityDeletionVoter.php @@ -12,13 +12,14 @@ declare(strict_types=1); namespace Chill\MainBundle\Security\Authorization; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; +use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class WorkflowEntityDeletionVoter extends Voter { /** - * @param \Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface[] $handlers + * @param EntityWorkflowHandlerInterface[] $handlers */ public function __construct(private $handlers, private readonly EntityWorkflowRepository $entityWorkflowRepository) {} @@ -30,7 +31,7 @@ class WorkflowEntityDeletionVoter extends Voter foreach ($this->handlers as $handler) { if ($handler->isObjectSupported($subject) - && \in_array($attribute, $handler->getDeletionRoles($subject), true)) { + && \in_array($attribute, $handler->getDeletionRoles(), true)) { return true; } } diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php index e79982e1c..edd1120a7 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php @@ -14,6 +14,9 @@ namespace Chill\MainBundle\Workflow; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +/** + * @template T of object + */ interface EntityWorkflowHandlerInterface { /** @@ -25,6 +28,9 @@ interface EntityWorkflowHandlerInterface public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string; + /** + * @return T|null + */ public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object; public function getRelatedObjects(object $object): array; diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php index a041b3c37..113d5f5c8 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php @@ -20,9 +20,16 @@ use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvalu use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationVoter; use Symfony\Contracts\Translation\TranslatorInterface; -class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowHandlerInterface +/** + * @implements EntityWorkflowHandlerInterface + */ +readonly class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowHandlerInterface { - public function __construct(private readonly AccompanyingPeriodWorkEvaluationRepository $repository, private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatorInterface $translator) {} + public function __construct( + private AccompanyingPeriodWorkEvaluationRepository $repository, + private TranslatableStringHelperInterface $translatableStringHelper, + private TranslatorInterface $translator + ) {} public function getDeletionRoles(): array { @@ -53,9 +60,6 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH return $this->repository->find($entityWorkflow->getRelatedEntityId()); } - /** - * @param AccompanyingPeriodWorkEvaluation $object - */ public function getRelatedObjects(object $object): array { $relateds = []; diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php index ce146a887..837ee2aac 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php @@ -21,9 +21,16 @@ use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepos use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter; use Symfony\Contracts\Translation\TranslatorInterface; -class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInterface +/** + * @implements EntityWorkflowHandlerInterface + */ +readonly class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInterface { - public function __construct(private readonly AccompanyingPeriodWorkRepository $repository, private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly TranslatorInterface $translator) {} + public function __construct( + private AccompanyingPeriodWorkRepository $repository, + private TranslatableStringHelperInterface $translatableStringHelper, + private TranslatorInterface $translator + ) {} public function getDeletionRoles(): array { @@ -55,9 +62,6 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte return $this->repository->find($entityWorkflow->getRelatedEntityId()); } - /** - * @param AccompanyingPeriodWork $object - */ public function getRelatedObjects(object $object): array { $relateds = []; From a887602f4fbb654a283da9b6019d577ebc7083a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 10 Jul 2024 00:21:07 +0200 Subject: [PATCH 083/375] Handle storing of new object in PdfSignedMessageHandler The PdfSignedMessageHandler has been updated to handle pdf signed messages: the content is now stored within the object. Also, a PdfSignedMessageHandlerTest has been created to ensure the correct functionality of the updated handler. --- .../BaseSigner/PdfSignedMessageHandler.php | 20 +++++ .../PdfSignedMessageHandlerTest.php | 84 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandlerTest.php diff --git a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php index 4002b107b..81fb97dbf 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandler.php @@ -11,6 +11,9 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner; +use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository; +use Chill\MainBundle\Workflow\EntityWorkflowManager; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; @@ -23,10 +26,27 @@ final readonly class PdfSignedMessageHandler implements MessageHandlerInterface public function __construct( private LoggerInterface $logger, + private EntityWorkflowManager $entityWorkflowManager, + private StoredObjectManagerInterface $storedObjectManager, + private EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository, ) {} public function __invoke(PdfSignedMessage $message): void { $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); } } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandlerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandlerTest.php new file mode 100644 index 000000000..62d50c03f --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/Signature/Driver/BaseSigner/PdfSignedMessageHandlerTest.php @@ -0,0 +1,84 @@ +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) + ); + + // 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)); + } + + 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; + } +} From db94af0958b3989bfa3c4b930dc63e9bbd6468a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 10 Jul 2024 12:47:02 +0200 Subject: [PATCH 084/375] Add support for user signatures in workflow transitions This update introduces the ability to specify user signatures in workflow transitions. It allows a nullable user to be declared that may be requested to apply a signature. The code now handles the use-case of signing a transition by a user in addition to previous functionality of having it signed by a "Person" entity. Corresponding tests are also updated to validate this new feature. --- .../Entity/Workflow/EntityWorkflow.php | 17 +++++++--- .../Entity/Workflow/EntityWorkflowTest.php | 33 +++++++++++++++++++ .../Workflow/WorkflowTransitionContextDTO.php | 10 +++++- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index 4b5da09db..eb187cd39 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -413,8 +413,13 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface * * @return $this */ - public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO, string $transition, \DateTimeImmutable $transitionAt, ?User $byUser = null): self - { + public function setStep( + string $step, + WorkflowTransitionContextDTO $transitionContextDTO, + string $transition, + \DateTimeImmutable $transitionAt, + ?User $byUser = null + ): self { $previousStep = $this->getCurrentStep(); $previousStep @@ -437,8 +442,12 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface $newStep->addDestEmail($email); } - foreach ($transitionContextDTO->futurePersonSignatures as $personSignature) { - new EntityWorkflowStepSignature($newStep, $personSignature); + if (null !== $transitionContextDTO->futureUserSignature) { + new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature); + } else { + foreach ($transitionContextDTO->futurePersonSignatures as $personSignature) { + new EntityWorkflowStepSignature($newStep, $personSignature); + } } // copy the freeze diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php index 30003144f..4eb56f995 100644 --- a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php @@ -13,7 +13,9 @@ namespace Chill\MainBundle\Tests\Entity\Workflow; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; +use Chill\PersonBundle\Entity\Person; use PHPUnit\Framework\TestCase; /** @@ -105,4 +107,35 @@ final class EntityWorkflowTest extends TestCase self::assertEquals('2024-01-02', $previous->getTransitionAt()?->format('Y-m-d')); self::assertEquals('to_step_two', $previous->getTransitionAfter()); } + + public function testSetStepSignatureForUserIsCreated() + { + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureUserSignature = $user = new User(); + + $entityWorkflow->setStep('new', $dto, 'to_new', new \DateTimeImmutable()); + + $actual = $entityWorkflow->getCurrentStep(); + + self::assertCount(1, $actual->getSignatures()); + self::assertSame($user, $actual->getSignatures()->first()->getSigner()); + } + + public function testSetStepSignatureForPersonIsCreated() + { + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futurePersonSignatures[] = $person1 = new Person(); + $dto->futurePersonSignatures[] = $person2 = new Person(); + + $entityWorkflow->setStep('new', $dto, 'to_new', new \DateTimeImmutable()); + + $actual = $entityWorkflow->getCurrentStep(); + $persons = $actual->getSignatures()->map(fn (EntityWorkflowStepSignature $signature) => $signature->getSigner()); + + self::assertCount(2, $actual->getSignatures()); + self::assertContains($person1, $persons); + self::assertContains($person2, $persons); + } } diff --git a/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php index ba4cde51e..88bb0fa76 100644 --- a/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php +++ b/src/Bundle/ChillMainBundle/Workflow/WorkflowTransitionContextDTO.php @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Workflow; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\PersonBundle\Entity\Person; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Workflow\Transition; @@ -52,10 +53,17 @@ class WorkflowTransitionContextDTO public array $futureDestEmails = []; /** - * a list of future @see{Person} with will sign the next step. + * A list of future @see{Person} with will sign the next step. + * + * @var list */ public array $futurePersonSignatures = []; + /** + * An eventual user which is requested to apply a signature. + */ + public ?User $futureUserSignature = null; + public ?Transition $transition = null; public string $comment = ''; From 8d543be5cc47cebc423872c90e84c0e05c4b49bb Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 11 Jul 2024 15:15:04 +0200 Subject: [PATCH 085/375] Add configuration on id_document_types to avoid errors --- .../ChillMainExtension.php | 5 +++++ .../DependencyInjection/Configuration.php | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 4dde2076c..bf041b5b3 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -193,6 +193,11 @@ class ChillMainExtension extends Extension implements [] ); + $container->setParameter( + 'chill_main.id_document_kinds', + $config['id_document_kinds'] + ); + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader->load('services.yaml'); $loader->load('services/doctrine.yaml'); diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php index 8bd3e35f4..9c890d9fe 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php @@ -151,6 +151,28 @@ class Configuration implements ConfigurationInterface ->append($this->addWidgetsConfiguration('homepage', $this->containerBuilder)) ->end() // end of widgets/children ->end() // end of widgets + ->arrayNode('id_document_kinds')->defaultValue([]) + ->arrayPrototype() + ->children() + ->scalarNode('key')->isRequired()->cannotBeEmpty() + ->info('the key stored in database') + ->example('id_card') + ->end() + ->arrayNode('labels')->isRequired()->requiresAtLeastOneElement() + ->arrayPrototype() + ->children() + ->scalarNode('lang')->isRequired()->cannotBeEmpty() + ->example('fr') + ->end() + ->scalarNode('label')->isRequired()->cannotBeEmpty() + ->example('Carte de séjour') + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() // end of document types ->arrayNode('cruds') ->defaultValue([]) ->arrayPrototype() From 52a3d1be1b2317afd79c5821ba35b733201d52fc Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 11 Jul 2024 15:15:41 +0200 Subject: [PATCH 086/375] Implement show and hide logic within workflow form --- .../ChillMainBundle/Form/WorkflowStepType.php | 26 ++++++ .../public/page/workflow-show/index.js | 86 +++++++++++++++++-- .../views/Workflow/_decision.html.twig | 23 ++++- 3 files changed, 126 insertions(+), 9 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php index 284781d54..8949432b2 100644 --- a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php +++ b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php @@ -17,6 +17,7 @@ use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; +use Chill\PersonBundle\Form\Type\PickPersonDynamicType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\EmailType; @@ -102,6 +103,7 @@ class WorkflowStepType extends AbstractType 'choice_attr' => static function (Transition $transition) use ($workflow) { $toFinal = true; $isForward = 'neutral'; + $isSignature = []; $metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition); @@ -121,15 +123,38 @@ class WorkflowStepType extends AbstractType ) { $toFinal = false; } + + if (\array_key_exists('isSignature', $meta)) { + $isSignature = $meta['isSignature']; + } } return [ 'data-is-transition' => 'data-is-transition', 'data-to-final' => $toFinal ? '1' : '0', 'data-is-forward' => $isForward, + 'data-is-signature' => json_encode($isSignature), ]; }, ]) + ->add('isPersonOrUserSignature', ChoiceType::class, [ + 'mapped' => false, + 'multiple' => false, + 'expanded' => true, + 'label' => 'workflow.Type of signature', + 'choices' => [ + 'person' => 'person', + 'user' => 'user', + ], + ]) + ->add('futurePersonSignatures', PickPersonDynamicType::class, [ + 'label' => 'workflow.person signatures', + 'multiple' => true, + ]) + ->add('futureUserSignature', PickUserDynamicType::class, [ + 'label' => 'workflow.user signatures', + 'multiple' => false, + ]) ->add('futureDestUsers', PickUserDynamicType::class, [ 'label' => 'workflow.dest for next steps', 'multiple' => true, @@ -140,6 +165,7 @@ class WorkflowStepType extends AbstractType 'multiple' => true, 'required' => false, 'suggested' => $options['suggested_users'], + 'attr' => ['class' => 'future-cc-users'], ]) ->add('futureDestEmails', ChillCollectionType::class, [ 'label' => 'workflow.dest by email', diff --git a/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js b/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js index 2f91c7777..6829dad8e 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js +++ b/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js @@ -4,8 +4,86 @@ window.addEventListener('DOMContentLoaded', function() { let divTransitions = document.querySelector('#transitions'), futureDestUsersContainer = document.querySelector('#futureDests') + personSignatureField = document.querySelector('#person-signature-field'); + userSignatureField = document.querySelector('#user-signature-field'); + signatureTypeChoices = document.querySelector('#signature-type-choice'); ; + console.log('signature type', signatureTypeChoices); + + let + transitionFilterContainer = document.querySelector('#transitionFilter'), + transitionsContainer = document.querySelector('#transitions') + ; + + personSignatureField.style.display = 'none'; + userSignatureField.style.display = 'none'; + futureDestUsersContainer.style.display = 'none'; + signatureTypeChoices.style.display = 'none'; + + // ShowHide instance for signatureTypeChoices and futureDestUsersContainer + new ShowHide({ + load_event: null, + froms: [transitionsContainer], + container: [signatureTypeChoices, futureDestUsersContainer], + test: function() { + const selectedTransition = document.querySelector('input[name="workflow_step[transition]"]:checked'); + console.log('transition', selectedTransition) + if (!selectedTransition) { + return false; // No transition selected, hide all + } + + const isSignature = JSON.parse(selectedTransition.getAttribute('data-is-signature') || '[]'); + + if (isSignature.includes('person') && isSignature.includes('user')) { + signatureTypeChoices.style.display = ''; + } else { + signatureTypeChoices.style.display = 'none'; + } + + if (!isSignature.length) { + futureDestUsersContainer.style.display = ''; + personSignatureField.style.display = 'none'; + userSignatureField.style.display = 'none'; + } else { + futureDestUsersContainer.style.display = 'none'; + } + + return true; // Always return true to ensure ShowHide manages visibility + } + }); + + // Event listener for changes in signature type selection + signatureTypeChoices.addEventListener('change', function() { + // ShowHide instance for personSignatureField and userSignatureField + new ShowHide({ + load_event: null, + froms: [signatureTypeChoices], + container: [personSignatureField, userSignatureField], + test: function() { + console.log(signatureTypeChoices) + const selectedSignatureType = document.querySelector('input[name="workflow_step[isPersonOrUserSignature]"]:checked'); + console.log('signataure type', selectedSignatureType) + if (!selectedSignatureType) { + return false; // No signature type selected, hide both fields + } + + if (selectedSignatureType.value === 'person') { + personSignatureField.style.display = ''; + userSignatureField.style.display = 'none'; + } else if (selectedSignatureType.value === 'user') { + personSignatureField.style.display = 'none'; + userSignatureField.style.display = ''; + } else { + personSignatureField.style.display = 'none'; + userSignatureField.style.display = 'none'; + } + + return true; // Always return true to ensure ShowHide manages visibility + } + }); + }); + if (null !== divTransitions) { new ShowHide({ load_event: null, @@ -29,13 +107,8 @@ window.addEventListener('DOMContentLoaded', function() { }); } - let - transitionFilterContainer = document.querySelector('#transitionFilter'), - transitions = document.querySelector('#transitions') - ; - if (null !== transitionFilterContainer) { - transitions.querySelectorAll('.form-check').forEach(function(row) { + transitionsContainer.querySelectorAll('.form-check').forEach(function(row) { const isForward = row.querySelector('input').dataset.isForward; @@ -66,5 +139,4 @@ window.addEventListener('DOMContentLoaded', function() { }); }); } - }); diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig index 35f587e38..391e42324 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig @@ -58,15 +58,34 @@ {{ form_row(transition_form.transition) }} +
    +
    + {{ form_row(transition_form.isPersonOrUserSignature) }} + {{ form_errors(transition_form.isPersonOrUserSignature) }} +
    +
    + {{ form_row(transition_form.futureUserSignature) }} + {{ form_errors(transition_form.futureUserSignature) }} +
    +
    + {{ form_row(transition_form.futurePersonSignatures) }} + {{ form_errors(transition_form.futurePersonSignatures) }} +
    +
    +
    +
    {{ form_row(transition_form.futureDestUsers) }} {{ form_errors(transition_form.futureDestUsers) }} - +
    +
    {{ form_row(transition_form.futureCcUsers) }} {{ form_errors(transition_form.futureCcUsers) }} - +
    +
    {{ form_row(transition_form.futureDestEmails) }} {{ form_errors(transition_form.futureDestEmails) }} +

    {{ form_label(transition_form.comment) }}

    From 215eba41b716e59849c52abfe73edb463574abba Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 11 Jul 2024 15:52:47 +0200 Subject: [PATCH 087/375] Fix unit test to accomodate changed constructor in StoredObjectNormalizer --- .../ChillDocStoreBundle/Tests/Form/StoredObjectTypeTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Form/StoredObjectTypeTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Form/StoredObjectTypeTest.php index 2fc17787a..9d486ac93 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Form/StoredObjectTypeTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Form/StoredObjectTypeTest.php @@ -23,6 +23,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Form\PreloadedExtension; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Serializer; @@ -80,11 +81,15 @@ class StoredObjectTypeTest extends TypeTestCase $urlGenerator->generate('chill_docstore_dav_document_get', Argument::type('array'), UrlGeneratorInterface::ABSOLUTE_URL) ->willReturn('http://url/fake'); + $security = $this->prophesize(Security::class); + $security->isGranted(Argument::cetera())->willReturn(true); + $serializer = new Serializer( [ new StoredObjectNormalizer( $jwtTokenProvider->reveal(), $urlGenerator->reveal(), + $security->reveal() ), ], [ From e83307ca6d13dda2851d86c8e75d349bb5db1982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 17:17:56 +0200 Subject: [PATCH 088/375] Remove obsolete security checks in StoredObjectVoter This commit eliminates antiquated security checks in the StoredObjectVoter class. Specifically, it removes a chunk of commented out code that checked for certain attributes on the token and also the import for DavTokenAuthenticationEventSubscriber class which is no longer needed. This results in code cleanup and prevents future confusion. --- .../Security/Authorization/StoredObjectVoter.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index c0851144a..91e767af2 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Security; @@ -35,19 +34,6 @@ class StoredObjectVoter extends Voter protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool { /** @var StoredObject $subject */ - /* - if ( - !$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT) - || $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT) - ) { - return false; - } - - if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) { - return false; - } - */ - $attributeAsEnum = StoredObjectRoleEnum::from($attribute); // Loop through context-specific voters From 7d0f9175be562871b5d692ba8a6728e31fc819ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 17:18:28 +0200 Subject: [PATCH 089/375] Refactor StoredObjectVoterTest to improve testing logic The existing StoredObjectVoter test logic was reworked to utilize UsernamePasswordToken and Security mock objects instead of defining its own token. This change improves the testing for different scenarios such as unsupported attributes and cases where role voters cannot see the stored object. Also, the redundancy in the test case provider was removed, which leads to cleaner and more maintainable code. --- .../Authorization/StoredObjectVoterTest.php | 132 +++++++++--------- 1 file changed, 65 insertions(+), 67 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php index 92c928681..a113cfb7b 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php @@ -14,11 +14,13 @@ namespace Chill\DocStoreBundle\Tests\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter; -use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; +use Chill\MainBundle\Entity\User; use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; 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\Security; /** * @internal @@ -27,97 +29,93 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; */ class StoredObjectVoterTest extends TestCase { - use ProphecyTrait; - /** * @dataProvider provideDataVote */ - public function testVote(TokenInterface $token, ?object $subject, string $attribute, mixed $expected): void + public function testVote(array $storedObjectVotersDefinition, object $subject, string $attribute, bool $fallbackSecurityExpected, bool $securityIsGrantedResult, mixed $expected): void { - $voter = new StoredObjectVoter(); + $storedObjectVoters = array_map(fn (array $definition) => $this->buildStoredObjectVoter($definition[0], $definition[1], $definition[2]), $storedObjectVotersDefinition); + $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); self::assertEquals($expected, $voter->vote($token, $subject, [$attribute])); } - public function provideDataVote(): iterable + private function buildStoredObjectVoter(bool $supportsIsCalled, bool $supports, bool $voteOnAttribute): StoredObjectVoterInterface + { + $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 [ - $this->buildToken(StoredObjectRoleEnum::EDIT, new StoredObject()), + // we try with something else than a SToredObject, the voter should abstain + [[false, false, false]], new \stdClass(), 'SOMETHING', + false, + false, VoterInterface::ACCESS_ABSTAIN, ]; - yield [ - $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()), - $so, + // we try with an unsupported attribute, the voter must abstain + [[false, false, false]], + new StoredObject(), 'SOMETHING', + false, + false, VoterInterface::ACCESS_ABSTAIN, ]; - yield [ - $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()), - $so, - StoredObjectRoleEnum::SEE->value, - VoterInterface::ACCESS_GRANTED, - ]; - - yield [ - $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()), - $so, - StoredObjectRoleEnum::EDIT->value, - 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), + // happy scenario: there is a role voter + [[true, true, true]], new StoredObject(), StoredObjectRoleEnum::SEE->value, - VoterInterface::ACCESS_DENIED, + false, + false, + VoterInterface::ACCESS_GRANTED, ]; - yield [ - $this->buildToken(null, null), + // there is a role voter, but not allowed to see the stored object + [[true, true, false]], new StoredObject(), StoredObjectRoleEnum::SEE->value, + false, + false, 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(); + 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, + ]; } } From 31f842471aa68d680d61a397b43f6ea612897b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 17:53:06 +0200 Subject: [PATCH 090/375] Refactor authorization for AccompanyingPeriodWorkEvaluationDocuments The AccompanyingPeriodWorkEvaluationStoredObjectVoter has been updated to use the AccompanyingPeriodWorkEvaluationDocument-related classes instead of the AccompanyingPeriodWork classes. Additionally, a new voters class, AccompanyingPeriodWorkEvaluationDocumentVoter has been created. Changes are also made in the repository to find the associated entity in the AccompanyingPeriodWorkEvaluationDocument repository instead of the AccompanyingPeriodWork repository. --- ...rkEvaluationDocumentStoredObjectVoter.php} | 16 +++++++------- ...PeriodWorkEvaluationDocumentRepository.php | 22 +++++++++++++++++-- .../AccompanyingPeriodWorkRepository.php | 17 +------------- ...nyingPeriodWorkEvaluationDocumentVoter.php | 8 ++++++- .../AccompanyingPeriodWorkEvaluationVoter.php | 4 ++++ 5 files changed, 40 insertions(+), 27 deletions(-) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/{AccompanyingPeriodWorkEvaluationStoredObjectVoter.php => AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php} (74%) diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php similarity index 74% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php rename to src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php index 7ba7e276e..d9eb4a843 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php @@ -14,15 +14,15 @@ namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Service\WorkflowDocumentService; -use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; -use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; -use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter; +use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; +use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter; use Symfony\Component\Security\Core\Security; -class AccompanyingPeriodWorkEvaluationStoredObjectVoter extends AbstractStoredObjectVoter +class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends AbstractStoredObjectVoter { public function __construct( - private readonly AccompanyingPeriodWorkRepository $repository, + private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, Security $security, WorkflowDocumentService $workflowDocumentService ) { @@ -36,14 +36,14 @@ class AccompanyingPeriodWorkEvaluationStoredObjectVoter extends AbstractStoredOb protected function getClass(): string { - return AccompanyingPeriodWork::class; + return AccompanyingPeriodWorkEvaluationDocument::class; } protected function attributeToRole(StoredObjectRoleEnum $attribute): string { return match ($attribute) { - StoredObjectRoleEnum::SEE => AccompanyingPeriodWorkVoter::SEE, - StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkVoter::UPDATE, + StoredObjectRoleEnum::SEE => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE, + StoredObjectRoleEnum::EDIT => AccompanyingPeriodWorkEvaluationDocumentVoter::SEE_AND_EDIT, }; } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 59bb3f915..5da541cf8 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -11,14 +11,18 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\AccompanyingPeriod; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; +use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\NonUniqueResultException; use Doctrine\Persistence\ObjectRepository; -class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository +readonly class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface { - private readonly EntityRepository $repository; + private EntityRepository $repository; public function __construct(EntityManagerInterface $em) { @@ -58,4 +62,18 @@ class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectReposi { return AccompanyingPeriodWorkEvaluationDocument::class; } + + /** + * @throws NonUniqueResultException + */ + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?AccompanyingPeriodWorkEvaluationDocument + { + $qb = $this->repository->createQueryBuilder('acpwed'); + $query = $qb + ->where('acpwed.storedObject = :storedObject') + ->setParameter('storedObject', $storedObject) + ->getQuery(); + + return $query->getOneOrNullResult(); + } } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index 324c3c176..95b995e74 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -11,8 +11,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\AccompanyingPeriod; -use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; @@ -24,7 +22,7 @@ use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; -final readonly class AccompanyingPeriodWorkRepository implements ObjectRepository, AssociatedEntityToStoredObjectInterface +final readonly class AccompanyingPeriodWorkRepository implements ObjectRepository { private EntityRepository $repository; @@ -253,17 +251,4 @@ final readonly class AccompanyingPeriodWorkRepository implements ObjectRepositor return $qb; } - - public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?AccompanyingPeriodWork - { - $qb = $this->repository->createQueryBuilder('acpw'); - $query = $qb - ->join('acpw.accompanyingPeriodWorkEvaluations', 'acpwe') - ->join('acpwe.documents', 'acpwed') - ->where('acpwed.storedObject = :storedObject') - ->setParameter('storedObject', $storedObject) - ->getQuery(); - - return $query->getOneOrNullResult(); - } } diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php index 97ca84a13..0e9f4201c 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationDocumentVoter.php @@ -24,13 +24,14 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter { final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW'; + final public const SEE_AND_EDIT = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_EDIT'; public function __construct(private readonly AccessDecisionManagerInterface $accessDecisionManager) {} public function supports($attribute, $subject): bool { return $subject instanceof AccompanyingPeriodWorkEvaluationDocument - && self::SEE === $attribute; + && (self::SEE === $attribute || self::SEE_AND_EDIT === $attribute); } /** @@ -47,6 +48,11 @@ class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter [AccompanyingPeriodWorkEvaluationVoter::SEE], $subject->getAccompanyingPeriodWorkEvaluation() ), + self::SEE_AND_EDIT => $this->accessDecisionManager->decide( + $token, + [AccompanyingPeriodWorkEvaluationVoter::SEE_AND_EDIT], + $subject->getAccompanyingPeriodWorkEvaluation() + ), default => throw new \UnexpectedValueException("The attribute {$attribute} is not supported"), }; } diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationVoter.php index ce5faca8d..bea63018c 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationVoter.php @@ -21,11 +21,14 @@ class AccompanyingPeriodWorkEvaluationVoter extends Voter implements ChillVoterI { final public const ALL = [ self::SEE, + self::SEE_AND_EDIT, self::STATS, ]; final public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_SHOW'; + final public const SEE_AND_EDIT = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_EDIT'; + final public const STATS = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_STATS'; public function __construct(private readonly Security $security) {} @@ -45,6 +48,7 @@ class AccompanyingPeriodWorkEvaluationVoter extends Voter implements ChillVoterI return match ($attribute) { self::STATS => $this->security->isGranted(AccompanyingPeriodVoter::STATS, $subject), self::SEE => $this->security->isGranted(AccompanyingPeriodWorkVoter::SEE, $subject->getAccompanyingPeriodWork()), + self::SEE_AND_EDIT => $this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $subject->getAccompanyingPeriodWork()), default => throw new \UnexpectedValueException("attribute {$attribute} is not supported"), }; } From 9e92ede16f61211bc9a2d2a054ed6a212a8a9c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 18:58:08 +0200 Subject: [PATCH 091/375] Handle cases when there are multiple EntityWorkflows associated with one entity --- .../Service/WorkflowDocumentService.php | 8 ++------ .../AccompanyingCourseDocumentWorkflowHandler.php | 4 ++-- .../Workflow/EntityWorkflowRepository.php | 5 ++++- .../Workflow/EntityWorkflowHandlerInterface.php | 5 ++++- .../Workflow/EntityWorkflowManager.php | 11 ++++++++--- ...nyingPeriodWorkEvaluationDocumentRepository.php | 1 - ...PeriodWorkEvaluationDocumentWorkflowHandler.php | 4 ++-- ...mpanyingPeriodWorkEvaluationWorkflowHandler.php | 14 +++++++------- .../AccompanyingPeriodWorkWorkflowHandler.php | 4 ++-- 9 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php index 78c7b7b7d..3444653b1 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Service; -use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\Security\Core\Security; @@ -21,13 +20,10 @@ class WorkflowDocumentService public function notBlockedByWorkflow(object $entity): bool { - /** - * @var EntityWorkflow - */ - $workflow = $this->entityWorkflowManager->findByRelatedEntity($entity); + $workflows = $this->entityWorkflowManager->findByRelatedEntity($entity); $currentUser = $this->security->getUser(); - if (null != $workflow) { + foreach ($workflows as $workflow) { if ($workflow->isFinal()) { return false; } diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index 74e51b19e..59d2718c8 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -121,10 +121,10 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl return false; } - public function findByRelatedEntity(object $object): ?EntityWorkflow + public function findByRelatedEntity(object $object): array { if (!$object instanceof AccompanyingCourseDocument) { - return null; + return []; } return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId()); diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php index dba68ee41..81cdcd551 100644 --- a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -99,7 +99,10 @@ class EntityWorkflowRepository implements ObjectRepository return $this->repository->findAll(); } - public function findByRelatedEntity($entityClass, $relatedEntityId): ?EntityWorkflow + /** + * @return list + */ + public function findByRelatedEntity($entityClass, $relatedEntityId): array { $qb = $this->repository->createQueryBuilder('w'); diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php index bff1057c8..ebe412587 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php @@ -52,5 +52,8 @@ interface EntityWorkflowHandlerInterface public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool; - public function findByRelatedEntity(object $object): ?EntityWorkflow; + /** + * @return list + */ + public function findByRelatedEntity(object $object): array; } diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php index 26c31506f..a5b9b6a23 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php @@ -38,12 +38,17 @@ class EntityWorkflowManager return $this->registry->all($entityWorkflow); } - public function findByRelatedEntity(object $object): ?EntityWorkflow + /** + * @return list + */ + public function findByRelatedEntity(object $object): array { foreach ($this->handlers as $handler) { - return $handler->findByRelatedEntity($object); + if ([] !== $workflows = $handler->findByRelatedEntity($object)) { + return $workflows; + } } - return null; + return []; } } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php index 5da541cf8..e3a484f0b 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocumentRepository.php @@ -13,7 +13,6 @@ namespace Chill\PersonBundle\Repository\AccompanyingPeriod; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; -use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index 34f8db4a7..cc59a509b 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php @@ -135,10 +135,10 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW return false; } - public function findByRelatedEntity(object $object): ?EntityWorkflow + public function findByRelatedEntity(object $object): array { if (!$object instanceof AccompanyingPeriodWorkEvaluationDocument) { - return null; + return []; } return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWorkEvaluationDocument::class, $object->getId()); diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php index e007f03f7..a9897f9bd 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php @@ -21,13 +21,13 @@ use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvalu use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationVoter; use Symfony\Contracts\Translation\TranslatorInterface; -class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowHandlerInterface +readonly class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowHandlerInterface { public function __construct( - private readonly AccompanyingPeriodWorkEvaluationRepository $repository, - private readonly EntityWorkflowRepository $workflowRepository, - private readonly TranslatableStringHelperInterface $translatableStringHelper, - private readonly TranslatorInterface $translator + private AccompanyingPeriodWorkEvaluationRepository $repository, + private EntityWorkflowRepository $workflowRepository, + private TranslatableStringHelperInterface $translatableStringHelper, + private TranslatorInterface $translator ) {} public function getDeletionRoles(): array @@ -121,10 +121,10 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH return false; } - public function findByRelatedEntity(object $object): ?EntityWorkflow + public function findByRelatedEntity(object $object): array { if (!$object instanceof AccompanyingPeriodWorkEvaluation) { - return null; + return []; } return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWorkEvaluation::class, $object->getId()); diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php index d40080e33..cb9462b9e 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php @@ -128,10 +128,10 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte return false; } - public function findByRelatedEntity(object $object): ?EntityWorkflow + public function findByRelatedEntity(object $object): array { if (!$object instanceof AccompanyingPeriodWork) { - return null; + return []; } return $this->workflowRepository->findByRelatedEntity(AccompanyingPeriodWork::class, $object->getId()); From 747a1de32185be597784962d7637151d65c16ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 18:58:44 +0200 Subject: [PATCH 092/375] Add locale requirement to search route, to avoid conflict with profiler route Introduces a locale requirement to the search route in the ChillMainBundle. This update specifies that a valid locale should consist of 1 to 3 lowercase alphabetic characters. This change will help constrain acceptable locale values. --- src/Bundle/ChillMainBundle/Controller/SearchController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Controller/SearchController.php b/src/Bundle/ChillMainBundle/Controller/SearchController.php index 4c4b6d22c..09e558b0a 100644 --- a/src/Bundle/ChillMainBundle/Controller/SearchController.php +++ b/src/Bundle/ChillMainBundle/Controller/SearchController.php @@ -96,7 +96,7 @@ class SearchController extends AbstractController return $this->render('@ChillMain/Search/choose_list.html.twig'); } - #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json'], defaults: ['_format' => 'html'])] + #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json', '_locale' => '[a-z]{1,3}'], defaults: ['_format' => 'html'])] public function searchAction(Request $request, mixed $_format) { $pattern = trim((string) $request->query->get('q', '')); From ca68b5824637404d5cf9ee3e822d65280ff9b4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 19:09:29 +0200 Subject: [PATCH 093/375] Move classes to dedicated bundle, and avoid plural for namespace name --- .../Security/Authorization/ActivityStoredObjectVoter.php | 2 +- .../AbstractStoredObjectVoter.php | 2 +- .../AccompanyingCourseDocumentStoredObjectVoter.php | 2 +- .../PersonDocumentStoredObjectVoter.php | 2 +- .../Security/Authorization/AbstractStoredObjectVoterTest.php | 3 +-- .../Security/Authorization/EventStoredObjectVoter.php | 2 +- ...companyingPeriodWorkEvaluationDocumentStoredObjectVoter.php | 3 ++- 7 files changed, 8 insertions(+), 8 deletions(-) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/{StoredObjectVoters => StoredObjectVoter}/AbstractStoredObjectVoter.php (99%) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/{StoredObjectVoters => StoredObjectVoter}/AccompanyingCourseDocumentStoredObjectVoter.php (99%) rename src/Bundle/ChillDocStoreBundle/Security/Authorization/{StoredObjectVoters => StoredObjectVoter}/PersonDocumentStoredObjectVoter.php (99%) rename src/Bundle/{ChillDocStoreBundle/Security/Authorization/StoredObjectVoters => ChillPersonBundle/Security/Authorization/StoredObjectVoter}/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php (91%) diff --git a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index be3013a08..f57df2a30 100644 --- a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -15,7 +15,7 @@ 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\StoredObjectVoters\AbstractStoredObjectVoter; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Symfony\Component\Security\Core\Security; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php similarity index 99% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php rename to src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php index 24def7ca9..293473b7e 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters; +namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php similarity index 99% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php rename to src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php index 2665553e8..75035474d 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters; +namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php similarity index 99% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php rename to src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php index 5791d724b..e0d6108af 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/PersonDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters; +namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Entity\PersonDocument; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index 24370c2db..a545a6a2e 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -11,11 +11,10 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Tests\Security\Authorization; -use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; -use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\MainBundle\Entity\User; use PHPUnit\Framework\TestCase; diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php index 8dda94aad..eec707b8e 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php +++ b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php @@ -13,7 +13,7 @@ namespace Chill\EventBundle\Security\Authorization; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; -use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters\AbstractStoredObjectVoter; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Repository\EventRepository; diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php similarity index 91% rename from src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php rename to src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php index d9eb4a843..979edd4f6 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoters/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php @@ -9,10 +9,11 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\DocStoreBundle\Security\Authorization\StoredObjectVoters; +namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; +use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Service\WorkflowDocumentService; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; From d5e499198240a083f0fb81801d363fe31b1191e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 19:14:01 +0200 Subject: [PATCH 094/375] Rename WorkflowDocumentService to WorkflowStoredObjectPermissionHelper The previous name, WorkflowDocumentService, was misleading as its functionality extends to all stored objects and not limited to documents. Therefore, it was renamed to WorkflowStoredObjectPermissionHelper. Consequently, all references to this service were updated throughout the codebase. --- .../Authorization/ActivityStoredObjectVoter.php | 4 ++-- .../StoredObjectVoter/AbstractStoredObjectVoter.php | 4 ++-- .../AccompanyingCourseDocumentStoredObjectVoter.php | 4 ++-- .../PersonDocumentStoredObjectVoter.php | 4 ++-- ...ce.php => WorkflowStoredObjectPermissionHelper.php} | 2 +- .../Authorization/AbstractStoredObjectVoterTest.php | 10 +++++----- .../Security/Authorization/EventStoredObjectVoter.php | 4 ++-- ...ngPeriodWorkEvaluationDocumentStoredObjectVoter.php | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) rename src/Bundle/ChillDocStoreBundle/Service/{WorkflowDocumentService.php => WorkflowStoredObjectPermissionHelper.php} (95%) diff --git a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index f57df2a30..4be101c45 100644 --- a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -16,7 +16,7 @@ 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\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Symfony\Component\Security\Core\Security; class ActivityStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,7 +24,7 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly ActivityRepository $repository, Security $security, - WorkflowDocumentService $workflowDocumentService + WorkflowStoredObjectPermissionHelper $workflowDocumentService ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php index 293473b7e..2e76da318 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php @@ -15,7 +15,7 @@ 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\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; @@ -34,7 +34,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function __construct( private readonly Security $security, - private readonly ?WorkflowDocumentService $workflowDocumentService = null, + private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null, ) {} public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php index 75035474d..0f7015b10 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php @@ -16,7 +16,7 @@ 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\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Symfony\Component\Security\Core\Security; final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,7 +24,7 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb public function __construct( private readonly AccompanyingCourseDocumentRepository $repository, Security $security, - WorkflowDocumentService $workflowDocumentService + WorkflowStoredObjectPermissionHelper $workflowDocumentService ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php index e0d6108af..577f362ef 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php @@ -16,7 +16,7 @@ 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\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Symfony\Component\Security\Core\Security; class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,7 +24,7 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly PersonDocumentRepository $repository, Security $security, - WorkflowDocumentService $workflowDocumentService + WorkflowStoredObjectPermissionHelper $workflowDocumentService ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php similarity index 95% rename from src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php rename to src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php index 3444653b1..519eee8de 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowDocumentService.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php @@ -14,7 +14,7 @@ namespace Chill\DocStoreBundle\Service; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\Security\Core\Security; -class WorkflowDocumentService +class WorkflowStoredObjectPermissionHelper { public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index a545a6a2e..4f420223a 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -15,7 +15,7 @@ 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\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Chill\MainBundle\Entity\User; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -30,16 +30,16 @@ class AbstractStoredObjectVoterTest extends TestCase { private AssociatedEntityToStoredObjectInterface $repository; private Security $security; - private WorkflowDocumentService $workflowDocumentService; + private WorkflowStoredObjectPermissionHelper $workflowDocumentService; protected function setUp(): void { $this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class); $this->security = $this->createMock(Security::class); - $this->workflowDocumentService = $this->createMock(WorkflowDocumentService::class); + $this->workflowDocumentService = $this->createMock(WorkflowStoredObjectPermissionHelper::class); } - private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowDocumentService $workflowDocumentService = null): AbstractStoredObjectVoter + 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 { @@ -47,7 +47,7 @@ class AbstractStoredObjectVoterTest extends TestCase private bool $canBeAssociatedWithWorkflow, private AssociatedEntityToStoredObjectInterface $repository, Security $security, - ?WorkflowDocumentService $workflowDocumentService = null + ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php index eec707b8e..66f54bc6d 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php +++ b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php @@ -14,7 +14,7 @@ namespace Chill\EventBundle\Security\Authorization; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\DocStoreBundle\Service\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Repository\EventRepository; use Symfony\Component\Security\Core\Security; @@ -24,7 +24,7 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly EventRepository $repository, Security $security, - WorkflowDocumentService $workflowDocumentService + WorkflowStoredObjectPermissionHelper $workflowDocumentService ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php index 979edd4f6..4a81367d7 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php @@ -14,7 +14,7 @@ namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\DocStoreBundle\Service\WorkflowDocumentService; +use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter; @@ -25,7 +25,7 @@ class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends Abstract public function __construct( private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, Security $security, - WorkflowDocumentService $workflowDocumentService + WorkflowStoredObjectPermissionHelper $workflowDocumentService ) { parent::__construct($security, $workflowDocumentService); } From d689ce9aef392ad4c6f973333096ddbd4d4651d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 19:14:17 +0200 Subject: [PATCH 095/375] Fix condition for checking if the user is allowed to edit a document attached to a workflow --- .../Service/WorkflowStoredObjectPermissionHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php index 519eee8de..b27b6d96a 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php @@ -28,8 +28,8 @@ class WorkflowStoredObjectPermissionHelper return false; } - if ($workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { - return true; + if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { + return false; } } From 9f88eef2493c0cac48ffe29524c14db36aae83ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 15 Jul 2024 21:25:22 +0200 Subject: [PATCH 096/375] Fix permission logic in StoredObjectNormalizer The logic for checking 'see' and 'edit' permissions within the StoredObjectNormalizer has been updated. It now correctly refers to the value of the StoredObjectRoleEnum to check access rights. --- .../Serializer/Normalizer/StoredObjectNormalizer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php index 17597901d..ebd7d3564 100644 --- a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php +++ b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php @@ -57,8 +57,8 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa // deprecated property $datas['creationDate'] = $datas['createdAt']; - $canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE, $object); - $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT, $object); + $canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object); + $canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object); if ($canSee || $canEdit) { $accessToken = $this->JWTDavTokenProvider->createToken( From 873940786fc4cb727fc202cebd553a5cbb2fcc63 Mon Sep 17 00:00:00 2001 From: LenaertsJ Date: Thu, 18 Jul 2024 13:51:08 +0000 Subject: [PATCH 097/375] Signature zone within workflow --- .../unreleased/Feature-20240718-151233.yaml | 5 ++ .../Controller/WorkflowController.php | 66 +++++++++++++------ .../ChillMainExtension.php | 5 ++ .../DependencyInjection/Configuration.php | 22 +++++++ .../Form/WorkflowSignatureMetadataType.php | 62 +++++++++++++++++ .../views/Workflow/_signature.html.twig | 19 ++++++ .../Workflow/_signature_metadata.html.twig | 24 +++++++ .../Resources/views/Workflow/index.html.twig | 3 + .../Workflow/person_signature_form.html.twig | 0 .../ChillMainBundle/config/services/form.yaml | 2 + .../translations/messages.fr.yml | 9 +++ 11 files changed, 198 insertions(+), 19 deletions(-) create mode 100644 .changes/unreleased/Feature-20240718-151233.yaml create mode 100644 src/Bundle/ChillMainBundle/Form/WorkflowSignatureMetadataType.php create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature_metadata.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/person_signature_form.html.twig diff --git a/.changes/unreleased/Feature-20240718-151233.yaml b/.changes/unreleased/Feature-20240718-151233.yaml new file mode 100644 index 000000000..6e2666be8 --- /dev/null +++ b/.changes/unreleased/Feature-20240718-151233.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: Metadata form added for person signatures +time: 2024-07-18T15:12:33.8134266+02:00 +custom: + Issue: "288" diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php index 6898d87e4..cdd432cf7 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php @@ -13,9 +13,9 @@ namespace Chill\MainBundle\Controller; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; -use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; -use Chill\MainBundle\Form\EntityWorkflowCommentType; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; +use Chill\MainBundle\Form\WorkflowSignatureMetadataType; use Chill\MainBundle\Form\WorkflowStepType; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; @@ -289,6 +289,7 @@ class WorkflowController extends AbstractController $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $errors = []; + $signatures = $entityWorkflow->getCurrentStep()->getSignatures(); if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) { // possible transition @@ -341,22 +342,6 @@ class WorkflowController extends AbstractController } } - /* - $commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment()); - $commentForm->handleRequest($request); - - if ($commentForm->isSubmitted() && $commentForm->isValid()) { - $this->entityManager->persist($newComment); - $this->entityManager->flush(); - - $this->addFlash('success', $this->translator->trans('workflow.Comment added')); - - return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]); - } elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) { - $this->addFlash('error', $this->translator->trans('This form contains errors')); - } - */ - return $this->render( '@ChillMain/Workflow/index.html.twig', [ @@ -366,7 +351,7 @@ class WorkflowController extends AbstractController 'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null, 'entity_workflow' => $entityWorkflow, 'transition_form_errors' => $errors, - // 'comment_form' => $commentForm->createView(), + 'signatures' => $signatures, ] ); } @@ -385,4 +370,47 @@ class WorkflowController extends AbstractController return $lines; } + + #[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/metadata', name: 'chill_main_workflow_signature_metadata')] + public function addSignatureMetadata(int $signature_id, Request $request): Response + { + $signature = $this->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id); + + if ($signature->getSigner() instanceof User) { + return $this->redirectToRoute('signature_route_user'); + } + + $metadataForm = $this->createForm(WorkflowSignatureMetadataType::class); + $metadataForm->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]); + + $metadataForm->handleRequest($request); + + if ($metadataForm->isSubmitted() && $metadataForm->isValid()) { + $data = $metadataForm->getData(); + + $signature->setSignatureMetadata( + [ + 'base_signer' => [ + 'document_type' => $data['documentType'], + 'document_number' => $data['documentNumber'], + 'expiration_date' => $data['expirationDate'], + ], + ] + ); + + $this->entityManager->persist($signature); + $this->entityManager->flush(); + + // Todo should redirect to document for actual signing? To be adjusted still + return $this->redirectToRoute('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]); + } + + return $this->render( + '@ChillMain/Workflow/_signature_metadata.html.twig', + [ + 'metadata_form' => $metadataForm->createView(), + 'person' => $signature->getSigner(), + ] + ); + } } diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 4dde2076c..bf041b5b3 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -193,6 +193,11 @@ class ChillMainExtension extends Extension implements [] ); + $container->setParameter( + 'chill_main.id_document_kinds', + $config['id_document_kinds'] + ); + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader->load('services.yaml'); $loader->load('services/doctrine.yaml'); diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php index 8bd3e35f4..9c890d9fe 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php @@ -151,6 +151,28 @@ class Configuration implements ConfigurationInterface ->append($this->addWidgetsConfiguration('homepage', $this->containerBuilder)) ->end() // end of widgets/children ->end() // end of widgets + ->arrayNode('id_document_kinds')->defaultValue([]) + ->arrayPrototype() + ->children() + ->scalarNode('key')->isRequired()->cannotBeEmpty() + ->info('the key stored in database') + ->example('id_card') + ->end() + ->arrayNode('labels')->isRequired()->requiresAtLeastOneElement() + ->arrayPrototype() + ->children() + ->scalarNode('lang')->isRequired()->cannotBeEmpty() + ->example('fr') + ->end() + ->scalarNode('label')->isRequired()->cannotBeEmpty() + ->example('Carte de séjour') + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() // end of document types ->arrayNode('cruds') ->defaultValue([]) ->arrayPrototype() diff --git a/src/Bundle/ChillMainBundle/Form/WorkflowSignatureMetadataType.php b/src/Bundle/ChillMainBundle/Form/WorkflowSignatureMetadataType.php new file mode 100644 index 000000000..47be9b88c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/WorkflowSignatureMetadataType.php @@ -0,0 +1,62 @@ +parameterBag->get('chill_main.id_document_kinds'); + + $choices = []; + + foreach ($documentTypeChoices as $documentType) { + $labels = []; + + foreach ($documentType['labels'] as $label) { + $labels[$label['lang']] = $label['label']; + } + + $localizedLabel = $this->translatableStringHelper->localize($labels); + if (null !== $localizedLabel) { + $choices[$localizedLabel] = $documentType['key']; + } + } + + $builder + ->add('documentType', ChoiceType::class, [ + 'label' => 'workflow.signature_zone.metadata.docType', + 'expanded' => false, + 'required' => true, + 'choices' => $choices, + ]) + ->add('documentNumber', TextType::class, [ + 'required' => true, + 'label' => 'workflow.signature_zone.metadata.docNumber', + ]) + ->add('expirationDate', ChillDateType::class, [ + 'required' => true, + 'input' => 'datetime_immutable', + 'label' => 'workflow.signature_zone.metadata.docExpiration', + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig new file mode 100644 index 000000000..a61808500 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig @@ -0,0 +1,19 @@ +

    {{ 'workflow.signature_zone.title'|trans }}

    + +
    +
    + {% for s in signatures %} +
    +
    {{ s.signer|chill_entity_render_box }}
    +
    + {{ 'workflow.signature_zone.button_sign'|trans }} + {% if s.state is same as('signed') %} +

    {{ s.stateDate }}

    + {% endif %} +
    +
    + {% endfor %} +
    +
    + + diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature_metadata.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature_metadata.html.twig new file mode 100644 index 000000000..5b828832d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature_metadata.html.twig @@ -0,0 +1,24 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block title %} + {{ 'Signature'|trans }} +{% endblock %} + +{% block content %} +
    +

    {{ 'workflow.signature_zone.metadata.sign_by'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname}) }}

    + + {% if metadata_form is not null %} + {{ form_start(metadata_form) }} + {{ form_row(metadata_form.documentType) }} + {{ form_row(metadata_form.documentNumber) }} + {{ form_row(metadata_form.expirationDate) }} +
      +
    • + {{ form_widget(metadata_form.submit, { 'attr' : { 'class' : 'btn btn-submit' }} ) }} +
    • +
    + {{ form_end(metadata_form) }} + {% endif %} +
    +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig index 782eae136..da4d073b2 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig @@ -57,6 +57,9 @@
    {% include '@ChillMain/Workflow/_follow.html.twig' %}
    + {% if signatures|length > 0 %} +
    {% include '@ChillMain/Workflow/_signature.html.twig' %}
    + {% endif %}
    {% include '@ChillMain/Workflow/_decision.html.twig' %}
    {#
    {% include '@ChillMain/Workflow/_comment.html.twig' %}
    #}
    {% include '@ChillMain/Workflow/_history.html.twig' %}
    diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/person_signature_form.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/person_signature_form.html.twig new file mode 100644 index 000000000..e69de29bb diff --git a/src/Bundle/ChillMainBundle/config/services/form.yaml b/src/Bundle/ChillMainBundle/config/services/form.yaml index ce303701c..f6b50cb57 100644 --- a/src/Bundle/ChillMainBundle/config/services/form.yaml +++ b/src/Bundle/ChillMainBundle/config/services/form.yaml @@ -133,6 +133,8 @@ services: Chill\MainBundle\Form\WorkflowStepType: ~ + Chill\MainBundle\Form\WorkflowSignatureMetadataType: ~ + Chill\MainBundle\Form\DataMapper\PrivateCommentDataMapper: autowire: true autoconfigure: true diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 421cac473..924ccec3a 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -528,6 +528,15 @@ workflow: This link grant any user to apply a transition: Le lien d'accès suivant permet d'appliquer une transition The workflow may be accssed through this link: Une transition peut être appliquée sur ce workflow grâce au lien d'accès suivant + signature_zone: + title: Appliquer les signatures électroniques + button_sign: Signer + metadata: + sign_by: 'Signature pour %name%' + docType: Type de document + docNumber: Numéro de document + docExpiration: Date d'expiration + Subscribe final: Recevoir une notification à l'étape finale Subscribe all steps: Recevoir une notification à chaque étape From 3836622d2733040ef7c76fa8ed85675dcceec876 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 18 Jul 2024 16:03:45 +0200 Subject: [PATCH 098/375] Use better namespacing for configuring workflow signature documents --- .../ChillMainExtension.php | 4 +- .../DependencyInjection/Configuration.php | 48 ++++++++++--------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index bf041b5b3..0d90854b4 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -194,8 +194,8 @@ class ChillMainExtension extends Extension implements ); $container->setParameter( - 'chill_main.id_document_kinds', - $config['id_document_kinds'] + 'chill_main.workflow_signatures.base_signer.document_kinds', + $config['workflow_signature']['base_signer']['document_kinds'] ); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php index 9c890d9fe..2185375b6 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/Configuration.php @@ -151,28 +151,6 @@ class Configuration implements ConfigurationInterface ->append($this->addWidgetsConfiguration('homepage', $this->containerBuilder)) ->end() // end of widgets/children ->end() // end of widgets - ->arrayNode('id_document_kinds')->defaultValue([]) - ->arrayPrototype() - ->children() - ->scalarNode('key')->isRequired()->cannotBeEmpty() - ->info('the key stored in database') - ->example('id_card') - ->end() - ->arrayNode('labels')->isRequired()->requiresAtLeastOneElement() - ->arrayPrototype() - ->children() - ->scalarNode('lang')->isRequired()->cannotBeEmpty() - ->example('fr') - ->end() - ->scalarNode('label')->isRequired()->cannotBeEmpty() - ->example('Carte de séjour') - ->end() - ->end() - ->end() - ->end() - ->end() - ->end() - ->end() // end of document types ->arrayNode('cruds') ->defaultValue([]) ->arrayPrototype() @@ -299,6 +277,32 @@ class Configuration implements ConfigurationInterface ->end() // end of root ; + $rootNode->children() + ->arrayNode('workflow_signature') + ->children() + ->arrayNode('base_signer') + ->children() + ->arrayNode('document_kinds') + ->arrayPrototype() + ->children() + ->scalarNode('key')->cannotBeEmpty()->end() + ->arrayNode('labels') + ->arrayPrototype() + ->children() + ->scalarNode('lang')->cannotBeEmpty()->end() + ->scalarNode('label')->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end(); + $rootNode->children() ->arrayNode('add_address')->addDefaultsIfNotSet()->children() ->scalarNode('default_country')->cannotBeEmpty()->defaultValue('BE')->end() From 919288321795f1b2d59706a419803651f89803d2 Mon Sep 17 00:00:00 2001 From: nobohan Date: Tue, 18 Jun 2024 17:31:29 +0200 Subject: [PATCH 099/375] ADDED bootstrap signature vue app --- .../Controller/DocumentPersonController.php | 19 ++++++ .../public/vuejs/DocumentSignature/App.vue | 16 +++++ .../public/vuejs/DocumentSignature/index.ts | 15 +++++ .../views/PersonDocument/signature.html.twig | 65 +++++++++++++++++++ .../chill.webpack.config.js | 1 + 5 files changed, 116 insertions(+) create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig diff --git a/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php b/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php index ad58b6e0c..c0edbe492 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php @@ -197,4 +197,23 @@ class DocumentPersonController extends AbstractController ['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); + + return $this->render( + '@ChillDocStore/PersonDocument/signature.html.twig', + ['document' => $document, 'person' => $person] + ); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue new file mode 100644 index 000000000..6dfdad330 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts new file mode 100644 index 000000000..2da7d0bf3 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts @@ -0,0 +1,15 @@ +import { createApp } from "vue"; +// import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n"; +// import { appMessages } from "ChillMainAssets/vuejs/HomepageWidget/js/i18n"; +//import { store } from "ChillDocStoreAssets/vuejs/DocumentSignature/store"; +import App from "./App.vue"; + +//const i18n = _createI18n(appMessages); + +const app = createApp({ + template: ``, +}) + //.use(store) + //.use(i18n) + .component("app", App) + .mount("#document-signature"); diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig new file mode 100644 index 000000000..d6e8b6d73 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig @@ -0,0 +1,65 @@ +{% extends "@ChillPerson/Person/layout.html.twig" %} + +{% set activeRouteKey = '' %} + +{% import "@ChillDocStore/Macro/macro.html.twig" as m %} +{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} + +{% block title %}{{ 'Detail of document of %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %} + +{% block js %} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} + + {{ encore_entry_script_tags('vue_document_signature') }} +{% endblock %} + +{% block css %} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} +{% endblock %} + +{% block content %} +

    {{ 'Document %title%' | trans({ '%title%': document.title }) }}

    + + {{ mm.mimeIcon(document.object.type) }} + +
    +
    {{ 'Title'|trans }}
    +
    {{ document.title }}
    +
    + +
    + +
      +
    • + + {{ 'Back to the list' | trans }} + +
    • + + {% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %} +
    • + +
    • + {% endif %} + +
    • + {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }} +
    • + + {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %} +
    • + + {{ 'Edit' | trans }} + +
    • + {% endif %} +{% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js b/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js index 5d021462d..0a95f61b0 100644 --- a/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js +++ b/src/Bundle/ChillDocStoreBundle/chill.webpack.config.js @@ -5,4 +5,5 @@ module.exports = function(encore) }); 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('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index.ts'); }; From bbfd0caf10281a7bde01b454d2a360103ba99d5b Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 19 Jun 2024 09:51:21 +0200 Subject: [PATCH 100/375] signature: download storedObject document in the vuejs app --- .../public/vuejs/DocumentSignature/App.vue | 48 +++++++++++++++++-- .../public/vuejs/DocumentSignature/index.ts | 2 - 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index 6dfdad330..5ad144e3d 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -1,16 +1,58 @@ + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts index 2da7d0bf3..2b2b75242 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts @@ -1,7 +1,6 @@ import { createApp } from "vue"; // import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n"; // import { appMessages } from "ChillMainAssets/vuejs/HomepageWidget/js/i18n"; -//import { store } from "ChillDocStoreAssets/vuejs/DocumentSignature/store"; import App from "./App.vue"; //const i18n = _createI18n(appMessages); @@ -9,7 +8,6 @@ import App from "./App.vue"; const app = createApp({ template: ``, }) - //.use(store) //.use(i18n) .component("app", App) .mount("#document-signature"); From 21c1e77d366fd482eff798381d02d2612a41f816 Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 19 Jun 2024 11:16:50 +0200 Subject: [PATCH 101/375] signature: POC of showing a pdf with vue-pdf-embed --- package.json | 1 + .../public/vuejs/DocumentSignature/App.vue | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2873e1317..fad48f34b 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "vue": "^3.2.37", "vue-i18n": "^9.1.6", "vue-multiselect": "3.0.0-alpha.2", + "vue-pdf-embed": "^2.0.4", "vue-toast-notification": "^2.0", "vuex": "^4.0.0" }, diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index 5ad144e3d..11a69c782 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -1,11 +1,13 @@ From 2d4fc45a0c55da351e4d024bfb4bbb9a4bedcb3a Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 20 Jun 2024 18:27:41 +0200 Subject: [PATCH 104/375] signature: manage multi-pages doc --- .../public/vuejs/DocumentSignature/App.vue | 85 ++++++++++++++----- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index dce51c8ca..eab7d64aa 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -1,6 +1,12 @@ @@ -16,23 +22,39 @@ import { download_and_decrypt_doc, } from "../StoredObjectButton/helpers"; -const msg: Ref = ref("world !"); +const page: Ref = ref(1); +const pageCount: Ref = ref(3); //TODO get page count from PDF loaded, can be done with pdfjs (https://stackoverflow.com/questions/10253669/how-to-get-the-number-of-pages-of-a-pdf-uploaded-by-user) +let userSignatureZones = {} as SignatureZone; +let pdfSource: Ref = ref(""); //console.log('signature', window.signature); const signature = { id: 1, + // storedObject: { + // filename: "gj72nCYsiuysZwZMTMVv5mhqmJdA", + // keyInfos: { + // alg: "A256CBC", + // ext: true, + // k: "WwEuXQqv5sJFzAM6P5q7Ecvbl2MiA9mE_MTQ1fAhVsY", + // key_ops: ["encrypt", "decrypt"], + // kty: "oct", + // }, + // iv: [ + // 50, 124, 210, 52, 177, 145, 165, 156, 90, 186, 155, 252, 241, 54, 194, 79, + // ], + // }, storedObject: { - filename: "gj72nCYsiuysZwZMTMVv5mhqmJdA", + filename: "U2HmWk5MGkUW1vHRA5sMEMkW9fyf", keyInfos: { alg: "A256CBC", ext: true, - k: "WwEuXQqv5sJFzAM6P5q7Ecvbl2MiA9mE_MTQ1fAhVsY", + k: "3GmJ8UBck3WhpmdoQy7cGQho0J9k9Rxhn23UIhqvpVY", key_ops: ["encrypt", "decrypt"], kty: "oct", }, iv: [ - 50, 124, 210, 52, 177, 145, 165, 156, 90, 186, 155, 252, 241, 54, 194, 79, + 254, 171, 69, 203, 89, 3, 202, 29, 187, 200, 19, 146, 201, 253, 79, 169, ], }, zones: [ @@ -45,15 +67,20 @@ const signature = { width: 80, height: 50, }, + { + page: 3, + pageWidth: 210, + pageHeight: 297, + x: 60, //from top-left corner + y: 20, + width: 80, + height: 50, + }, ], }; -let userSignatureZones = {} as SignatureZone; - const urlInfo = build_download_info_link(signature.storedObject.filename); -let pdfSource: Ref = ref(""); - async function download_and_open(): Promise { let raw; try { @@ -63,19 +90,40 @@ async function download_and_open(): Promise { new Uint8Array(signature.storedObject.iv) ); pdfSource.value = URL.createObjectURL(raw); - setTimeout(() => add_zones(), 2000); //TODO gestion async + setTimeout(() => init_pdf(), 2000); //TODO gestion async } catch (e) { console.error("error while downloading and decrypting document", e); throw e; } return raw; } + +const init_pdf = () => { + const canvas = document.querySelectorAll("canvas")[0]; + canvas.addEventListener( + "pointerup", + (e: PointerEvent) => canvas_click(e), + false + ); + add_zones(); +}; + const canvas_click = (e: PointerEvent) => { console.log("click event x and y", e.x, e.y); //TODO what to do with this: 1) draw the signature if it falls within a zone, 2) output the zone userSignatureZones = signature.zones[0]; //TODO for now, need another way to select a zone }; +const turnPage = () => { + page.value = page.value + 1; + setTimeout(() => add_zones(), 200); +}; + +const unTurnPage = () => { + page.value = page.value - 1; + setTimeout(() => add_zones(), 200); +}; + const draw_zone = ( zone: SignatureZone, ctx: CanvasRenderingContext2D, @@ -93,21 +141,16 @@ const draw_zone = ( }; const add_zones = () => { - //TODO multipage: draw zones on page change? const canvas = document.querySelectorAll("canvas")[0]; const ctx = canvas.getContext("2d"); if (ctx) { ctx.fillStyle = "green"; - signature.zones.map( - //TODO gestion du numéro de la page - (z) => draw_zone(z, ctx, canvas.width, canvas.height) - ); + signature.zones.map((z) => { + if (z.page === page.value) { + draw_zone(z, ctx, canvas.width, canvas.height); + } + }); } - canvas.addEventListener( - "pointerup", - (e: PointerEvent) => canvas_click(e), - false - ); }; download_and_open(); From 0f589ec57e0cc74894b665229f5e227ebdece1a0 Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 21 Jun 2024 13:44:41 +0200 Subject: [PATCH 105/375] signature: POC of showing PDF with pdfjs-dist --- .../public/vuejs/DocumentSignature/App.vue | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index eab7d64aa..e4ed22cd6 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -6,16 +6,23 @@ - - + + diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig index 9c2017184..431de25b7 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig @@ -1,107 +1,84 @@ -{% extends "@ChillPerson/Person/layout.html.twig" %} + + + + + + + + Signature -{% set activeRouteKey = '' %} + {{ encore_entry_link_tags('mod_bootstrap') }} -{% import "@ChillDocStore/Macro/macro.html.twig" as m %} -{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} + -{% block title %}{{ 'Detail of document of %name%'|trans({ '%name%': person|chill_entity_render_string } ) }}{% endblock %} + -{% block js %} - {{ encore_entry_script_tags('mod_document_action_buttons_group') }} - - {{ encore_entry_script_tags('vue_document_signature') }} -{% endblock %} + }; + window.signature = signature; + + {{ encore_entry_script_tags('vue_document_signature') }} + {% endblock %} -{% block css %} - {{ encore_entry_link_tags('mod_document_action_buttons_group') }} -{% endblock %} - -{% block content %} -

      {{ 'Document %title%' | trans({ '%title%': document.title }) }}

      - - {{ mm.mimeIcon(document.object.type) }} - -
      -
      {{ 'Title'|trans }}
      -
      {{ document.title }}
      -
      - -
      - -
        -
      • - - {{ 'Back to the list' | trans }} - -
      • - - {% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %} -
      • - -
      • - {% endif %} - -
      • - {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }} -
      • - - {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %} -
      • - - {{ 'Edit' | trans }} - -
      • - {% endif %} -{% endblock %} +
        +
        +
        +
        +

        {{ 'Document %title%' | trans({ '%title%': document.title }) }}

        +
        +
        +
        +
        +
        + + From 0c8ef3786092c783e263e2ff2930eea06e4b9022 Mon Sep 17 00:00:00 2001 From: nobohan Date: Tue, 2 Jul 2024 16:20:40 +0200 Subject: [PATCH 111/375] signature - more css bootstrap layout of the signature vue app --- .../public/vuejs/DocumentSignature/App.vue | 21 +++++++++++-------- .../views/PersonDocument/signature.html.twig | 2 ++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index da31c1b47..1cecea801 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -12,23 +12,26 @@
        -
        - +
        -
        -
        -
        +
        -
        +
        @@ -36,7 +39,7 @@
        - {{ page }} / {{ pageCount }} + page {{ page }} / {{ pageCount }}
        diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig index 431de25b7..df21b178d 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig @@ -8,6 +8,8 @@ Signature {{ encore_entry_link_tags('mod_bootstrap') }} + {{ encore_entry_link_tags('mod_forkawesome') }} + {{ encore_entry_link_tags('chill') }} From 5b7e3f033601f46544dfd1c5aba23678ad3aa05e Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 3 Jul 2024 16:08:44 +0200 Subject: [PATCH 112/375] signature - modale and translations in the vue app --- .../public/vuejs/DocumentSignature/App.vue | 100 ++++++++++-------- .../public/vuejs/DocumentSignature/index.ts | 21 +++- 2 files changed, 72 insertions(+), 49 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index 1cecea801..dd702c2b1 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -1,51 +1,80 @@ @@ -73,15 +102,7 @@ import { pdfjsLib.GlobalWorkerOptions.workerSrc = "pdfjs-dist/build/pdf.worker.mjs"; -// export const i18n = { -// messages: { -// fr: { -// upload_a_document: "Téléversez un document", -// }, -// }, -// }; - -const modalOpened: Ref = ref(false); +const modalOpen: Ref = ref(false); const page: Ref = ref(1); const pageCount: Ref = ref(0); const zone: Ref = ref(0); @@ -95,17 +116,6 @@ declare global { } } -interface AddressModalState { - show_modal: boolean, -} -const state: AddressModalState = reactive({show_modal: false}); -const open = (): void => { - state.show_modal = true; -} -const close = (): void => { - state.show_modal = false; -} - const signature = window.signature; const urlInfo = build_download_info_link(signature.storedObject.filename); @@ -188,7 +198,10 @@ const canvas_click = (e: PointerEvent, canvas: HTMLCanvasElement) => ) { const ctx = canvas.getContext("2d"); if (ctx) { - draw_zone(z, ctx, canvas.width, canvas.height, true); + setTimeout( + () => draw_zone(z, ctx, canvas.width, canvas.height, true), + 200 + ); } userSignatureZones.value = z; } @@ -215,7 +228,6 @@ const turn_signature = async (upOrDown: number) => { } }; - const draw_zone = ( zone: SignatureZone, ctx: CanvasRenderingContext2D, @@ -260,10 +272,6 @@ const add_zones = (page: number) => { }; const confirm_sign = () => { - open() -}; - -const post_zone = () => { console.log(userSignatureZones.value); //TODO POST userSignatureZones to backend }; diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts index 6d73bf6a4..d141a9358 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts @@ -1,12 +1,27 @@ import { createApp } from "vue"; -//import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n"; +// @ts-ignore +import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n"; import App from "./App.vue"; -// const i18n = _createI18n(appMessages); +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 signature', + cancel_signing: 'Refuser de signer', + last_sign_zone: 'Zone de signature précédente', + next_sign_zone: 'Zone de signature suivante', + } +} + +const i18n = _createI18n(appMessages); const app = createApp({ template: ``, }) - // .use(i18n) + .use(i18n) .component("app", App) .mount("#document-signature"); From c428e6665fb15bf42910f07bfdb522ad7ebac8a4 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 4 Jul 2024 15:17:04 +0200 Subject: [PATCH 113/375] signature: use PDFSignatureZoneParser in vue app signature --- .../Controller/DocumentPersonController.php | 19 ++++++- .../Resources/public/types.ts | 37 +++++++------- .../public/vuejs/DocumentSignature/App.vue | 14 ++--- .../views/PersonDocument/signature.html.twig | 51 +------------------ 4 files changed, 46 insertions(+), 75 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php b/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php index c0edbe492..4867a12b2 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/DocumentPersonController.php @@ -26,6 +26,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; +use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser; +use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; /** * Class DocumentPersonController. @@ -40,6 +42,8 @@ class DocumentPersonController extends AbstractController protected TranslatorInterface $translator, protected EventDispatcherInterface $eventDispatcher, protected AuthorizationHelper $authorizationHelper, + protected PDFSignatureZoneParser $PDFSignatureZoneParser, + protected StoredObjectManagerInterface $storedObjectManagerInterface, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry ) {} @@ -211,9 +215,22 @@ class DocumentPersonController extends AbstractController ]); $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] + ['document' => $document, 'person' => $person, 'signature' => $signature] ); } } diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts index a96949fdc..8fad4d9c8 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts @@ -26,11 +26,11 @@ export interface StoredObject { } export interface StoredObjectCreated { - status: "stored_object_created", - filename: string, - iv: Uint8Array, - keyInfos: object, - type: string, + status: "stored_object_created", + filename: string, + iv: Uint8Array, + keyInfos: object, + type: string, } export interface StoredObjectStatusChange { @@ -51,21 +51,24 @@ export type WopiEditButtonExecutableBeforeLeaveFunction = { * Object containing information for performering a POST request to a swift object store */ export interface PostStoreObjectSignature { - method: "POST", - max_file_size: number, - max_file_count: 1, - expires: number, - submit_delay: 180, - redirect: string, - prefix: string, - url: string, - signature: string, + method: "POST", + max_file_size: number, + max_file_count: 1, + expires: number, + submit_delay: 180, + redirect: string, + prefix: string, + url: string, + signature: string, } +export interface PDFPage { + index: number, + width: number, + height: number, +} export interface SignatureZone { - page: number, - pageWidth: number, - pageHeight: number, + PDFPage: PDFPage, x: number, y: number, width: number, diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index dd702c2b1..b22f4bdbd 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -179,8 +179,8 @@ const hit_signature = ( canvasWidth: number, canvasHeight: number ) => { - const scaleXToCanvas = (x: number) => (x * canvasWidth) / zone.pageWidth; - const scaleYToCanvas = (y: number) => (y * canvasHeight) / zone.pageHeight; + const scaleXToCanvas = (x: number) => (x * canvasWidth) / zone.PDFPage.width; + const scaleYToCanvas = (y: number) => (y * canvasHeight) / zone.PDFPage.height; return ( scaleXToCanvas(zone.x) < xy[0] && xy[0] < scaleXToCanvas(zone.x + zone.width) && @@ -191,7 +191,7 @@ const hit_signature = ( const canvas_click = (e: PointerEvent, canvas: HTMLCanvasElement) => signature.zones - .filter((z) => z.page === page.value) + .filter((z) => z.PDFPage.index + 1 === page.value) .map((z) => { if ( hit_signature(z, [e.offsetX, e.offsetY], canvas.width, canvas.height) @@ -217,7 +217,7 @@ const turn_page = async (upOrDown: number) => { const turn_signature = async (upOrDown: number) => { let currentZone = signature.zones[zone.value]; if (currentZone) { - page.value = currentZone.page; + page.value = currentZone.PDFPage.index + 1; await set_page(page.value); setTimeout(() => add_zones(page.value), 200); } @@ -236,9 +236,9 @@ const draw_zone = ( selected = false ) => { const scaleXToCanvas = (x: number) => - Math.round((x * canvasWidth) / zone.pageWidth); + Math.round((x * canvasWidth) / zone.PDFPage.width); const scaleYToCanvas = (y: number) => - Math.round((y * canvasHeight) / zone.pageHeight); + Math.round((y * canvasHeight) / zone.PDFPage.height); ctx.strokeStyle = selected ? "orange " : "yellow"; ctx.lineWidth = 10; ctx.lineJoin = "bevel"; @@ -266,7 +266,7 @@ const add_zones = (page: number) => { const ctx = canvas.getContext("2d"); if (ctx) { signature.zones - .filter((z) => z.page === page) + .filter((z) => z.PDFPage.index + 1 === page) .map((z) => draw_zone(z, ctx, canvas.width, canvas.height)); } }; diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig index df21b178d..5f2a2da3b 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig @@ -18,56 +18,7 @@ {% block js %} {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {{ encore_entry_script_tags('vue_document_signature') }} {% endblock %} From c968d6c541c737b1a4db51b3d68411448efa196c Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 4 Jul 2024 17:10:20 +0200 Subject: [PATCH 114/375] signature: improve layout and some functionalities of the signature app --- .../public/vuejs/DocumentSignature/App.vue | 115 +++++++++++------- .../public/vuejs/DocumentSignature/index.ts | 1 + .../views/PersonDocument/signature.html.twig | 3 +- 3 files changed, 77 insertions(+), 42 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index b22f4bdbd..1720ce996 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -16,7 +16,52 @@
        -
        +
        +
        + +
        +
        + +
        +
        + +
        +
        + + page {{ page }} / {{ pageCount }} + +
        +
        +
        +
        + +
        + +
        +
        +
        - -
        -
        - -
        -
        - -
        -
        - -
        -
        - - page {{ page }} / {{ pageCount }} - -
        -
        -
        -
        -
        @@ -180,7 +198,8 @@ const hit_signature = ( canvasHeight: number ) => { const scaleXToCanvas = (x: number) => (x * canvasWidth) / zone.PDFPage.width; - const scaleYToCanvas = (y: number) => (y * canvasHeight) / zone.PDFPage.height; + const scaleYToCanvas = (y: number) => + (y * canvasHeight) / zone.PDFPage.height; return ( scaleXToCanvas(zone.x) < xy[0] && xy[0] < scaleXToCanvas(zone.x + zone.width) && @@ -189,7 +208,8 @@ const hit_signature = ( ); }; -const canvas_click = (e: PointerEvent, canvas: HTMLCanvasElement) => +const canvas_click = (e: PointerEvent, canvas: HTMLCanvasElement) => { + undo_sign(); signature.zones .filter((z) => z.PDFPage.index + 1 === page.value) .map((z) => { @@ -206,6 +226,7 @@ const canvas_click = (e: PointerEvent, canvas: HTMLCanvasElement) => userSignatureZones.value = z; } }); +}; const turn_page = async (upOrDown: number) => { userSignatureZones.value = null; @@ -292,8 +313,20 @@ download_and_open(); diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts index d141a9358..b2bc57feb 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/index.ts @@ -11,6 +11,7 @@ const appMessages = { signature_confirmation: 'Confirmation de la signature', sign: 'Signer', choose_another_signature: 'Choisir une autre signature', + cancel: 'Annuler', cancel_signing: 'Refuser de signer', last_sign_zone: 'Zone de signature précédente', next_sign_zone: 'Zone de signature suivante', diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig index 5f2a2da3b..b410f70d3 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/PersonDocument/signature.html.twig @@ -10,6 +10,7 @@ {{ encore_entry_link_tags('mod_bootstrap') }} {{ encore_entry_link_tags('mod_forkawesome') }} {{ encore_entry_link_tags('chill') }} + {{ encore_entry_link_tags('vue_document_signature') }} @@ -27,7 +28,7 @@
        -

        {{ 'Document %title%' | trans({ '%title%': document.title }) }}

        +

        {{ 'Document %title%' | trans({ '%title%': document.title }) }}

        From fb62e54d63bf460cd50b15fc9823a0a36fcd85f5 Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 5 Jul 2024 10:10:26 +0200 Subject: [PATCH 115/375] signature: correct positioning of zones in vue app wrt to PDFSignatureZoneParser --- .../public/vuejs/DocumentSignature/App.vue | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index 1720ce996..160028ce2 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -197,14 +197,17 @@ const hit_signature = ( canvasWidth: number, canvasHeight: number ) => { - const scaleXToCanvas = (x: number) => (x * canvasWidth) / zone.PDFPage.width; + const scaleXToCanvas = (x: number) => + Math.round((x * canvasWidth) / zone.PDFPage.width); + const scaleHeightToCanvas = (h: number) => + Math.round((h * canvasHeight) / zone.PDFPage.height); const scaleYToCanvas = (y: number) => - (y * canvasHeight) / zone.PDFPage.height; + Math.round(zone.PDFPage.height - scaleHeightToCanvas(y)); return ( scaleXToCanvas(zone.x) < xy[0] && xy[0] < scaleXToCanvas(zone.x + zone.width) && scaleYToCanvas(zone.y) < xy[1] && - xy[1] < scaleYToCanvas(zone.y + zone.height) + xy[1] < scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height) ); }; @@ -258,8 +261,10 @@ const draw_zone = ( ) => { const scaleXToCanvas = (x: number) => Math.round((x * canvasWidth) / zone.PDFPage.width); + const scaleHeightToCanvas = (h: number) => + Math.round((h * canvasHeight) / zone.PDFPage.height); const scaleYToCanvas = (y: number) => - Math.round((y * canvasHeight) / zone.PDFPage.height); + Math.round(zone.PDFPage.height - scaleHeightToCanvas(y)); ctx.strokeStyle = selected ? "orange " : "yellow"; ctx.lineWidth = 10; ctx.lineJoin = "bevel"; @@ -267,13 +272,13 @@ const draw_zone = ( scaleXToCanvas(zone.x), scaleYToCanvas(zone.y), scaleXToCanvas(zone.width), - scaleYToCanvas(zone.height) + scaleHeightToCanvas(zone.height) ); ctx.font = "bold 16px serif"; ctx.textAlign = "center"; ctx.fillStyle = "black"; const xText = scaleXToCanvas(zone.x) + scaleXToCanvas(zone.width) / 2; - const yText = scaleYToCanvas(zone.y) + scaleYToCanvas(zone.height) / 2; + const yText = scaleYToCanvas(zone.y) + scaleHeightToCanvas(zone.height) / 2; ctx.strokeStyle = "grey"; ctx.strokeText("Choisir cette", xText, yText - 12); ctx.strokeText("zone de signature", xText, yText + 12); From 39d3ba2f40a42d6d366a55ac9312c3bd15eea1cd Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 5 Jul 2024 14:59:46 +0200 Subject: [PATCH 116/375] signature: fake POSTing of signature, adjustments --- .../public/vuejs/DocumentSignature/App.vue | 90 ++++++++++++------- .../public/vuejs/DocumentSignature/index.ts | 2 + .../Signature/PDFSignatureZoneParser.php | 2 +- 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue index 160028ce2..35e842243 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentSignature/App.vue @@ -5,8 +5,19 @@

        {{ $t("signature_confirmation") }}