From 4a229ebf6b20b6578975bf935829c436c27656fd Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 14 Jun 2024 15:32:51 +0200 Subject: [PATCH 001/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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/577] 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 499640e48b6f0e6c86b3c46cc119898836ba2d1b Mon Sep 17 00:00:00 2001 From: nobohan Date: Mon, 1 Jul 2024 15:33:39 +0200 Subject: [PATCH 056/577] Add a button to duplicate calendar ranges from a week to another one --- .../unreleased/Feature-20240701-153223.yaml | 5 + .../public/vuejs/MyCalendarRange/App2.vue | 298 +++++++++++++----- .../public/vuejs/MyCalendarRange/i18n.ts | 8 +- .../store/modules/calendarRanges.ts | 36 ++- 4 files changed, 266 insertions(+), 81 deletions(-) create mode 100644 .changes/unreleased/Feature-20240701-153223.yaml diff --git a/.changes/unreleased/Feature-20240701-153223.yaml b/.changes/unreleased/Feature-20240701-153223.yaml new file mode 100644 index 000000000..aaf7fa9c3 --- /dev/null +++ b/.changes/unreleased/Feature-20240701-153223.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: Add a button to duplicate calendar ranges from a week to another one +time: 2024-07-01T15:32:23.602091234+02:00 +custom: + Issue: "123" diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue index 1d353baed..873e780e4 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue @@ -1,7 +1,7 @@ diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/i18n.ts b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/i18n.ts index 2950c16da..4c910aa87 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/i18n.ts +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/i18n.ts @@ -5,11 +5,9 @@ const appMessages = { show_my_calendar: "Afficher mon calendrier", show_weekends: "Afficher les week-ends", copy_range: "Copier", - copy_range_from_to: "Copier les plages d'un jour à l'autre", - copy_range_to_next_day: "Copier les plages du jour au jour suivant", - copy_range_from_day: "Copier les plages du ", - to_the_next_day: " au jour suivant", - copy_range_to_next_week: "Copier les plages de la semaine à la semaine suivante", + copy_range_from_to: "Copier les plages", + from_day_to_day: "d'un jour à l'autre", + from_week_to_week: "d'une semaine à l'autre", copy_range_how_to: "Créez les plages de disponibilités durant une journée et copiez-les facilement au jour suivant avec ce bouton. Si les week-ends sont cachés, le jour suivant un vendredi sera le lundi.", new_range_to_save: "Nouvelles plages à enregistrer", update_range_to_save: "Plages à modifier", diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts index aaa530909..02f14e247 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts @@ -52,6 +52,23 @@ export default >{ } } + return founds; + }, + getRangesOnWeek: (state: CalendarRangesState) => (mondayDate: Date): EventInputCalendarRange[] => { + const founds = []; + for (let d of Array.from(Array(7).keys())) { + const dateOfWeek = new Date(mondayDate); + dateOfWeek.setDate(mondayDate.getDate() + d); + const dateStr = dateToISO(dateOfWeek); + for (let range of state.ranges) { + if (isEventInputCalendarRange(range) + && range.start.startsWith(dateStr) + ) { + founds.push(range); + } + } + } + return founds; }, }, @@ -238,7 +255,7 @@ export default >{ for (let r of rangesToCopy) { let start = new Date(ISOToDatetime(r.start)); - start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate()) + start.setFullYear(to.getFullYear(), to.getMonth(), to.getDate()); let end = new Date(ISOToDatetime(r.end)); end.setFullYear(to.getFullYear(), to.getMonth(), to.getDate()); let location = ctx.rootGetters['locations/getLocationById'](r.locationId); @@ -246,6 +263,23 @@ export default >{ promises.push(ctx.dispatch('createRange', {start, end, location})); } + return Promise.all(promises).then(_ => Promise.resolve(null)); + }, + copyFromWeekToAnotherWeek(ctx, {fromMonday, toMonday}: {fromMonday: Date, toMonday: Date}): Promise { + + const rangesToCopy: EventInputCalendarRange[] = ctx.getters['getRangesOnWeek'](fromMonday); + const promises = []; + const diffTime = toMonday.getTime() - fromMonday.getTime(); + for (let r of rangesToCopy) { + let start = new Date(ISOToDatetime(r.start)); + let end = new Date(ISOToDatetime(r.end)); + start.setTime(start.getTime() + diffTime); + end.setTime(end.getTime() + diffTime); + let location = ctx.rootGetters['locations/getLocationById'](r.locationId); + + promises.push(ctx.dispatch('createRange', {start, end, location})); + } + return Promise.all(promises).then(_ => Promise.resolve(null)); } } From 5b0babb9b08d260bfc0c64be37b6b43feb4ba270 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 15:37:47 +0200 Subject: [PATCH 057/577] 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 843698a1d8aabb61756cb5c82c1a03d26e573816 Mon Sep 17 00:00:00 2001 From: nobohan Date: Mon, 1 Jul 2024 15:39:52 +0200 Subject: [PATCH 058/577] DX vuejs code style --- .../Resources/public/vuejs/MyCalendarRange/App2.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue index 873e780e4..a2afd8fb0 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue @@ -241,9 +241,8 @@ const dateOptions: Intl.DateTimeFormatOptions = { const lastWeeks = computed((): Weeks[] => Array.from(Array(15).keys()).map((w) => { const lastMonday = getMonday(-w); - //copyFromWeek.value = getMonday(0); //TODO fix it return { - value: dateToISO(lastMonday), //TODO cast as maybe smthg else + value: dateToISO(lastMonday), text: `Semaine du ${lastMonday.toLocaleDateString("fr-FR", dateOptions)}`, }; }) @@ -253,7 +252,7 @@ const nextWeeks = computed((): Weeks[] => Array.from(Array(52).keys()).map((w) => { const nextMonday = getMonday(w + 1); return { - value: dateToISO(nextMonday), //TODO cast as maybe smthg else + value: dateToISO(nextMonday), text: `Semaine du ${nextMonday.toLocaleDateString("fr-FR", dateOptions)}`, }; }) 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 059/577] 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 060/577] 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 061/577] 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 062/577] 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 063/577] 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 064/577] 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 065/577] 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 066/577] 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 3bee18b0fa8da6183600d187893567181b873ed6 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 13 Jun 2024 14:00:25 +0200 Subject: [PATCH 067/577] #271 Account for acp closing date inn action filters (export) --- .changes/unreleased/Fixed-20240613-135945.yaml | 5 +++++ .../AccompanyingPeriodWorkEndDateBetweenDateFilter.php | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/Fixed-20240613-135945.yaml diff --git a/.changes/unreleased/Fixed-20240613-135945.yaml b/.changes/unreleased/Fixed-20240613-135945.yaml new file mode 100644 index 000000000..38028a735 --- /dev/null +++ b/.changes/unreleased/Fixed-20240613-135945.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: Take into account the acp closing date in the acp works date filter +time: 2024-06-13T13:59:45.561891547+02:00 +custom: + Issue: "271" diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php index 92258ccf9..38ca39320 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php @@ -86,11 +86,11 @@ final readonly class AccompanyingPeriodWorkEndDateBetweenDateFilter implements F }; $end = match ($data['keep_null']) { true => $qb->expr()->orX( - $qb->expr()->gt('acpw.endDate', ':'.$as), + $qb->expr()->gt('COALESCE(acp.closingDate, acpw.endDate)', ':'.$as), $qb->expr()->isNull('acpw.endDate') ), false => $qb->expr()->andX( - $qb->expr()->gt('acpw.endDate', ':'.$as), + $qb->expr()->gt('COALESCE(acp.closingDate, acpw.endDate)', ':'.$as), $qb->expr()->isNotNull('acpw.endDate') ), default => throw new \LogicException('This value is not supported'), From 0573f567828413d88a33d98f276e767a473e20e4 Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 3 Jul 2024 11:35:33 +0200 Subject: [PATCH 068/577] copy week in my calendar - improve layout --- .../public/vuejs/MyCalendarRange/App2.vue | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue index a2afd8fb0..e690f9524 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue @@ -63,7 +63,7 @@ -
    +
    @@ -105,38 +105,36 @@
    -
    +
    -
    -
    +
    +
    {{ $t("copy_range_from_to") }}
    -
    +
    -
    -
    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 120/577] 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 121/577] 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 122/577] 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 123/577] 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 124/577] 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 125/577] 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") }}