From 30e0c9716e6064d569ff808c040dd6ecfc11eff6 Mon Sep 17 00:00:00 2001 From: fossabot Date: Thu, 10 Mar 2022 06:13:38 -0500 Subject: [PATCH 01/46] Add license scan report and status Signed off by: fossabot --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7afacd92..07136e29 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ [![CGL](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/cgl.yaml/badge.svg)](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/cgl.yaml) [![Latest Stable Version](http://poser.pugx.org/eliashaeussler/typo3-form-consent/v)](https://packagist.org/packages/eliashaeussler/typo3-form-consent) [![License](http://poser.pugx.org/eliashaeussler/typo3-form-consent/license)](LICENSE.md) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feliashaeussler%2Ftypo3-form-consent.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Feliashaeussler%2Ftypo3-form-consent?ref=badge_shield) :package: [Packagist](https://packagist.org/packages/eliashaeussler/typo3-form-consent) | :hatched_chick: [TYPO3 extension repository](https://extensions.typo3.org/extension/form_consent) | @@ -97,3 +98,6 @@ Icons made by [Google](https://www.flaticon.com/authors/google) from ## :star: License This project is licensed under [GNU General Public License 2.0 (or later)](LICENSE.md). + + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feliashaeussler%2Ftypo3-form-consent.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Feliashaeussler%2Ftypo3-form-consent?ref=badge_large) \ No newline at end of file From ae63dea54319aa8f7fad8f4b7e1b13de1300b6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Thu, 10 Mar 2022 13:52:46 +0100 Subject: [PATCH 02/46] [DOCS] Remove FOSSA badge from README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 07136e29..0fc8ba8c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ [![CGL](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/cgl.yaml/badge.svg)](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/cgl.yaml) [![Latest Stable Version](http://poser.pugx.org/eliashaeussler/typo3-form-consent/v)](https://packagist.org/packages/eliashaeussler/typo3-form-consent) [![License](http://poser.pugx.org/eliashaeussler/typo3-form-consent/license)](LICENSE.md) -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feliashaeussler%2Ftypo3-form-consent.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Feliashaeussler%2Ftypo3-form-consent?ref=badge_shield) :package: [Packagist](https://packagist.org/packages/eliashaeussler/typo3-form-consent) | :hatched_chick: [TYPO3 extension repository](https://extensions.typo3.org/extension/form_consent) | @@ -99,5 +98,4 @@ Icons made by [Google](https://www.flaticon.com/authors/google) from This project is licensed under [GNU General Public License 2.0 (or later)](LICENSE.md). - -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feliashaeussler%2Ftypo3-form-consent.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Feliashaeussler%2Ftypo3-form-consent?ref=badge_large) \ No newline at end of file +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feliashaeussler%2Ftypo3-form-consent.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Feliashaeussler%2Ftypo3-form-consent?ref=badge_large) From b147574f3fabe9cdb385c167277020443efc04cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Thu, 10 Mar 2022 14:00:08 +0100 Subject: [PATCH 03/46] [TASK] Replace SonarCloud by codecov and CodeClimate --- .gitattributes | 1 - .github/workflows/tests.yaml | 11 ++++++----- README.md | 2 +- packaging_exclude.php | 1 - sonar-project.properties | 7 ------- 5 files changed, 7 insertions(+), 15 deletions(-) delete mode 100644 sonar-project.properties diff --git a/.gitattributes b/.gitattributes index e50beac9..67777ea7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,5 +15,4 @@ /phpunit.ci.unit.xml export-ignore /phpunit.functional.xml export-ignore /phpunit.unit.xml export-ignore -/sonar-project.properties export-ignore /typoscript-lint.yml export-ignore diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 101ddc86..6bce2c57 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -70,11 +70,12 @@ jobs: working-directory: .Build/log/coverage run: sed -i 's/\/home\/runner\/work\/typo3-form-consent\/typo3-form-consent\//\/github\/workspace\//g' clover.xml if: ${{ matrix.coverage }} - - name: Run SonarCloud scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - name: Report coverage + uses: codecov/codecov-action@v2 + with: + directory: .Build/log/coverage + fail_ci_if_error: true + verbose: true if: ${{ matrix.coverage }} tests-lowest: diff --git a/README.md b/README.md index 0fc8ba8c..544b0d27 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # TYPO3 extension `form_consent` -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=eliashaeussler_typo3-form-consent&metric=coverage)](https://sonarcloud.io/dashboard?id=eliashaeussler_typo3-form-consent) +[![Coverage](https://codecov.io/gh/eliashaeussler/typo3-form-consent/branch/master/graph/badge.svg?token=PQ0101QE3S)](https://codecov.io/gh/eliashaeussler/typo3-form-consent) [![Tests](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/tests.yaml/badge.svg)](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/tests.yaml) [![CGL](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/cgl.yaml/badge.svg)](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/cgl.yaml) [![Latest Stable Version](http://poser.pugx.org/eliashaeussler/typo3-form-consent/v)](https://packagist.org/packages/eliashaeussler/typo3-form-consent) diff --git a/packaging_exclude.php b/packaging_exclude.php index 82884247..4f0f1e0a 100644 --- a/packaging_exclude.php +++ b/packaging_exclude.php @@ -50,7 +50,6 @@ 'phpunit.ci.unit.xml', 'phpunit.functional.xml', 'phpunit.unit.xml', - 'sonar-project.properties', 'typoscript-lint.yml', ], ]; diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index fc3ace32..00000000 --- a/sonar-project.properties +++ /dev/null @@ -1,7 +0,0 @@ -sonar.projectKey=eliashaeussler_typo3-form-consent -sonar.organization=eliashaeussler -sonar.sources=. -sonar.exclusions=**/Resources/**,**/Tests/**,**/.Build/** -sonar.php.coverage.reportPaths=.Build/log/coverage/clover.xml -sonar.php.tests.reportPaths=.Build/log/coverage/junit/ -sonar.coverage.exclusions=*.php,Configuration/**,Resources/** From a75d2481b4b8b53a1ac7aec866ee42d5ab051a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Thu, 10 Mar 2022 15:22:18 +0100 Subject: [PATCH 04/46] [TASK] Add CodeClimate integration --- .github/workflows/tests.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 6bce2c57..2b68a5f3 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -66,11 +66,15 @@ jobs: composer test:ci:merge # Report coverage - - name: Fix coverage path - working-directory: .Build/log/coverage - run: sed -i 's/\/home\/runner\/work\/typo3-form-consent\/typo3-form-consent\//\/github\/workspace\//g' clover.xml + - name: CodeClimate report + uses: paambaati/codeclimate-action@v3.0.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageLocations: | + ${{ github.workspace }}/.Build/log/coverage/clover.xml:clover if: ${{ matrix.coverage }} - - name: Report coverage + - name: codecov report uses: codecov/codecov-action@v2 with: directory: .Build/log/coverage From e1a86123bdce2461f523f74e29da3ee9bf8de0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Mon, 21 Feb 2022 14:27:37 +0100 Subject: [PATCH 05/46] [!!!][FEATURE] Allow execution of post consent approval finishers (v11+) Resolves: #11 --- .github/workflows/tests.yaml | 23 +- Classes/Configuration/Localization.php | 5 + Classes/Controller/ConsentController.php | 87 +++- Classes/Domain/Factory/ConsentFactory.php | 147 ++++++ Classes/Domain/Finishers/ConsentFinisher.php | 421 ++-------------- Classes/Domain/Finishers/FinisherOptions.php | 287 +++++++++++ Classes/Domain/Model/Consent.php | 63 ++- Classes/Event/ApproveConsentEvent.php | 17 + ...vokeFinishersOnConsentApprovalListener.php | 160 +++++++ Classes/Exception/NotAllowedException.php | 38 ++ .../Exception/UnsupportedTypeException.php | 38 ++ .../ConsentConditionProvider.php | 44 ++ .../ConsentConditionFunctionsProvider.php | 79 +++ Classes/Form/Element/ConsentDataElement.php | 4 +- Classes/Http/StringableResponse.php | 40 ++ Classes/Http/StringableResponseFactory.php | 57 +++ Classes/Registry/ConsentManagerRegistry.php | 74 +++ Classes/Registry/Dto/ConsentState.php | 58 +++ Classes/Service/HashService.php | 6 +- Classes/Type/JsonType.php | 115 +++++ .../FormRequestTypeTransformer.php | 105 ++++ .../Transformer/FormValuesTypeTransformer.php | 139 ++++++ .../Transformer/TypeTransformerFactory.php | 64 +++ .../Transformer/TypeTransformerInterface.php | 40 ++ Configuration/ExpressionLanguage.php | 30 ++ Configuration/Services.php | 4 + Configuration/Services.yaml | 11 + .../tx_formconsent_domain_model_consent.php | 22 + Resources/Private/Language/locallang_db.xlf | 6 + Resources/Private/Language/locallang_form.xlf | 11 +- Tests/Functional/Configuration/IconTest.php | 16 +- .../Configuration/LocalizationTest.php | 11 + .../Domain/Finishers/FinisherOptionsTest.php | 449 ++++++++++++++++++ .../Repository/ConsentRepositoryTest.php | 13 +- Tests/Functional/Fixtures/pages.xml | 10 + .../Service/HashServiceTest.php | 92 ++-- .../FormRequestTypeTransformerTest.php | 72 +++ .../FormValuesTypeTransformerTest.php | 81 ++++ .../Provider/ConsentChartDataProviderTest.php | 4 +- Tests/Unit/Domain/Model/ConsentTest.php | 48 +- Tests/Unit/Event/ApproveConsentEventTest.php | 13 + .../Registry/ConsentManagerRegistryTest.php | 118 +++++ Tests/Unit/Registry/Dto/ConsentStateTest.php | 87 ++++ Tests/Unit/Type/JsonTypeTest.php | 101 ++++ .../TypeTransformerFactoryTest.php | 97 ++++ composer.json | 31 +- ext_emconf.php | 2 +- ext_tables.sql | 2 + 48 files changed, 2925 insertions(+), 517 deletions(-) create mode 100644 Classes/Domain/Factory/ConsentFactory.php create mode 100644 Classes/Domain/Finishers/FinisherOptions.php create mode 100644 Classes/Event/Listener/InvokeFinishersOnConsentApprovalListener.php create mode 100644 Classes/Exception/NotAllowedException.php create mode 100644 Classes/Exception/UnsupportedTypeException.php create mode 100644 Classes/ExpressionLanguage/ConsentConditionProvider.php create mode 100644 Classes/ExpressionLanguage/FunctionsProvider/ConsentConditionFunctionsProvider.php create mode 100644 Classes/Http/StringableResponse.php create mode 100644 Classes/Http/StringableResponseFactory.php create mode 100644 Classes/Registry/ConsentManagerRegistry.php create mode 100644 Classes/Registry/Dto/ConsentState.php create mode 100644 Classes/Type/JsonType.php create mode 100644 Classes/Type/Transformer/FormRequestTypeTransformer.php create mode 100644 Classes/Type/Transformer/FormValuesTypeTransformer.php create mode 100644 Classes/Type/Transformer/TypeTransformerFactory.php create mode 100644 Classes/Type/Transformer/TypeTransformerInterface.php create mode 100644 Configuration/ExpressionLanguage.php create mode 100644 Tests/Functional/Domain/Finishers/FinisherOptionsTest.php create mode 100644 Tests/Functional/Fixtures/pages.xml rename Tests/{Unit => Functional}/Service/HashServiceTest.php (67%) create mode 100644 Tests/Functional/Type/Transformer/FormRequestTypeTransformerTest.php create mode 100644 Tests/Functional/Type/Transformer/FormValuesTypeTransformerTest.php create mode 100644 Tests/Unit/Registry/ConsentManagerRegistryTest.php create mode 100644 Tests/Unit/Registry/Dto/ConsentStateTest.php create mode 100644 Tests/Unit/Type/JsonTypeTest.php create mode 100644 Tests/Unit/Type/Transformer/TypeTransformerFactoryTest.php diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2b68a5f3..d18f5565 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -8,15 +8,9 @@ jobs: strategy: fail-fast: false matrix: + typo3-version: [11.5] + php-version: [7.4, 8.0] include: - - typo3-version: 10.4 - php-version: 7.3 - - typo3-version: 10.4 - php-version: 7.4 - - typo3-version: 11.5 - php-version: 7.4 - - typo3-version: 11.5 - php-version: 8.0 - typo3-version: 11.5 php-version: 8.1 coverage: 1 @@ -88,17 +82,8 @@ jobs: strategy: fail-fast: false matrix: - include: - - typo3-version: 10.4 - php-version: 7.3 - - typo3-version: 10.4 - php-version: 7.4 - - typo3-version: 11.5 - php-version: 7.4 - - typo3-version: 11.5 - php-version: 8.0 - - typo3-version: 11.5 - php-version: 8.1 + typo3-version: [11.5] + php-version: [7.4, 8.0, 8.1] env: typo3DatabaseName: typo3 typo3DatabaseHost: '127.0.0.1' diff --git a/Classes/Configuration/Localization.php b/Classes/Configuration/Localization.php index d9c96c2d..9a0e9bc9 100644 --- a/Classes/Configuration/Localization.php +++ b/Classes/Configuration/Localization.php @@ -81,6 +81,11 @@ public static function forPlugin(string $pluginName, bool $translate = false): s return self::forKey('plugins.' . lcfirst($pluginName), self::TYPE_DATABASE, $translate); } + public static function forFinisherOption(string $option, string $item = 'label', bool $translate = false): string + { + return self::forKey('finishers.' . $option . '.' . $item, self::TYPE_FORM, $translate); + } + public static function forFormValidation(string $key, bool $translate = false): string { return self::forKey('validation.' . $key, self::TYPE_FORM, $translate); diff --git a/Classes/Controller/ConsentController.php b/Classes/Controller/ConsentController.php index c628b502..9fb8212c 100644 --- a/Classes/Controller/ConsentController.php +++ b/Classes/Controller/ConsentController.php @@ -26,7 +26,10 @@ use EliasHaeussler\Typo3FormConsent\Domain\Repository\ConsentRepository; use EliasHaeussler\Typo3FormConsent\Event\ApproveConsentEvent; use EliasHaeussler\Typo3FormConsent\Event\DismissConsentEvent; +use EliasHaeussler\Typo3FormConsent\Http\StringableResponseFactory; +use EliasHaeussler\Typo3FormConsent\Registry\ConsentManagerRegistry; use Psr\Http\Message\ResponseInterface; +use TYPO3\CMS\Core\Http\PropagateResponseException; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException; use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException; @@ -50,17 +53,26 @@ class ConsentController extends ActionController */ protected $persistenceManager; - public function __construct(ConsentRepository $consentRepository, PersistenceManagerInterface $persistenceManager) - { + /** + * @var StringableResponseFactory + */ + protected $stringableResponseFactory; + + public function __construct( + ConsentRepository $consentRepository, + PersistenceManagerInterface $persistenceManager, + StringableResponseFactory $stringableResponseFactory + ) { $this->consentRepository = $consentRepository; $this->persistenceManager = $persistenceManager; + $this->stringableResponseFactory = $stringableResponseFactory; } /** * @throws IllegalObjectTypeException * @throws UnknownObjectException */ - public function approveAction(string $hash, string $email): ?ResponseInterface + public function approveAction(string $hash, string $email): ResponseInterface { $consent = $this->consentRepository->findOneByValidationHash($hash); @@ -69,34 +81,44 @@ public function approveAction(string $hash, string $email): ?ResponseInterface // Early return if consent could not be found if (null === $consent) { - return $this->renderError('invalidConsent'); + return $this->createErrorResponse('invalidConsent'); } // Early return if given email does not match registered email if ($email !== $consent->getEmail()) { - return $this->renderError('invalidEmail'); + return $this->createErrorResponse('invalidEmail'); } // Early return if consent is already approved if ($consent->isApproved()) { - return $this->renderError('alreadyApproved'); + return $this->createErrorResponse('alreadyApproved'); } + // Register consent state + ConsentManagerRegistry::registerConsent($consent); + // Approve consent $consent->setApproved(true); $consent->setApprovalDate(new \DateTime()); $consent->setValidUntil(null); - $this->eventDispatcher->dispatch(new ApproveConsentEvent($consent)); + + // Dispatch approve event + $event = new ApproveConsentEvent($consent); + $this->eventDispatcher->dispatch($event); + $consent->setOriginalRequestParameters(null); + + // Update approved consent $this->consentRepository->update($consent); + $this->persistenceManager->persistAll(); - return $this->renderViewAsResponse(); + return $this->createHtmlResponse($event->getResponse()); } /** * @throws IllegalObjectTypeException * @throws UnknownObjectException */ - public function dismissAction(string $hash, string $email): ?ResponseInterface + public function dismissAction(string $hash, string $email): ResponseInterface { $consent = $this->consentRepository->findOneByValidationHash($hash); @@ -105,17 +127,21 @@ public function dismissAction(string $hash, string $email): ?ResponseInterface // Early return if consent could not be found if ($consent === null) { - return $this->renderError('invalidConsent'); + return $this->createErrorResponse('invalidConsent'); } // Early return if given email does not match registered email if ($consent->getEmail() !== $email) { - return $this->renderError('invalidEmail'); + return $this->createErrorResponse('invalidEmail'); } + // Register consent state + ConsentManagerRegistry::registerConsent($consent); + // Un-approve consent and obfuscate submitted data $consent->setApproved(false); - $consent->setData([]); + $consent->setData(null); + $consent->setOriginalRequestParameters(null); $this->eventDispatcher->dispatch(new DismissConsentEvent($consent)); $this->consentRepository->update($consent); @@ -123,25 +149,46 @@ public function dismissAction(string $hash, string $email): ?ResponseInterface $this->consentRepository->remove($consent); $this->persistenceManager->persistAll(); - return $this->renderViewAsResponse(); + return $this->createResponse(); } - protected function renderError(string $reason): ?ResponseInterface + protected function createErrorResponse(string $reason): ResponseInterface { $this->view->assign('error', true); $this->view->assign('reason', $reason); - return $this->renderViewAsResponse(); + return $this->createHtmlResponse(); + } + + protected function createHtmlResponse(ResponseInterface $previous = null): ResponseInterface + { + if (null === $previous) { + return $this->createResponse(); + } + + if ($previous->getStatusCode() >= 300) { + throw new PropagateResponseException($previous, 1645646663); + } + + $content = (string)$previous->getBody(); + + if ('' !== trim($content)) { + return $this->createResponse($content); + } + + return $this->createResponse(); } - protected function renderViewAsResponse(): ?ResponseInterface + protected function createResponse(string $html = null): ResponseInterface { + // TYPO3 v11+ if (method_exists($this, 'htmlResponse')) { - return $this->htmlResponse(); + return $this->htmlResponse($html); } - // For TYPO3 v10 compatibility, we return NULL in order to render - // the view from ActionController::callActionMethod(). - return null; + // TYPO3 v10 + return $this->stringableResponseFactory->createResponse() + ->withHeader('Content-Type', 'text/html; charset=utf-8') + ->withBody($this->streamFactory->createStream($html ?? $this->view->render())); } } diff --git a/Classes/Domain/Factory/ConsentFactory.php b/Classes/Domain/Factory/ConsentFactory.php new file mode 100644 index 00000000..b4526b9d --- /dev/null +++ b/Classes/Domain/Factory/ConsentFactory.php @@ -0,0 +1,147 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Domain\Factory; + +use EliasHaeussler\Typo3FormConsent\Domain\Finishers\FinisherOptions; +use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; +use EliasHaeussler\Typo3FormConsent\Event\ModifyConsentEvent; +use EliasHaeussler\Typo3FormConsent\Service\HashService; +use EliasHaeussler\Typo3FormConsent\Type\Transformer\FormRequestTypeTransformer; +use EliasHaeussler\Typo3FormConsent\Type\Transformer\FormValuesTypeTransformer; +use EliasHaeussler\Typo3FormConsent\Type\Transformer\TypeTransformerFactory; +use Psr\EventDispatcher\EventDispatcherInterface; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; + +/** + * ConsentFactory + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class ConsentFactory +{ + private ConfigurationManagerInterface $configurationManager; + private Context $context; + private EventDispatcherInterface $eventDispatcher; + private HashService $hashService; + private TypeTransformerFactory $typeTransformerFactory; + + public function __construct( + ConfigurationManagerInterface $configurationManager, + Context $context, + EventDispatcherInterface $eventDispatcher, + HashService $hashService, + TypeTransformerFactory $typeTransformerFactory + ) { + $this->configurationManager = $configurationManager; + $this->context = $context; + $this->eventDispatcher = $eventDispatcher; + $this->hashService = $hashService; + $this->typeTransformerFactory = $typeTransformerFactory; + } + + public function createFromForm(FinisherOptions $finisherOptions, FormRuntime $formRuntime): Consent + { + $submitDate = $this->getSubmitDate(); + $approvalPeriod = $finisherOptions->getApprovalPeriod(); + + $formRequestTransformer = $this->getFormRequestTransformer(); + $formValuesTransformer = $this->getFormValuesTransformer(); + + $consent = GeneralUtility::makeInstance(Consent::class) + ->setEmail($finisherOptions->getRecipientAddress()) + ->setDate($submitDate) + ->setData($formValuesTransformer->transform($formRuntime)) + ->setFormPersistenceIdentifier($formRuntime->getFormDefinition()->getPersistenceIdentifier()) + ->setOriginalRequestParameters($formRequestTransformer->transform($formRuntime)) + ->setOriginalContentElementUid($this->getCurrentContentElementUid()) + ->setValidUntil($this->calculateExpiryDate($approvalPeriod, $submitDate)) + ; + + if (($storagePid = $finisherOptions->getStoragePid()) > 0) { + $consent->setPid($storagePid); + } + + $consent->setValidationHash($this->hashService->generate($consent)); + + // Dispatch ModifyConsent event + $this->eventDispatcher->dispatch(new ModifyConsentEvent($consent)); + + // Re-generate validation hash if consent has changed in the meantime + if (!$this->hashService->isValid($consent)) { + $consent->setValidationHash($this->hashService->generate($consent)); + } + + return $consent; + } + + private function getSubmitDate(): \DateTime + { + return new \DateTime('@' . $this->context->getPropertyFromAspect('date', 'timestamp', time())); + } + + private function calculateExpiryDate(int $approvalPeriod, \DateTime $submitDate): ?\DateTime + { + // Early return if invalid approval period is given + if ($approvalPeriod <= 0) { + return null; + } + + $target = $submitDate->getTimestamp() + $approvalPeriod; + + return new \DateTime('@' . $target); + } + + private function getCurrentContentElementUid(): int + { + $contentObjectRenderer = $this->configurationManager->getContentObject(); + + if (null !== $contentObjectRenderer) { + return (int)($contentObjectRenderer->data['uid'] ?? 0); + } + + return 0; + } + + private function getFormRequestTransformer(): FormRequestTypeTransformer + { + $transformer = $this->typeTransformerFactory->get(FormRequestTypeTransformer::getName()); + + \assert($transformer instanceof FormRequestTypeTransformer); + + return $transformer; + } + + private function getFormValuesTransformer(): FormValuesTypeTransformer + { + $transformer = $this->typeTransformerFactory->get(FormValuesTypeTransformer::getName()); + + \assert($transformer instanceof FormValuesTypeTransformer); + + return $transformer; + } +} diff --git a/Classes/Domain/Finishers/ConsentFinisher.php b/Classes/Domain/Finishers/ConsentFinisher.php index f97e79a9..69d66b9e 100644 --- a/Classes/Domain/Finishers/ConsentFinisher.php +++ b/Classes/Domain/Finishers/ConsentFinisher.php @@ -23,36 +23,23 @@ namespace EliasHaeussler\Typo3FormConsent\Domain\Finishers; -use EliasHaeussler\Typo3FormConsent\Configuration\Extension; use EliasHaeussler\Typo3FormConsent\Configuration\Localization; -use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; -use EliasHaeussler\Typo3FormConsent\Domain\Repository\ConsentRepository; -use EliasHaeussler\Typo3FormConsent\Event\ModifyConsentEvent; +use EliasHaeussler\Typo3FormConsent\Domain\Factory\ConsentFactory; use EliasHaeussler\Typo3FormConsent\Event\ModifyConsentMailEvent; -use EliasHaeussler\Typo3FormConsent\Service\HashService; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mime\Address; -use TYPO3\CMS\Core\Context\Context; -use TYPO3\CMS\Core\Domain\Repository\PageRepository; use TYPO3\CMS\Core\Mail\FluidEmail; use TYPO3\CMS\Core\Mail\Mailer; use TYPO3\CMS\Core\Messaging\AbstractMessage; -use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; -use TYPO3\CMS\Extbase\Domain\Model\FileReference as ExtbaseFileReference; use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException; use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface; -use TYPO3\CMS\Fluid\View\TemplatePaths; use TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher; use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException; -use TYPO3\CMS\Form\Domain\Finishers\FlashMessageFinisher; -use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; -use TYPO3\CMS\Form\Exception; use TYPO3\CMS\Form\ViewHelpers\RenderRenderableViewHelper; /** @@ -65,99 +52,21 @@ class ConsentFinisher extends AbstractFinisher implements LoggerAwareInterface { use LoggerAwareTrait; - /** - * @var Context - */ - protected $context; - - /** - * @var EventDispatcherInterface - */ - protected $eventDispatcher; - - /** - * @var ConsentRepository - */ - protected $consentRepository; - - /** - * @var ConfigurationManagerInterface - */ - protected $configurationManager; - - /** - * @var PersistenceManagerInterface - */ - protected $persistenceManager; - - /** - * @var FlashMessageFinisher - */ - protected $flashMessageFinisher; - - /** - * @var HashService - */ - protected $hashService; - - /** - * @var PageRepository - */ - protected $pageRepository; - - /** - * @var array - */ - protected $configuration = []; - - public function injectContext(Context $context): void - { - $this->context = $context; - } - - public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void - { + protected ConsentFactory $consentFactory; + protected EventDispatcherInterface $eventDispatcher; + protected PersistenceManagerInterface $persistenceManager; + + /** @noinspection PhpMissingParentConstructorInspection */ + public function __construct( + ConsentFactory $consentFactory, + EventDispatcherInterface $eventDispatcher, + PersistenceManagerInterface $persistenceManager + ) { + $this->consentFactory = $consentFactory; $this->eventDispatcher = $eventDispatcher; - } - - public function injectConsentRepository(ConsentRepository $consentRepository): void - { - $this->consentRepository = $consentRepository; - } - - public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void - { - $this->configurationManager = $configurationManager; - $this->configuration = $this->configurationManager->getConfiguration( - ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, - Extension::NAME - ); - } - - public function injectPersistenceManager(PersistenceManagerInterface $persistenceManager): void - { $this->persistenceManager = $persistenceManager; } - public function injectFlashMessageFinisher(FlashMessageFinisher $flashMessageFinisher): void - { - $this->flashMessageFinisher = $flashMessageFinisher; - - if (method_exists($this->flashMessageFinisher, 'setFinisherIdentifier')) { - $this->flashMessageFinisher->setFinisherIdentifier('FlashMessage'); - } - } - - public function injectHashService(HashService $hashService): void - { - $this->hashService = $hashService; - } - - public function injectPageRepository(PageRepository $pageRepository): void - { - $this->pageRepository = $pageRepository; - } - /** * @throws IllegalObjectTypeException */ @@ -174,80 +83,28 @@ protected function executeInternal(): ?string /** * @throws FinisherException - * @throws IllegalObjectTypeException * @throws \Exception */ protected function executeConsent(): void { - // Parse finisher options - $recipientAddress = $this->parseOption('recipientAddress'); - $recipientName = $this->parseOption('recipientName'); - $senderAddress = $this->parseOption('senderAddress'); - $senderName = $this->parseOption('senderName'); - $approvalPeriod = (int)$this->parseOption('approvalPeriod'); - $confirmationPid = (int)$this->parseOption('confirmationPid'); - $storagePid = (int)$this->parseOption('storagePid'); - $showDismissLink = (bool)$this->parseOption('showDismissLink'); - - // Validate finisher options - $this->validateRecipientAddress($recipientAddress); - $this->validateSenderAddress($senderAddress); - $this->validateApprovalPeriod($approvalPeriod); - $this->validateConfirmationPid($confirmationPid); - $this->validateStoragePid($storagePid); - - \assert(\is_string($recipientAddress)); - \assert(\is_string($recipientName)); - \assert(\is_string($senderAddress)); - \assert(\is_string($senderName)); - - // Define consent variables - $data = $this->resolveFormData(); $formRuntime = $this->finisherContext->getFormRuntime(); - $formPersistenceIdentifier = $formRuntime->getFormDefinition()->getPersistenceIdentifier(); - $date = new \DateTime('@' . $this->context->getPropertyFromAspect('date', 'timestamp', time())); - $validUntil = $this->calculateExpiryDate($approvalPeriod, $date); - - // Build domain model - $consent = GeneralUtility::makeInstance(Consent::class) - ->setEmail($recipientAddress) - ->setDate($date) - ->setData($data) - ->setFormPersistenceIdentifier($formPersistenceIdentifier) - ->setValidUntil($validUntil); + $finisherOptions = new FinisherOptions(function (string $optionName) { + return $this->parseOption($optionName); + }); - // Build validation hash - $validationHash = $this->hashService->generate($consent); - $consent->setValidationHash($validationHash); - - // Apply storage pid if set, otherwise stick to default pid from TypoScript settings - if ($storagePid) { - $consent->setPid($storagePid); - } - - // Dispatch ModifyConsent event - $this->eventDispatcher->dispatch(new ModifyConsentEvent($consent)); - - // Re-generate validation hash if consent has changed in the meantime - if (!$this->hashService->isValid($consent)) { - $validationHash = $this->hashService->generate($consent); - $consent->setValidationHash($validationHash); - } + // Create consent + $consent = $this->consentFactory->createFromForm($finisherOptions, $formRuntime); // Persist consent - $this->consentRepository->add($consent); + $this->persistenceManager->add($consent); $this->persistenceManager->persistAll(); // Build mail - $mail = $this->initializeMail() - ->to(new Address($recipientAddress, $recipientName)) - ->assign('consent', $consent) - ->assign('formRuntime', $formRuntime) - ->assign('showDismissLink', $showDismissLink) - ->assign('confirmationPid', $confirmationPid); + $mail = $this->initializeMail($finisherOptions); + $mail->assign('consent', $consent); - if ($senderAddress !== '') { - $mail->from(new Address($senderAddress, $senderName)); + if ('' !== ($senderAddress = $finisherOptions->getSenderAddress())) { + $mail->from(new Address($senderAddress, $finisherOptions->getSenderName())); } // Provide form runtime as view helper variable to allow usage of @@ -272,121 +129,16 @@ protected function executeConsent(): void } } - protected function resolveSubject(string $subject): string + protected function initializeMail(FinisherOptions $finisherOptions): FluidEmail { - $subject = trim($subject); - if (strpos($subject, 'LLL:') === 0) { - $subject = Localization::translate($subject); - } - if ($subject === '') { - $subject = Localization::forKey('consentMail.subject', null, true); - } - return $subject; - } - - /** - * @return array - */ - protected function resolveFormData(): array - { - // Get all form values - $formData = $this->finisherContext->getFormValues(); - - // Remove honeypot field - $honeypotIdentifier = $this->getHoneypotIdentifier(); - unset($formData[$honeypotIdentifier]); - - foreach ($formData as $key => $value) { - if (\is_object($value)) { - if ($value instanceof ExtbaseFileReference) { - $value = $value->getOriginalResource(); - } - if ($value instanceof CoreFileReference) { - $formData[$key] = $value->getOriginalFile()->getUid(); - } - } - } - - return $formData; - } - - protected function getHoneypotIdentifier(): ?string - { - $formRuntime = $this->finisherContext->getFormRuntime(); - $formState = $formRuntime->getFormState(); - - // Early return if form state is not available (this should never happen) - if (null === $formState) { - return null; - } - - // Get last displayed page - $lastDisplayedPageIndex = $formState->getLastDisplayedPageIndex(); - try { - $currentPage = $formRuntime->getFormDefinition()->getPageByIndex($lastDisplayedPageIndex); - } catch (Exception $e) { - // If last displayed page is not set, try to use current page instead - $currentPage = $formRuntime->getCurrentPage(); - } - - // Early return if neither last displayed page nor current page are available - if ($currentPage === null) { - return null; - } - - // Build honeypot session identifier - $frontendUser = $this->getTypoScriptFrontendController()->fe_user; - $isUserAuthenticated = (bool)$this->context->getPropertyFromAspect('frontend.user', 'isLoggedIn'); - $sessionType = $isUserAuthenticated ? 'user' : 'ses'; - $honeypotSessionIdentifier = implode('', [ - FormRuntime::HONEYPOT_NAME_SESSION_IDENTIFIER, - $formRuntime->getIdentifier(), - $currentPage->getIdentifier(), - ]); - - return (string)$frontendUser->getKey($sessionType, $honeypotSessionIdentifier) ?: null; - } - - protected function calculateExpiryDate(int $approvalPeriod, \DateTime $base = null): ?\DateTime - { - // Early return if invalid approval period is given - if ($approvalPeriod <= 0) { - return null; - } - - $base = $base !== null ? clone $base : new \DateTime(); - $target = $base->getTimestamp() + $approvalPeriod; - - return new \DateTime('@' . $target); - } - - protected function initializeMail(): FluidEmail - { - $defaultTemplateConfiguration = $GLOBALS['TYPO3_CONF_VARS']['MAIL']; - $typoScriptTemplateConfiguration = $this->configuration['view'] ?? []; - $finisherTemplateConfiguration = [ - 'templateRootPaths' => $this->options['templateRootPaths'] ?? [], - 'partialRootPaths' => $this->options['partialRootPaths'] ?? [], - 'layoutRootPaths' => $this->options['layoutRootPaths'] ?? [], - ]; - $mergedTemplateConfiguration = array_replace_recursive( - $defaultTemplateConfiguration, - $typoScriptTemplateConfiguration, - $finisherTemplateConfiguration - ); - $templatePaths = GeneralUtility::makeInstance(TemplatePaths::class, $mergedTemplateConfiguration); - - // Resolve mail subject - $subject = $this->parseOption('subject'); - if (!\is_string($subject)) { - $subject = ''; - } - $subject = $this->resolveSubject($subject); - // Initialize mail - $mail = GeneralUtility::makeInstance(FluidEmail::class, $templatePaths) - ->subject($subject) - ->setTemplate('ConsentMail'); + $mail = GeneralUtility::makeInstance(FluidEmail::class, $finisherOptions->getTemplatePaths()) + ->to(new Address($finisherOptions->getRecipientAddress(), $finisherOptions->getRecipientName())) + ->subject($finisherOptions->getSubject()) + ->setTemplate('ConsentMail') + ->assign('formRuntime', $this->finisherContext->getFormRuntime()) + ->assign('showDismissLink', $finisherOptions->getShowDismissLink()) + ->assign('confirmationPid', $finisherOptions->getConfirmationPid()); // Set the PSR-7 request object if available $serverRequest = $this->getServerRequest(); @@ -397,117 +149,20 @@ protected function initializeMail(): FluidEmail return $mail; } - /** - * @param mixed $recipientAddress - * @throws FinisherException - */ - protected function validateRecipientAddress($recipientAddress): void - { - if (!\is_string($recipientAddress)) { - throw new FinisherException( - Localization::forFormValidation('recipientAddress.invalid', true), - 1640186663 - ); - } - if ('' === trim($recipientAddress)) { - throw new FinisherException( - Localization::forFormValidation('recipientAddress.empty', true), - 1576947638 - ); - } - if (!GeneralUtility::validEmail($recipientAddress)) { - throw new FinisherException( - Localization::forFormValidation('recipientAddress.invalid', true), - 1576947682 - ); - } - } - - /** - * @param mixed $senderAddress - * @throws FinisherException - */ - protected function validateSenderAddress($senderAddress): void - { - if (!\is_string($senderAddress)) { - throw new FinisherException( - Localization::forFormValidation('senderAddress.invalid', true), - 1640186811 - ); - } - if ('' !== trim($senderAddress) && !GeneralUtility::validEmail($senderAddress)) { - throw new FinisherException( - Localization::forFormValidation('senderAddress.invalid', true), - 1587842752 - ); - } - } - - /** - * @throws FinisherException - */ - protected function validateApprovalPeriod(int $approvalPeriod): void - { - if ($approvalPeriod < 0) { - throw new FinisherException( - Localization::forFormValidation('validationPeriod.invalid', true), - 1576948900 - ); - } - } - - /** - * @throws FinisherException - */ - protected function validateConfirmationPid(int $confirmationPid): void - { - if ($confirmationPid <= 0) { - throw new FinisherException( - Localization::forFormValidation('confirmationPid.empty', true), - 1576948961 - ); - } - if (!\is_array($this->pageRepository->checkRecord('pages', $confirmationPid))) { - throw new FinisherException( - Localization::forFormValidation('confirmationPid.invalid', true), - 1576949163 - ); - } - } - - /** - * @throws FinisherException - */ - protected function validateStoragePid(int $storagePid): void - { - // Return if storage pid is not set since it is not a mandatory option - if ($storagePid === 0) { - return; - } - - if ($storagePid < 0) { - throw new FinisherException( - Localization::forFormValidation('storagePid.empty', true), - 1576951495 - ); - } - if (!\is_array($this->pageRepository->checkRecord('pages', $storagePid))) { - throw new FinisherException( - Localization::forFormValidation('storagePid.invalid', true), - 1576951499 - ); - } - } - protected function addFlashMessage(\Exception $exception, bool $cancel = true): void { - // Add flash message - $this->flashMessageFinisher->setOptions([ + $formDefinition = $this->finisherContext->getFormRuntime()->getFormDefinition(); + $flashMessageFinisher = $formDefinition->createFinisher('FlashMessage', [ 'messageBody' => $exception->getMessage(), 'messageCode' => $exception->getCode(), 'severity' => AbstractMessage::ERROR, + + // Disable finisher since it's already executed below + 'renderingOptions' => [ + 'enabled' => false, + ], ]); - $this->flashMessageFinisher->execute($this->finisherContext); + $flashMessageFinisher->execute($this->finisherContext); // Cancel execution if ($cancel) { diff --git a/Classes/Domain/Finishers/FinisherOptions.php b/Classes/Domain/Finishers/FinisherOptions.php new file mode 100644 index 00000000..eea9f437 --- /dev/null +++ b/Classes/Domain/Finishers/FinisherOptions.php @@ -0,0 +1,287 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Domain\Finishers; + +use EliasHaeussler\Typo3FormConsent\Configuration\Extension; +use EliasHaeussler\Typo3FormConsent\Configuration\Localization; +use EliasHaeussler\Typo3FormConsent\Exception\NotAllowedException; +use TYPO3\CMS\Core\Domain\Repository\PageRepository; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; +use TYPO3\CMS\Fluid\View\TemplatePaths; +use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException; + +/** + * FinisherOptions + * + * @author Elias Häußler + * @license GPL-2.0-or-later + * + * @implements \ArrayAccess + */ +final class FinisherOptions implements \ArrayAccess +{ + /** + * @var PageRepository + */ + private static $pageRepository; + + /** + * @var callable(string): mixed + */ + private $optionFetcher; + + /** + * @var array{subject?: string, templatePaths?: TemplatePaths, recipientAddress?: string, recipientName?: string, senderAddress?: string, senderName?: string, approvalPeriod?: int, confirmationPid?: int, storagePid?: int, showDismissLink?: bool} + */ + private array $parsedOptions = []; + + /** + * @param callable(string): mixed $optionFetcher + */ + public function __construct(callable $optionFetcher) + { + $this->optionFetcher = $optionFetcher; + } + + public function getSubject(): string + { + if (isset($this->parsedOptions['subject'])) { + return $this->parsedOptions['subject']; + } + + $subject = trim((string)($this->optionFetcher)('subject')); + + if (str_starts_with($subject, 'LLL:')) { + $subject = Localization::translate($subject); + } + if ($subject === '') { + $subject = Localization::forKey('consentMail.subject', null, true); + } + + return $this->parsedOptions['subject'] = $subject; + } + + public function getTemplatePaths(): TemplatePaths + { + if (isset($this->parsedOptions['templatePaths'])) { + return $this->parsedOptions['templatePaths']; + } + + $configurationManager = GeneralUtility::makeInstance(ConfigurationManagerInterface::class); + $typoScriptConfiguration = $configurationManager->getConfiguration( + ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, + Extension::NAME + ); + $typoScriptTemplateConfiguration = $typoScriptConfiguration['view'] ?? []; + + $defaultTemplateConfiguration = $GLOBALS['TYPO3_CONF_VARS']['MAIL']; + $finisherTemplateConfiguration = [ + 'templateRootPaths' => ($this->optionFetcher)('templateRootPaths') ?? [], + 'partialRootPaths' => ($this->optionFetcher)('partialRootPaths') ?? [], + 'layoutRootPaths' => ($this->optionFetcher)('layoutRootPaths') ?? [], + ]; + + $mergedTemplateConfiguration = array_replace_recursive( + $defaultTemplateConfiguration, + $typoScriptTemplateConfiguration, + $finisherTemplateConfiguration + ); + + return $this->parsedOptions['templatePaths'] = GeneralUtility::makeInstance( + TemplatePaths::class, + $mergedTemplateConfiguration + ); + } + + public function getRecipientAddress(): string + { + if (isset($this->parsedOptions['recipientAddress'])) { + return $this->parsedOptions['recipientAddress']; + } + + $recipientAddress = ($this->optionFetcher)('recipientAddress'); + + if (!\is_string($recipientAddress)) { + $this->throwException('recipientAddress.invalid', 1640186663); + } + if ('' === trim($recipientAddress)) { + $this->throwException('recipientAddress.empty', 1576947638); + } + if (!GeneralUtility::validEmail($recipientAddress)) { + $this->throwException('recipientAddress.invalid', 1576947682); + } + + return $this->parsedOptions['recipientAddress'] = $recipientAddress; + } + + public function getRecipientName(): string + { + return $this->parsedOptions['recipientName'] + ?? $this->parsedOptions['recipientName'] = (string)($this->optionFetcher)('recipientName'); + } + + public function getSenderAddress(): string + { + if (isset($this->parsedOptions['senderAddress'])) { + return $this->parsedOptions['senderAddress']; + } + + $senderAddress = ($this->optionFetcher)('senderAddress'); + + if (!\is_string($senderAddress)) { + $this->throwException('senderAddress.invalid', 1640186811); + } + if ('' !== trim($senderAddress) && !GeneralUtility::validEmail($senderAddress)) { + $this->throwException('senderAddress.invalid', 1587842752); + } + + return $this->parsedOptions['senderAddress'] = $senderAddress; + } + + public function getSenderName(): string + { + return $this->parsedOptions['senderName'] + ?? $this->parsedOptions['senderName'] = (string)($this->optionFetcher)('senderName'); + } + + public function getApprovalPeriod(): int + { + if (isset($this->parsedOptions['approvalPeriod'])) { + return $this->parsedOptions['approvalPeriod']; + } + + $approvalPeriod = (int)($this->optionFetcher)('approvalPeriod'); + + if ($approvalPeriod < 0) { + $this->throwException('approvalPeriod.invalid', 1576948900); + } + + return $this->parsedOptions['approvalPeriod'] = $approvalPeriod; + } + + public function getConfirmationPid(): int + { + if (isset($this->parsedOptions['confirmationPid'])) { + return $this->parsedOptions['confirmationPid']; + } + + $confirmationPid = (int)($this->optionFetcher)('confirmationPid'); + + if ($confirmationPid <= 0) { + $this->throwException('confirmationPid.empty', 1576948961); + } + if (!\is_array($this->getPageRepository()->checkRecord('pages', $confirmationPid))) { + $this->throwException('confirmationPid.invalid', 1576949163); + } + + return $this->parsedOptions['confirmationPid'] = $confirmationPid; + } + + public function getStoragePid(): int + { + if (isset($this->parsedOptions['storagePid'])) { + return $this->parsedOptions['storagePid']; + } + + $storagePid = (int)($this->optionFetcher)('storagePid'); + + // Return if storage pid is not set since it is not a mandatory option + if ($storagePid === 0) { + return $this->parsedOptions['storagePid'] = $storagePid; + } + + if ($storagePid < 0) { + $this->throwException('storagePid.empty', 1576951495); + } + if (!\is_array($this->getPageRepository()->checkRecord('pages', $storagePid))) { + $this->throwException('storagePid.invalid', 1576951499); + } + + return $this->parsedOptions['storagePid'] = $storagePid; + } + + public function getShowDismissLink(): bool + { + return $this->parsedOptions['showDismissLink'] + ?? $this->parsedOptions['showDismissLink'] = (bool)($this->optionFetcher)('showDismissLink'); + } + + public function offsetExists($offset): bool + { + if (!\is_string($offset)) { + return false; + } + + $getterMethodName = 'get' . ucfirst($offset); + if (method_exists($this, $getterMethodName)) { + return true; + } + + return false; + } + + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + if (!\is_string($offset)) { + return null; + } + + $getterMethodName = 'get' . ucfirst($offset); + if (method_exists($this, $getterMethodName)) { + return $this->{$getterMethodName}(); + } + + return null; + } + + public function offsetSet($offset, $value): void + { + throw NotAllowedException::forMethod(__METHOD__); + } + + public function offsetUnset($offset): void + { + throw NotAllowedException::forMethod(__METHOD__); + } + + private function getPageRepository(): PageRepository + { + if (null === self::$pageRepository) { + self::$pageRepository = GeneralUtility::makeInstance(PageRepository::class); + } + + return self::$pageRepository; + } + + /** + * @throws FinisherException + * @return never-returns + */ + private function throwException(string $message, int $code = 0): void + { + throw new FinisherException(Localization::forFormValidation($message, true), $code); + } +} diff --git a/Classes/Domain/Model/Consent.php b/Classes/Domain/Model/Consent.php index 0cf9d188..f899bb94 100644 --- a/Classes/Domain/Model/Consent.php +++ b/Classes/Domain/Model/Consent.php @@ -23,6 +23,7 @@ namespace EliasHaeussler\Typo3FormConsent\Domain\Model; +use EliasHaeussler\Typo3FormConsent\Type\JsonType; use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; /** @@ -46,15 +47,25 @@ class Consent extends AbstractEntity protected $date; /** - * @var string + * @var JsonType|null */ - protected $data = ''; + protected $data; /** * @var string */ protected $formPersistenceIdentifier = ''; + /** + * @var JsonType>>|null + */ + protected $originalRequestParameters; + + /** + * @var int + */ + protected $originalContentElementUid = 0; + /** * @var bool */ @@ -97,28 +108,20 @@ public function setDate(\DateTime $date): self return $this; } - public function getData(): string - { - return $this->data; - } - /** - * @return array + * @return JsonType|null */ - public function getDataArray(): array + public function getData(): ?JsonType { - return json_decode($this->data, true) ?: []; + return $this->data; } /** - * @param string|array $data + * @param JsonType|null $data */ - public function setData($data): self + public function setData(?JsonType $data): self { - if (\is_array($data)) { - $data = json_encode($data) ?: ''; - } - $this->data = (string)$data; + $this->data = $data; return $this; } @@ -133,6 +136,34 @@ public function setFormPersistenceIdentifier(string $formPersistenceIdentifier): return $this; } + /** + * @return JsonType>>|null + */ + public function getOriginalRequestParameters(): ?JsonType + { + return $this->originalRequestParameters; + } + + /** + * @param JsonType>>|null $originalRequestParameters + */ + public function setOriginalRequestParameters(?JsonType $originalRequestParameters): self + { + $this->originalRequestParameters = $originalRequestParameters; + return $this; + } + + public function getOriginalContentElementUid(): int + { + return $this->originalContentElementUid; + } + + public function setOriginalContentElementUid(int $originalContentElementUid): self + { + $this->originalContentElementUid = $originalContentElementUid; + return $this; + } + public function isApproved(): bool { return $this->approved; diff --git a/Classes/Event/ApproveConsentEvent.php b/Classes/Event/ApproveConsentEvent.php index f6d9ee8b..fb336c26 100644 --- a/Classes/Event/ApproveConsentEvent.php +++ b/Classes/Event/ApproveConsentEvent.php @@ -24,6 +24,7 @@ namespace EliasHaeussler\Typo3FormConsent\Event; use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; +use Psr\Http\Message\ResponseInterface; /** * ApproveConsentEvent @@ -38,6 +39,11 @@ class ApproveConsentEvent */ protected $consent; + /** + * @var ResponseInterface|null + */ + protected $response; + public function __construct(Consent $consent) { $this->consent = $consent; @@ -47,4 +53,15 @@ public function getConsent(): Consent { return $this->consent; } + + public function getResponse(): ?ResponseInterface + { + return $this->response; + } + + public function setResponse(?ResponseInterface $response): self + { + $this->response = $response; + return $this; + } } diff --git a/Classes/Event/Listener/InvokeFinishersOnConsentApprovalListener.php b/Classes/Event/Listener/InvokeFinishersOnConsentApprovalListener.php new file mode 100644 index 00000000..01f6fe6a --- /dev/null +++ b/Classes/Event/Listener/InvokeFinishersOnConsentApprovalListener.php @@ -0,0 +1,160 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Event\Listener; + +use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; +use EliasHaeussler\Typo3FormConsent\Event\ApproveConsentEvent; +use EliasHaeussler\Typo3FormConsent\Type\JsonType; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Domain\Repository\PageRepository; +use TYPO3\CMS\Core\Http\ImmediateResponseException; +use TYPO3\CMS\Core\Http\Response; +use TYPO3\CMS\Core\Http\ServerRequestFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Core\Bootstrap; +use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; + +/** + * InvokeFinishersOnConsentApprovalListener + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class InvokeFinishersOnConsentApprovalListener +{ + private FormPersistenceManagerInterface $formPersistenceManager; + private PageRepository $pageRepository; + + public function __construct(FormPersistenceManagerInterface $formPersistenceManager, PageRepository $pageRepository) + { + $this->formPersistenceManager = $formPersistenceManager; + $this->pageRepository = $pageRepository; + } + + public function __invoke(ApproveConsentEvent $event): void + { + $consent = $event->getConsent(); + + // Early return if original request is missing + // or no finisher variants are configured + if ( + empty($consent->getOriginalRequestParameters()) + || 0 === $consent->getOriginalContentElementUid() + || !$this->arePostApprovalVariantsConfigured($consent->getFormPersistenceIdentifier()) + ) { + return; + } + + // Re-render form to invoke finishers + $request = $this->createRequestFromOriginalRequestParameters($consent->getOriginalRequestParameters()); + $response = $this->dispatchFormReRendering($consent, $request); + $event->setResponse($response); + } + + private function dispatchFormReRendering(Consent $consent, ServerRequestInterface $serverRequest): ?ResponseInterface + { + // Fetch record of original content element + $contentElementRecord = $this->fetchOriginalContentElementRecord($consent->getOriginalContentElementUid()); + + // Early return if content element record cannot be resolved + if (!\is_array($contentElementRecord)) { + return null; + } + + // Build extbase bootstrap object + $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); + $contentObjectRenderer->start($contentElementRecord, 'tt_content', $serverRequest); + $contentObjectRenderer->setUserObjectType(ContentObjectRenderer::OBJECTTYPE_USER_INT); + $bootstrap = GeneralUtility::makeInstance(Bootstrap::class); + $bootstrap->setContentObjectRenderer($contentObjectRenderer); + + $configuration = [ + 'extensionName' => 'Form', + 'pluginName' => 'Formframework', + ]; + + try { + // Dispatch extbase request + $content = $bootstrap->run('', $configuration, $serverRequest); + $response = new Response(); + $response->getBody()->write($content); + + return $response; + } catch (ImmediateResponseException $exception) { + // If any immediate response is thrown, use this for further processing + return $exception->getResponse(); + } + } + + /** + * @return array|null + */ + private function fetchOriginalContentElementRecord(int $contentElementUid): ?array + { + // Early return if content element UID cannot be determined + if (0 === $contentElementUid) { + return null; + } + + // Fetch content element record + $record = $this->pageRepository->checkRecord('tt_content', $contentElementUid); + + // Early return if content element record cannot be resolved + if (!\is_array($record)) { + return null; + } + + return $this->pageRepository->getLanguageOverlay('tt_content', $record); + } + + /** + * @param JsonType>> $originalRequestParameters + */ + private function createRequestFromOriginalRequestParameters(JsonType $originalRequestParameters): ServerRequestInterface + { + return $this->getServerRequest() + ->withMethod('POST') + ->withParsedBody($originalRequestParameters->toArray()); + } + + private function arePostApprovalVariantsConfigured(string $formPersistenceIdentifier): bool + { + $formConfiguration = $this->formPersistenceManager->load($formPersistenceIdentifier); + + foreach ($formConfiguration['variants'] ?? [] as $variant) { + if (str_contains($variant['condition'] ?? '', 'isConsentApproved()') && isset($variant['finishers'])) { + return true; + } + } + + return false; + } + + private function getServerRequest(): ServerRequestInterface + { + return $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + } +} diff --git a/Classes/Exception/NotAllowedException.php b/Classes/Exception/NotAllowedException.php new file mode 100644 index 00000000..22e2831a --- /dev/null +++ b/Classes/Exception/NotAllowedException.php @@ -0,0 +1,38 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Exception; + +/** + * NotAllowedException + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class NotAllowedException extends \Exception +{ + public static function forMethod(string $methodName): self + { + return new self(sprintf('Calling the method "%s" is not allowed.', $methodName), 1645781267); + } +} diff --git a/Classes/Exception/UnsupportedTypeException.php b/Classes/Exception/UnsupportedTypeException.php new file mode 100644 index 00000000..1d2fa45c --- /dev/null +++ b/Classes/Exception/UnsupportedTypeException.php @@ -0,0 +1,38 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Exception; + +/** + * UnsupportedTypeException + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class UnsupportedTypeException extends \Exception +{ + public static function create(string $type): self + { + return new self(sprintf('The type "%s" is not supported.', $type), 1645774926); + } +} diff --git a/Classes/ExpressionLanguage/ConsentConditionProvider.php b/Classes/ExpressionLanguage/ConsentConditionProvider.php new file mode 100644 index 00000000..d3ca87c5 --- /dev/null +++ b/Classes/ExpressionLanguage/ConsentConditionProvider.php @@ -0,0 +1,44 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\ExpressionLanguage; + +use EliasHaeussler\Typo3FormConsent\ExpressionLanguage\FunctionsProvider\ConsentConditionFunctionsProvider; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; +use TYPO3\CMS\Core\ExpressionLanguage\AbstractProvider; + +/** + * ConsentConditionProvider + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class ConsentConditionProvider extends AbstractProvider +{ + /** + * @var list> + */ + protected $expressionLanguageProviders = [ + ConsentConditionFunctionsProvider::class, + ]; +} diff --git a/Classes/ExpressionLanguage/FunctionsProvider/ConsentConditionFunctionsProvider.php b/Classes/ExpressionLanguage/FunctionsProvider/ConsentConditionFunctionsProvider.php new file mode 100644 index 00000000..970a09e9 --- /dev/null +++ b/Classes/ExpressionLanguage/FunctionsProvider/ConsentConditionFunctionsProvider.php @@ -0,0 +1,79 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\ExpressionLanguage\FunctionsProvider; + +use EliasHaeussler\Typo3FormConsent\Registry\ConsentManagerRegistry; +use Symfony\Component\ExpressionLanguage\ExpressionFunction; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; + +/** + * ConsentConditionFunctionsProvider + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class ConsentConditionFunctionsProvider implements ExpressionFunctionProviderInterface +{ + /** + * @return list + */ + public function getFunctions(): array + { + return [ + $this->getIsConsentApprovedFunction(), + $this->getIsConsentDismissedFunction(), + ]; + } + + private function getIsConsentApprovedFunction(): ExpressionFunction + { + return new ExpressionFunction('isConsentApproved', static function () { + // Not implemented, we only use the evaluator + }, static function ($arguments): bool { + $formRuntime = $arguments['formRuntime'] ?? null; + + if (!($formRuntime instanceof FormRuntime)) { + return false; + } + + return ConsentManagerRegistry::isConsentApproved($formRuntime->getFormDefinition()->getPersistenceIdentifier()); + }); + } + + private function getIsConsentDismissedFunction(): ExpressionFunction + { + return new ExpressionFunction('isConsentDismissed', static function () { + // Not implemented, we only use the evaluator + }, static function ($arguments): bool { + $formRuntime = $arguments['formRuntime'] ?? null; + + if (!($formRuntime instanceof FormRuntime)) { + return false; + } + + return ConsentManagerRegistry::isConsentDismissed($formRuntime->getFormDefinition()->getPersistenceIdentifier()); + }); + } +} diff --git a/Classes/Form/Element/ConsentDataElement.php b/Classes/Form/Element/ConsentDataElement.php index e1fa634c..55cf467a 100644 --- a/Classes/Form/Element/ConsentDataElement.php +++ b/Classes/Form/Element/ConsentDataElement.php @@ -85,7 +85,7 @@ protected function renderFormElement(array $result, string $elementHtml): array $html[] = ''; $html[] = ''; - $result['html'] = implode(LF, $html); + $result['html'] = implode(PHP_EOL, $html); return $result; } @@ -96,6 +96,6 @@ protected function renderAlert(string $localizationKey): string $html[] = Localization::forBackendForm('message.' . $localizationKey, true); $html[] = ''; - return implode(LF, $html); + return implode(PHP_EOL, $html); } } diff --git a/Classes/Http/StringableResponse.php b/Classes/Http/StringableResponse.php new file mode 100644 index 00000000..49dcf82a --- /dev/null +++ b/Classes/Http/StringableResponse.php @@ -0,0 +1,40 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Http; + +use TYPO3\CMS\Core\Http\Response; + +/** + * StringableResponse + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class StringableResponse extends Response +{ + public function __toString(): string + { + return (string)$this->body; + } +} diff --git a/Classes/Http/StringableResponseFactory.php b/Classes/Http/StringableResponseFactory.php new file mode 100644 index 00000000..baafb1a6 --- /dev/null +++ b/Classes/Http/StringableResponseFactory.php @@ -0,0 +1,57 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Http; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use TYPO3\CMS\Core\Http\Response; +use TYPO3\CMS\Core\Information\Typo3Version; + +/** + * StringableResponseFactory + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class StringableResponseFactory implements ResponseFactoryInterface +{ + /** + * @var bool + */ + private $useCompatibilityLayer; + + public function __construct() + { + $this->useCompatibilityLayer = (new Typo3Version())->getMajorVersion() < 11; + } + + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + if ($this->useCompatibilityLayer) { + return new StringableResponse('php://temp', $code, [], $reasonPhrase); + } + + return new Response(null, $code, [], $reasonPhrase); + } +} diff --git a/Classes/Registry/ConsentManagerRegistry.php b/Classes/Registry/ConsentManagerRegistry.php new file mode 100644 index 00000000..95d01b93 --- /dev/null +++ b/Classes/Registry/ConsentManagerRegistry.php @@ -0,0 +1,74 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Registry; + +use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; +use EliasHaeussler\Typo3FormConsent\Registry\Dto\ConsentState; + +/** + * ConsentManagerRegistry + * + * @author Elias Häußler + * @license GPL-2.0-or-later + * @internal + */ +final class ConsentManagerRegistry +{ + /** + * @var array> + */ + private static $states = []; + + public static function registerConsent(Consent $consent): ConsentState + { + return self::$states[$consent->getFormPersistenceIdentifier()][(int)$consent->getUid()] = new ConsentState($consent); + } + + public static function unregisterConsent(Consent $consent): void + { + unset(self::$states[$consent->getFormPersistenceIdentifier()][(int)$consent->getUid()]); + } + + public static function isConsentApproved(string $formPersistenceIdentifier): bool + { + foreach (self::$states[$formPersistenceIdentifier] ?? [] as $state) { + if ($state->isApproved()) { + return true; + } + } + + return false; + } + + public static function isConsentDismissed(string $formPersistenceIdentifier): bool + { + foreach (self::$states[$formPersistenceIdentifier] ?? [] as $state) { + if ($state->isDismissed()) { + return true; + } + } + + return false; + } +} diff --git a/Classes/Registry/Dto/ConsentState.php b/Classes/Registry/Dto/ConsentState.php new file mode 100644 index 00000000..56e9bf14 --- /dev/null +++ b/Classes/Registry/Dto/ConsentState.php @@ -0,0 +1,58 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Registry\Dto; + +use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; + +/** + * ConsentState + * + * @author Elias Häußler + * @license GPL-2.0-or-later + * @internal + */ +final class ConsentState +{ + /** + * @var Consent + */ + private $consent; + + public function __construct(Consent $consent) + { + $this->consent = $consent; + } + + public function isApproved(): bool + { + return $this->consent->isApproved(); + } + + public function isDismissed(): bool + { + return !$this->consent->isApproved() + && null === $this->consent->getData() + && null === $this->consent->getOriginalRequestParameters(); + } +} diff --git a/Classes/Service/HashService.php b/Classes/Service/HashService.php index 48dfc074..c2fd3a55 100644 --- a/Classes/Service/HashService.php +++ b/Classes/Service/HashService.php @@ -50,9 +50,11 @@ public function generate(Consent $consent): string { $hashComponents = [ $consent->getDate()->getTimestamp(), - $consent->getData(), ]; - if ($consent->getValidUntil() !== null) { + if (null !== $consent->getData()) { + $hashComponents[] = (string)$consent->getData(); + } + if (null !== $consent->getValidUntil()) { $hashComponents[] = $consent->getValidUntil()->getTimestamp(); } diff --git a/Classes/Type/JsonType.php b/Classes/Type/JsonType.php new file mode 100644 index 00000000..c0b8dfbf --- /dev/null +++ b/Classes/Type/JsonType.php @@ -0,0 +1,115 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Type; + +use TYPO3\CMS\Core\Type\TypeInterface; + +/** + * JsonType + * + * @author Elias Häußler + * @license GPL-2.0-or-later + * + * @template TKey + * @template TValue + * @implements \ArrayAccess + */ +final class JsonType implements TypeInterface, \ArrayAccess, \JsonSerializable +{ + /** + * @var array + */ + private array $data; + + public function __construct(string $json) + { + $this->data = json_decode($json, true); + } + + /** + * @param array $data + * @return self + * @throws \JsonException + */ + public static function fromArray(array $data): self + { + return new self(json_encode($data, JSON_THROW_ON_ERROR)); + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->data; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->data; + } + + public function __toString(): string + { + return json_encode($this, JSON_THROW_ON_ERROR); + } + + /** + * @param TKey $offset + */ + public function offsetExists($offset): bool + { + return isset($this->data[$offset]); + } + + /** + * @param TKey $offset + * @return TValue|null + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->data[$offset] ?? null; + } + + /** + * @param TKey $offset + * @param TValue $value + */ + public function offsetSet($offset, $value): void + { + $this->data[$offset] = $value; + } + + /** + * @param TKey $offset + */ + public function offsetUnset($offset): void + { + unset($this->data[$offset]); + } +} diff --git a/Classes/Type/Transformer/FormRequestTypeTransformer.php b/Classes/Type/Transformer/FormRequestTypeTransformer.php new file mode 100644 index 00000000..d1fa001d --- /dev/null +++ b/Classes/Type/Transformer/FormRequestTypeTransformer.php @@ -0,0 +1,105 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Type\Transformer; + +use EliasHaeussler\Typo3FormConsent\Type\JsonType; +use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference; +use TYPO3\CMS\Core\Utility\ArrayUtility; +use TYPO3\CMS\Extbase\Domain\Model\FileReference as ExtbaseFileReference; +use TYPO3\CMS\Extbase\Security\Cryptography\HashService; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; + +/** + * FormRequestTypeTransformer + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class FormRequestTypeTransformer implements TypeTransformerInterface +{ + /** + * @var HashService + */ + private $hashService; + + public function __construct(HashService $hashService) + { + $this->hashService = $hashService; + } + + /** + * @return JsonType>> + * @throws \JsonException + */ + public function transform(FormRuntime $formRuntime = null): JsonType + { + if (null === $formRuntime) { + throw new \InvalidArgumentException('Expected a valid FormRuntime object, but gut none.', 1646044629); + } + + // Handle submitted form values + $requestParameters = []; + if (\is_array($formRuntime->getRequest()->getParsedBody())) { + $requestParameters = $formRuntime->getRequest()->getParsedBody(); + } + + // Handle uploaded files + $uploadedFiles = $formRuntime->getRequest()->getUploadedFiles(); + array_walk_recursive($uploadedFiles, function (&$value, string $elementIdentifier) use ($formRuntime): void { + $file = $formRuntime[$elementIdentifier]; + if ($file instanceof ExtbaseFileReference || $file instanceof CoreFileReference) { + $value = $this->transformUploadedFile($file); + } + }); + + ArrayUtility::mergeRecursiveWithOverrule($requestParameters, $uploadedFiles); + + return JsonType::fromArray($requestParameters); + } + + /** + * @param CoreFileReference|ExtbaseFileReference $file + * @return array{submittedFile: array{resourcePointer: string}} + */ + private function transformUploadedFile($file): array + { + if ($file instanceof ExtbaseFileReference) { + $file = $file->getOriginalResource(); + } + if ($file instanceof CoreFileReference) { + $file = $file->getOriginalFile(); + } + + return [ + 'submittedFile' => [ + 'resourcePointer' => $this->hashService->appendHmac('file:' . $file->getUid()), + ], + ]; + } + + public static function getName(): string + { + return 'formRequest'; + } +} diff --git a/Classes/Type/Transformer/FormValuesTypeTransformer.php b/Classes/Type/Transformer/FormValuesTypeTransformer.php new file mode 100644 index 00000000..e30fc9c7 --- /dev/null +++ b/Classes/Type/Transformer/FormValuesTypeTransformer.php @@ -0,0 +1,139 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Type\Transformer; + +use EliasHaeussler\Typo3FormConsent\Type\JsonType; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference; +use TYPO3\CMS\Extbase\Domain\Model\FileReference as ExtbaseFileReference; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; +use TYPO3\CMS\Form\Exception; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; + +/** + * FormValuesTypeTransformer + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class FormValuesTypeTransformer implements TypeTransformerInterface +{ + /** + * @var Context + */ + private $context; + + public function __construct(Context $context) + { + $this->context = $context; + } + + /** + * @return JsonType + * @throws \JsonException + */ + public function transform(FormRuntime $formRuntime = null): JsonType + { + if (null === $formRuntime) { + throw new \InvalidArgumentException('Expected a valid FormRuntime object, but gut none.', 1646044591); + } + + // Early return if form state is not available + $formState = $formRuntime->getFormState(); + if (null === $formState) { + return JsonType::fromArray([]); + } + + // Get all form values + $formValues = $formState->getFormValues(); + + // Remove honeypot field + $honeypotIdentifier = $this->getHoneypotIdentifier($formRuntime); + unset($formValues[$honeypotIdentifier]); + + foreach ($formValues as $key => $value) { + if (\is_object($value)) { + if ($value instanceof ExtbaseFileReference) { + $value = $value->getOriginalResource(); + } + if ($value instanceof CoreFileReference) { + $formValues[$key] = $value->getOriginalFile()->getUid(); + } + } + } + + return JsonType::fromArray($formValues); + } + + private function getHoneypotIdentifier(FormRuntime $formRuntime): ?string + { + // @todo This highly depends on internal logic in FormRuntime + // which is likely to be changed in the future. Consider + // refactoring this to a more robust solution or drop it + // completely to avoid inconsistencies in future versions. + + $formState = $formRuntime->getFormState(); + + // Early return if form state is not available (this should never happen) + if (null === $formState) { + return null; + } + + // Get last displayed page + $lastDisplayedPageIndex = $formState->getLastDisplayedPageIndex(); + try { + $currentPage = $formRuntime->getFormDefinition()->getPageByIndex($lastDisplayedPageIndex); + } catch (Exception $e) { + // If last displayed page is not set, try to use current page instead + $currentPage = $formRuntime->getCurrentPage(); + } + + // Early return if neither last displayed page nor current page are available + if ($currentPage === null) { + return null; + } + + // Build honeypot session identifier + $frontendUser = $this->getTypoScriptFrontendController()->fe_user; + $isUserAuthenticated = (bool)$this->context->getPropertyFromAspect('frontend.user', 'isLoggedIn', false); + $sessionType = $isUserAuthenticated ? 'user' : 'ses'; + $honeypotSessionIdentifier = implode('', [ + FormRuntime::HONEYPOT_NAME_SESSION_IDENTIFIER, + $formRuntime->getIdentifier(), + $currentPage->getIdentifier(), + ]); + + return (string)$frontendUser->getKey($sessionType, $honeypotSessionIdentifier) ?: null; + } + + public static function getName(): string + { + return 'formValues'; + } + + private function getTypoScriptFrontendController(): TypoScriptFrontendController + { + return $GLOBALS['TSFE']; + } +} diff --git a/Classes/Type/Transformer/TypeTransformerFactory.php b/Classes/Type/Transformer/TypeTransformerFactory.php new file mode 100644 index 00000000..340bbd01 --- /dev/null +++ b/Classes/Type/Transformer/TypeTransformerFactory.php @@ -0,0 +1,64 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Type\Transformer; + +use EliasHaeussler\Typo3FormConsent\Exception\UnsupportedTypeException; +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * TypeTransformerFactory + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class TypeTransformerFactory +{ + /** + * @var ServiceLocator + */ + private $typeTransformers; + + public function __construct(ServiceLocator $transformers) + { + $this->typeTransformers = $transformers; + } + + /** + * @throws UnsupportedTypeException + */ + public function get(string $type): TypeTransformerInterface + { + if (!$this->typeTransformers->has($type)) { + throw UnsupportedTypeException::create($type); + } + + $transformer = $this->typeTransformers->get($type); + + if (!($transformer instanceof TypeTransformerInterface)) { + throw UnsupportedTypeException::create($type); + } + + return $transformer; + } +} diff --git a/Classes/Type/Transformer/TypeTransformerInterface.php b/Classes/Type/Transformer/TypeTransformerInterface.php new file mode 100644 index 00000000..ede47ee3 --- /dev/null +++ b/Classes/Type/Transformer/TypeTransformerInterface.php @@ -0,0 +1,40 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Type\Transformer; + +use EliasHaeussler\Typo3FormConsent\Type\JsonType; + +/** + * TypeTransformerInterface + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +interface TypeTransformerInterface +{ + /* @phpstan-ignore-next-line */ + public function transform(): JsonType; + + public static function getName(): string; +} diff --git a/Configuration/ExpressionLanguage.php b/Configuration/ExpressionLanguage.php new file mode 100644 index 00000000..c9a0d786 --- /dev/null +++ b/Configuration/ExpressionLanguage.php @@ -0,0 +1,30 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +use EliasHaeussler\Typo3FormConsent\ExpressionLanguage\ConsentConditionProvider; + +return [ + 'form' => [ + ConsentConditionProvider::class, + ], +]; diff --git a/Configuration/Services.php b/Configuration/Services.php index eb1707c9..28b9e769 100644 --- a/Configuration/Services.php +++ b/Configuration/Services.php @@ -24,10 +24,14 @@ namespace EliasHaeussler\Typo3FormConsent; use EliasHaeussler\Typo3FormConsent\DependencyInjection\DashboardServicesConfigurator; +use EliasHaeussler\Typo3FormConsent\Type\Transformer\TypeTransformerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator, ContainerBuilder $container) { + $container->registerForAutoconfiguration(TypeTransformerInterface::class) + ->addTag('form_consent.type_transformer'); + if ($container->hasDefinition('dashboard.views.widget')) { $servicesConfigurator = new DashboardServicesConfigurator($containerConfigurator->services()); $servicesConfigurator->configureServices(); diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 8fa50e7f..8d468b0a 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -8,8 +8,19 @@ services: resource: '../Classes/*' exclude: - '../Classes/DependencyInjection/*' + - '../Classes/Domain/Finishers/FinisherOptions.php' - '../Classes/Domain/Model/*' EliasHaeussler\Typo3FormConsent\Domain\Finishers\ConsentFinisher: public: true shared: false + + EliasHaeussler\Typo3FormConsent\Type\Transformer\TypeTransformerFactory: + arguments: + $transformers: !tagged_locator { tag: 'form_consent.type_transformer', default_index_method: 'getName' } + + EliasHaeussler\Typo3FormConsent\Event\Listener\InvokeFinishersOnConsentApprovalListener: + tags: + - name: event.listener + identifier: 'formConsentInvokeFinishersOnApproveListener' + event: EliasHaeussler\Typo3FormConsent\Event\ApproveConsentEvent diff --git a/Configuration/TCA/tx_formconsent_domain_model_consent.php b/Configuration/TCA/tx_formconsent_domain_model_consent.php index d7f0d994..96ba807e 100644 --- a/Configuration/TCA/tx_formconsent_domain_model_consent.php +++ b/Configuration/TCA/tx_formconsent_domain_model_consent.php @@ -75,6 +75,27 @@ 'readOnly' => true, ], ], + 'original_request_parameters' => [ + 'exclude' => true, + 'label' => \EliasHaeussler\Typo3FormConsent\Configuration\Localization::forField('original_request_parameters', $tableName), + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'original_content_element_uid' => [ + 'exclude' => true, + 'label' => \EliasHaeussler\Typo3FormConsent\Configuration\Localization::forField('original_content_element_uid', $tableName), + 'config' => [ + 'type' => 'group', + 'internal_type' => 'db', + 'allowed' => 'tt_content', + 'foreign_table' => 'tt_content', + 'size' => 1, + 'minitems' => 1, + 'maxitems' => 1, + 'readOnly' => true, + ], + ], 'approved' => [ 'exclude' => true, 'label' => \EliasHaeussler\Typo3FormConsent\Configuration\Localization::forField('approved', $tableName), @@ -127,6 +148,7 @@ date, data, form_persistence_identifier, + original_content_element_uid, --div--;' . \EliasHaeussler\Typo3FormConsent\Configuration\Localization::forTab('consent') . ', approved, approval_date, diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index fd1db479..5b8e617c 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -32,6 +32,12 @@ Form location + + Original request parameters + + + Content element + Approved diff --git a/Resources/Private/Language/locallang_form.xlf b/Resources/Private/Language/locallang_form.xlf index 4e2e1734..d6fcf8cb 100644 --- a/Resources/Private/Language/locallang_form.xlf +++ b/Resources/Private/Language/locallang_form.xlf @@ -94,8 +94,8 @@ The finisher option "senderAddress" must contain a valid e-mail address. - - The finisher option "validationPeriod" must be zero or more seconds. + + The finisher option "approvalPeriod" must be zero or more seconds. The finisher option "confirmationPid" must be set. @@ -109,6 +109,13 @@ The finisher option "storagePid" must be set to a valid page. + + + Execute after consent approval + + + If enabled, the finisher will not be executed until the form submitter has given his consent. Will be ignored if no Consent finisher is added to the form. + diff --git a/Tests/Functional/Configuration/IconTest.php b/Tests/Functional/Configuration/IconTest.php index 3249adb3..938d6d9b 100644 --- a/Tests/Functional/Configuration/IconTest.php +++ b/Tests/Functional/Configuration/IconTest.php @@ -26,7 +26,6 @@ use EliasHaeussler\Typo3FormConsent\Configuration\Icon; use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider; use TYPO3\CMS\Core\Imaging\IconRegistry; -use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; /** @@ -37,6 +36,17 @@ */ class IconTest extends FunctionalTestCase { + /** + * @var IconRegistry + */ + protected $iconRegistry; + + protected function setUp(): void + { + parent::setUp(); + $this->iconRegistry = $this->getContainer()->get(IconRegistry::class); + } + /** * @test */ @@ -44,7 +54,7 @@ public function registerForPluginIdentifierRegistersIconCorrectly(): void { Icon::registerForPluginIdentifier('Consent'); - $actual = GeneralUtility::makeInstance(IconRegistry::class)->getIconConfigurationByIdentifier('content-plugin-consent'); + $actual = $this->iconRegistry->getIconConfigurationByIdentifier('content-plugin-consent'); $expected = [ 'provider' => SvgIconProvider::class, 'options' => [ @@ -62,7 +72,7 @@ public function registerForWidgetIdentifierRegistersIconCorrectly(): void { Icon::registerForWidgetIdentifier('approvedConsents'); - $actual = GeneralUtility::makeInstance(IconRegistry::class)->getIconConfigurationByIdentifier('content-widget-approved-consents'); + $actual = $this->iconRegistry->getIconConfigurationByIdentifier('content-widget-approved-consents'); $expected = [ 'provider' => SvgIconProvider::class, 'options' => [ diff --git a/Tests/Functional/Configuration/LocalizationTest.php b/Tests/Functional/Configuration/LocalizationTest.php index 9b04bddd..0abe1ec9 100644 --- a/Tests/Functional/Configuration/LocalizationTest.php +++ b/Tests/Functional/Configuration/LocalizationTest.php @@ -81,6 +81,17 @@ public function forPluginReturnsLocalizationKeyForGivenPlugin(): void self::assertSame($expected, Localization::forPlugin('FooBaz')); } + /** + * @test + */ + public function forFinisherOptionReturnsLocalizationKeyForGivenFinisherOption(): void + { + $expected = 'LLL:EXT:form_consent/Resources/Private/Language/locallang_form.xlf:finishers.foo.label'; + self::assertSame($expected, Localization::forFinisherOption('foo')); + $expected = 'LLL:EXT:form_consent/Resources/Private/Language/locallang_form.xlf:finishers.foo.fieldExplanationText'; + self::assertSame($expected, Localization::forFinisherOption('foo', 'fieldExplanationText')); + } + /** * @test */ diff --git a/Tests/Functional/Domain/Finishers/FinisherOptionsTest.php b/Tests/Functional/Domain/Finishers/FinisherOptionsTest.php new file mode 100644 index 00000000..4a4e3c71 --- /dev/null +++ b/Tests/Functional/Domain/Finishers/FinisherOptionsTest.php @@ -0,0 +1,449 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Tests\Functional\Domain\Finishers; + +use EliasHaeussler\Typo3FormConsent\Domain\Finishers\FinisherOptions; +use EliasHaeussler\Typo3FormConsent\Exception\NotAllowedException; +use TYPO3\CMS\Core\Core\Bootstrap; +use TYPO3\CMS\Fluid\View\TemplatePaths; +use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +/** + * FinisherOptionsTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +class FinisherOptionsTest extends FunctionalTestCase +{ + protected $coreExtensionsToLoad = [ + 'form', + ]; + + protected $testExtensionsToLoad = [ + 'typo3conf/ext/form_consent', + ]; + + /** + * @var FinisherOptions + */ + protected $subject; + + /** + * @var array + */ + protected $options = []; + + protected function setUp(): void + { + parent::setUp(); + + $this->subject = new FinisherOptions([$this, 'fetchOption']); + + $this->importDataSet(\dirname(__DIR__, 2) . '/Fixtures/pages.xml'); + + Bootstrap::initializeLanguageObject(); + } + + /** + * @test + */ + public function getSubjectReturnsAlreadyParsedSubject(): void + { + $this->options['subject'] = 'foo'; + + self::assertSame('foo', $this->subject->getSubject()); + + $this->options = []; + + self::assertSame('foo', $this->subject->getSubject()); + } + + /** + * @test + */ + public function getSubjectReturnsLocalizedTranslatableSubject(): void + { + $this->options['subject'] = 'LLL:EXT:form_consent/Resources/Private/Language/locallang.xlf:consentMail.subject'; + + self::assertSame('Approve your consent', $this->subject->getSubject()); + } + + /** + * @test + */ + public function getSubjectReturnsDefaultSubjectIfFetchedSubjectIsEmpty(): void + { + $this->options['subject'] = ''; + + self::assertSame('Approve your consent', $this->subject->getSubject()); + } + + /** + * @test + */ + public function getTemplatePathsReturnsTemplatePathsAndStoresTheParsedResult(): void + { + $this->options = [ + 'templateRootPaths' => [ + 0 => 'foo', + 10 => 'baz', + ], + 'partialRootPaths' => [ + 0 => 'foo', + 10 => 'baz', + ], + 'layoutRootPaths' => [ + 0 => 'foo', + 10 => 'baz', + ], + ]; + + $templatePathsArray = $this->options; + $templatePathsArray['format'] = 'both'; + + $expected = new TemplatePaths($templatePathsArray); + + self::assertEquals($expected, $this->subject->getTemplatePaths()); + + $this->options = []; + + self::assertEquals($expected, $this->subject->getTemplatePaths()); + } + + /** + * @test + */ + public function getRecipientAddressReturnsAlreadyParsedRecipientAddress(): void + { + $this->options['recipientAddress'] = 'foo@baz.de'; + + self::assertSame('foo@baz.de', $this->subject->getRecipientAddress()); + + $this->options = []; + + self::assertSame('foo@baz.de', $this->subject->getRecipientAddress()); + } + + /** + * @test + */ + public function getRecipientAddressThrowsExceptionIfFetchedRecipientAddressIsNotAString(): void + { + $this->options['recipientAddress'] = null; + + $this->expectException(FinisherException::class); + $this->expectExceptionCode(1640186663); + $this->expectExceptionMessage('The finisher option "recipientAddress" must contain a valid e-mail address.'); + + $this->subject->getRecipientAddress(); + } + + /** + * @test + */ + public function getRecipientAddressThrowsExceptionIfFetchedRecipientAddressIsEmpty(): void + { + $this->options['recipientAddress'] = ''; + + $this->expectException(FinisherException::class); + $this->expectExceptionCode(1576947638); + $this->expectExceptionMessage('The finisher option "recipientAddress" must be set.'); + + $this->subject->getRecipientAddress(); + } + + /** + * @test + */ + public function getRecipientAddressThrowsExceptionIfFetchedRecipientAddressIsInvalid(): void + { + $this->options['recipientAddress'] = 'foo'; + + $this->expectException(FinisherException::class); + $this->expectExceptionCode(1576947682); + $this->expectExceptionMessage('The finisher option "recipientAddress" must contain a valid e-mail address.'); + + $this->subject->getRecipientAddress(); + } + + /** + * @test + */ + public function getRecipientNameReturnsRecipientNameAndStoresTheParsedResult(): void + { + $this->options['recipientName'] = 'foo'; + + self::assertSame('foo', $this->subject->getRecipientName()); + + $this->options = []; + + self::assertSame('foo', $this->subject->getRecipientName()); + } + + /** + * @test + */ + public function getSenderAddressReturnsAlreadyParsedSenderAddress(): void + { + $this->options['senderAddress'] = 'foo@baz.de'; + + self::assertSame('foo@baz.de', $this->subject->getSenderAddress()); + + $this->options = []; + + self::assertSame('foo@baz.de', $this->subject->getSenderAddress()); + } + + /** + * @test + */ + public function getSenderAddressThrowsExceptionIfFetchedSenderAddressIsNotAString(): void + { + $this->options['senderAddress'] = null; + + $this->expectException(FinisherException::class); + $this->expectExceptionCode(1640186811); + $this->expectExceptionMessage('The finisher option "senderAddress" must contain a valid e-mail address.'); + + $this->subject->getSenderAddress(); + } + + /** + * @test + */ + public function getSenderAddressThrowsExceptionIfFetchedSenderAddressIsNotEmptyAndInvalid(): void + { + $this->options['senderAddress'] = 'foo'; + + $this->expectException(FinisherException::class); + $this->expectExceptionCode(1587842752); + $this->expectExceptionMessage('The finisher option "senderAddress" must contain a valid e-mail address.'); + + $this->subject->getSenderAddress(); + } + + /** + * @test + */ + public function getSenderNameReturnsSenderNameAndStoresTheParsedResult(): void + { + $this->options['senderName'] = 'foo'; + + self::assertSame('foo', $this->subject->getSenderName()); + + $this->options = []; + + self::assertSame('foo', $this->subject->getSenderName()); + } + + /** + * @test + */ + public function getApprovalPeriodReturnsAlreadyParsedApprovalPeriod(): void + { + $this->options['approvalPeriod'] = 86400; + + self::assertSame(86400, $this->subject->getApprovalPeriod()); + + $this->options = []; + + self::assertSame(86400, $this->subject->getApprovalPeriod()); + } + + /** + * @test + */ + public function getApprovalPeriodThrowsExceptionIfFetchedApprovalPeriodIsInvalid(): void + { + $this->options['approvalPeriod'] = -1; + + $this->expectException(FinisherException::class); + $this->expectExceptionCode(1576948900); + $this->expectExceptionMessage('The finisher option "approvalPeriod" must be zero or more seconds.'); + + $this->subject->getApprovalPeriod(); + } + + /** + * @test + */ + public function getConfirmationPidThrowsExceptionIfFetchedConfirmationPidIsLowerThanZero(): void + { + $this->options['confirmationPid'] = -1; + + $this->expectException(FinisherException::class); + $this->expectExceptionCode(1576948961); + $this->expectExceptionMessage('The finisher option "confirmationPid" must be set.'); + + $this->subject->getConfirmationPid(); + } + + /** + * @test + */ + public function getConfirmationPidThrowsExceptionIfFetchedConfirmationPidIsInvalid(): void + { + $this->options['confirmationPid'] = 123; + + $this->expectException(FinisherException::class); + $this->expectExceptionCode(1576949163); + $this->expectExceptionMessage('The finisher option "confirmationPid" must be set to a valid page.'); + + $this->subject->getConfirmationPid(); + } + + /** + * @test + */ + public function getConfirmationPidReturnsConfirmationPidAndStoresTheParsedResult(): void + { + $this->options['confirmationPid'] = 1; + + self::assertSame(1, $this->subject->getConfirmationPid()); + + $this->options['confirmationPid'] = 123; + + self::assertSame(1, $this->subject->getConfirmationPid()); + } + + /** + * @test + */ + public function getStoragePidReturnsZeroIfFetchedStoragePidIsZero(): void + { + $this->options['storagePid'] = 0; + + self::assertSame(0, $this->subject->getStoragePid()); + } + + /** + * @test + */ + public function getStoragePidThrowsExceptionIfFetchedStoragePidIsLowerThanZero(): void + { + $this->options['storagePid'] = -1; + + $this->expectException(FinisherException::class); + $this->expectExceptionCode(1576951495); + $this->expectExceptionMessage('The finisher option "storagePid" must be set.'); + + $this->subject->getStoragePid(); + } + + /** + * @test + */ + public function getStoragePidThrowsExceptionIfFetchedStoragePidIsInvalid(): void + { + $this->options['storagePid'] = 123; + + $this->expectException(FinisherException::class); + $this->expectExceptionCode(1576951499); + $this->expectExceptionMessage('The finisher option "storagePid" must be set to a valid page.'); + + $this->subject->getStoragePid(); + } + + /** + * @test + */ + public function getStoragePidReturnsStoragePidAndStoresTheParsedResult(): void + { + $this->options['storagePid'] = 1; + + self::assertSame(1, $this->subject->getStoragePid()); + + $this->options['storagePid'] = 123; + + self::assertSame(1, $this->subject->getStoragePid()); + } + + /** + * @test + */ + public function getShowDismissLinkReturnsShowDismissLinkAndStoresTheParsedResult(): void + { + $this->options['showDismissLink'] = false; + + self::assertFalse($this->subject->getShowDismissLink()); + + $this->options['showDismissLink'] = true; + + self::assertFalse($this->subject->getShowDismissLink()); + } + + /** + * @test + */ + public function objectCanBeAccessedAsArrayInReadMode(): void + { + // offsetExists() + self::assertTrue(isset($this->subject['approvalPeriod'])); + self::assertFalse(isset($this->subject['foo'])); + self::assertFalse(isset($this->subject[null])); + + // offsetGet() + self::assertSame(0, $this->subject['approvalPeriod']); + self::assertNull($this->subject['foo']); + self::assertNull($this->subject[null]); + } + + /** + * @test + */ + public function objectCannotBeAccessedAsArrayInWriteModeViaOffsetSet(): void + { + $this->expectException(NotAllowedException::class); + $this->expectExceptionCode(1645781267); + $this->expectExceptionMessage( + sprintf('Calling the method "%s" is not allowed.', FinisherOptions::class . '::offsetSet') + ); + + $this->subject['approvalPeriod'] = 0; + } + + /** + * @test + */ + public function objectCannotBeAccessedAsArrayInWriteModeViaOffsetUnset(): void + { + $this->expectException(NotAllowedException::class); + $this->expectExceptionCode(1645781267); + $this->expectExceptionMessage( + sprintf('Calling the method "%s" is not allowed.', FinisherOptions::class . '::offsetUnset') + ); + + unset($this->subject['approvalPeriod']); + } + + /** + * @return mixed|null + */ + public function fetchOption(string $optionName) + { + return $this->options[$optionName] ?? null; + } +} diff --git a/Tests/Functional/Domain/Repository/ConsentRepositoryTest.php b/Tests/Functional/Domain/Repository/ConsentRepositoryTest.php index cf56cc1e..cf6931d8 100644 --- a/Tests/Functional/Domain/Repository/ConsentRepositoryTest.php +++ b/Tests/Functional/Domain/Repository/ConsentRepositoryTest.php @@ -23,13 +23,8 @@ namespace EliasHaeussler\Typo3FormConsent\Tests\Functional\Domain\Repository; -use Doctrine\DBAL\DBALException; use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; use EliasHaeussler\Typo3FormConsent\Domain\Repository\ConsentRepository; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Object\ObjectManager; -use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; -use TYPO3\TestingFramework\Core\Exception; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; /** @@ -53,18 +48,12 @@ class ConsentRepositoryTest extends FunctionalTestCase */ protected $subject; - /** - * @throws DBALException - * @throws Exception - */ protected function setUp(): void { parent::setUp(); // Build subject - $this->subject = new ConsentRepository(GeneralUtility::makeInstance(ObjectManager::class)); - $this->subject->injectPersistenceManager(GeneralUtility::makeInstance(PersistenceManager::class)); - $this->subject->initializeObject(); + $this->subject = $this->getContainer()->get(ConsentRepository::class); // Import data $this->importDataSet(__DIR__ . '/../../Fixtures/tx_formconsent_domain_model_consent.xml'); diff --git a/Tests/Functional/Fixtures/pages.xml b/Tests/Functional/Fixtures/pages.xml new file mode 100644 index 00000000..6b8b70f6 --- /dev/null +++ b/Tests/Functional/Fixtures/pages.xml @@ -0,0 +1,10 @@ + + + + 1 + 0 + Root + 1 + / + + diff --git a/Tests/Unit/Service/HashServiceTest.php b/Tests/Functional/Service/HashServiceTest.php similarity index 67% rename from Tests/Unit/Service/HashServiceTest.php rename to Tests/Functional/Service/HashServiceTest.php index 38423840..8d0e533c 100644 --- a/Tests/Unit/Service/HashServiceTest.php +++ b/Tests/Functional/Service/HashServiceTest.php @@ -21,16 +21,17 @@ * along with this program. If not, see . */ -namespace EliasHaeussler\Typo3FormConsent\Tests\Unit\Service; +namespace EliasHaeussler\Typo3FormConsent\Tests\Functional\Service; use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; use EliasHaeussler\Typo3FormConsent\Event\GenerateHashEvent; use EliasHaeussler\Typo3FormConsent\Service\HashService; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; +use EliasHaeussler\Typo3FormConsent\Type\JsonType; +use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; -use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Symfony\Component\DependencyInjection\Container; +use TYPO3\CMS\Core\EventDispatcher\ListenerProvider; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; /** * HashServiceTest @@ -38,19 +39,22 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class HashServiceTest extends UnitTestCase +class HashServiceTest extends FunctionalTestCase { - use ProphecyTrait; - /** * @var Consent */ protected $consent; /** - * @var ObjectProphecy|EventDispatcherInterface + * @var ListenerProvider + */ + protected $listenerProvider; + + /** + * @var ContainerInterface */ - protected $eventDispatcherProphecy; + protected $container; /** * @var HashService @@ -64,12 +68,12 @@ protected function setUp(): void $this->consent = (new Consent()) ->setEmail('dummy@example.com') ->setDate(new \DateTime()) - ->setData(['foo' => 'baz']) + ->setData(JsonType::fromArray(['foo' => 'baz'])) ->setValidUntil(new \DateTime()); - $this->eventDispatcherProphecy = $this->prophesize(EventDispatcherInterface::class); - $this->eventDispatcherProphecy->dispatch(Argument::any())->willReturnArgument(0); - $this->subject = new HashService($this->eventDispatcherProphecy->reveal()); + $this->container = $this->getContainer(); + $this->listenerProvider = $this->container->get(ListenerProvider::class); + $this->subject = new HashService($this->container->get(EventDispatcherInterface::class)); } /** @@ -94,12 +98,17 @@ public function generateRespectsComponentsModifiedThroughEvent(): void { $hashWithDefaultComponents = $this->subject->generate($this->consent); - $this->eventDispatcherProphecy->dispatch(Argument::type(GenerateHashEvent::class))->will(function ($args) { - /** @var GenerateHashEvent $event */ - $event = $args[0]; - $event->setComponents([]); - return $event; - }); + $this->addEventListener( + GenerateHashEvent::class, + __METHOD__, + new class() { + public function __invoke(GenerateHashEvent $event): void + { + $event->setComponents([]); + } + } + ); + $hashWithNoComponents = $this->subject->generate($this->consent); self::assertNotSame($hashWithNoComponents, $hashWithDefaultComponents); @@ -112,12 +121,17 @@ public function generateReturnsCustomHashGeneratedThroughEvent(): void { $defaultHashGeneration = $this->subject->generate($this->consent); - $this->eventDispatcherProphecy->dispatch(Argument::type(GenerateHashEvent::class))->will(function ($args) { - /** @var GenerateHashEvent $event */ - $event = $args[0]; - $event->setHash('foo'); - return $event; - }); + $this->addEventListener( + GenerateHashEvent::class, + __METHOD__, + new class() { + public function __invoke(GenerateHashEvent $event): void + { + $event->setHash('foo'); + } + } + ); + $customHashGeneration = $this->subject->generate($this->consent); self::assertNotSame($customHashGeneration, $defaultHashGeneration); @@ -161,14 +175,28 @@ public function isValidReturnsCorrectStateForGivenHashAndConsent(): void */ public function isValidRespectsInitialHashModificationThroughEvent(): void { - $this->eventDispatcherProphecy->dispatch(Argument::type(GenerateHashEvent::class))->will(function ($args) { - /** @var GenerateHashEvent $event */ - $event = $args[0]; - $event->setComponents([]); - return $event; - }); + $this->addEventListener( + GenerateHashEvent::class, + __METHOD__, + new class() { + public function __invoke(GenerateHashEvent $event): void + { + $event->setComponents([]); + } + } + ); + $hash = $this->subject->generate($this->consent); $this->consent->setValidationHash($hash); + self::assertTrue($this->subject->isValid($this->consent)); } + + private function addEventListener(string $event, string $service, object $object): void + { + self::assertInstanceOf(Container::class, $this->container); + + $this->container->set($service, $object); + $this->listenerProvider->addListener($event, $service); + } } diff --git a/Tests/Functional/Type/Transformer/FormRequestTypeTransformerTest.php b/Tests/Functional/Type/Transformer/FormRequestTypeTransformerTest.php new file mode 100644 index 00000000..946d0bec --- /dev/null +++ b/Tests/Functional/Type/Transformer/FormRequestTypeTransformerTest.php @@ -0,0 +1,72 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Tests\Functional\Type\Transformer; + +use EliasHaeussler\Typo3FormConsent\Type\Transformer\FormRequestTypeTransformer; +use TYPO3\CMS\Extbase\Security\Cryptography\HashService; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +/** + * FormRequestTypeTransformerTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +class FormRequestTypeTransformerTest extends FunctionalTestCase +{ + protected $coreExtensionsToLoad = [ + 'form', + ]; + + /** + * @var FormRequestTypeTransformer + */ + protected $subject; + + /** + * @var FormRuntime + */ + protected $formRuntime; + + protected function setUp(): void + { + parent::setUp(); + + $this->subject = new FormRequestTypeTransformer($this->getContainer()->get(HashService::class)); + $this->formRuntime = $this->getContainer()->get(FormRuntime::class); + } + + /** + * @test + */ + public function transformThrowsExceptionIfFormRuntimeIsNotGiven(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(1646044629); + $this->expectExceptionMessage('Expected a valid FormRuntime object, but gut none.'); + + $this->subject->transform(); + } +} diff --git a/Tests/Functional/Type/Transformer/FormValuesTypeTransformerTest.php b/Tests/Functional/Type/Transformer/FormValuesTypeTransformerTest.php new file mode 100644 index 00000000..3f12fde2 --- /dev/null +++ b/Tests/Functional/Type/Transformer/FormValuesTypeTransformerTest.php @@ -0,0 +1,81 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Tests\Functional\Type\Transformer; + +use EliasHaeussler\Typo3FormConsent\Type\JsonType; +use EliasHaeussler\Typo3FormConsent\Type\Transformer\FormValuesTypeTransformer; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +/** + * FormValuesTypeTransformerTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +class FormValuesTypeTransformerTest extends FunctionalTestCase +{ + protected $coreExtensionsToLoad = [ + 'form', + ]; + + /** + * @var FormValuesTypeTransformer + */ + protected $subject; + + /** + * @var FormRuntime + */ + protected $formRuntime; + + protected function setUp(): void + { + parent::setUp(); + + $this->subject = new FormValuesTypeTransformer(new Context()); + $this->formRuntime = $this->getContainer()->get(FormRuntime::class); + } + + /** + * @test + */ + public function transformThrowsExceptionIfFormRuntimeIsNotGiven(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(1646044591); + $this->expectExceptionMessage('Expected a valid FormRuntime object, but gut none.'); + + $this->subject->transform(); + } + + /** + * @test + */ + public function transformReturnsJsonTypeWithEmptyArrayIfFormIsUninitialized(): void + { + self::assertEquals(JsonType::fromArray([]), $this->subject->transform($this->formRuntime)); + } +} diff --git a/Tests/Functional/Widget/Provider/ConsentChartDataProviderTest.php b/Tests/Functional/Widget/Provider/ConsentChartDataProviderTest.php index ba91bac8..a3ccfebb 100644 --- a/Tests/Functional/Widget/Provider/ConsentChartDataProviderTest.php +++ b/Tests/Functional/Widget/Provider/ConsentChartDataProviderTest.php @@ -26,8 +26,6 @@ use Doctrine\DBAL\DBALException; use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; use EliasHaeussler\Typo3FormConsent\Widget\Provider\ConsentChartDataProvider; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\TestingFramework\Core\Exception; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; @@ -66,7 +64,7 @@ protected function setUp(): void parent::setUp(); // Build subject - $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(Consent::TABLE_NAME); + $connection = $this->getConnectionPool()->getConnectionForTable(Consent::TABLE_NAME); $this->subject = new ConsentChartDataProvider($connection); // Import data diff --git a/Tests/Unit/Domain/Model/ConsentTest.php b/Tests/Unit/Domain/Model/ConsentTest.php index feec5952..e5b8368f 100644 --- a/Tests/Unit/Domain/Model/ConsentTest.php +++ b/Tests/Unit/Domain/Model/ConsentTest.php @@ -24,6 +24,7 @@ namespace EliasHaeussler\Typo3FormConsent\Tests\Unit\Domain\Model; use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; +use EliasHaeussler\Typo3FormConsent\Type\JsonType; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; /** @@ -70,16 +71,47 @@ public function setDateStoresCreationDateCorrectly(): void */ public function setDataStoresUserDataCorrectly(): void { - $expectedJson = '{"foo":"baz"}'; - $expectedArray = json_decode($expectedJson, true); + $data = JsonType::fromArray(['foo' => 'baz']); + $this->subject->setData($data); + self::assertSame($data, $this->subject->getData()); + } + + /** + * @test + */ + public function getOriginalRequestParametersReturnsNullOnInitialObject(): void + { + self::assertNull($this->subject->getValidUntil()); + } + + /** + * @test + */ + public function setOriginalRequestParametersStoresOriginalRequestParametersCorrectly(): void + { + $originalRequestParameters = JsonType::fromArray(['foo' => 'baz']); - $this->subject->setData('{"foo":"baz"}'); - self::assertSame($expectedJson, $this->subject->getData()); - self::assertSame($expectedArray, $this->subject->getDataArray()); + $this->subject->setOriginalRequestParameters($originalRequestParameters); + + self::assertSame($originalRequestParameters, $this->subject->getOriginalRequestParameters()); + } + + /** + * @test + */ + public function getOriginalContentElementUidReturnsZeroOnInitialState(): void + { + self::assertSame(0, $this->subject->getOriginalContentElementUid()); + } + + /** + * @test + */ + public function setOriginalContentElementUidStoresOriginalContentElementUidCorrectly(): void + { + $this->subject->setOriginalContentElementUid(123); - $this->subject->setData(['foo' => 'baz']); - self::assertSame($expectedJson, $this->subject->getData()); - self::assertSame($expectedArray, $this->subject->getDataArray()); + self::assertSame(123, $this->subject->getOriginalContentElementUid()); } /** diff --git a/Tests/Unit/Event/ApproveConsentEventTest.php b/Tests/Unit/Event/ApproveConsentEventTest.php index 1811975d..8f16eecd 100644 --- a/Tests/Unit/Event/ApproveConsentEventTest.php +++ b/Tests/Unit/Event/ApproveConsentEventTest.php @@ -25,6 +25,7 @@ use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; use EliasHaeussler\Typo3FormConsent\Event\ApproveConsentEvent; +use TYPO3\CMS\Core\Http\Response; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; /** @@ -61,4 +62,16 @@ public function getConsentReturnsInitialConsent(): void $expected = $this->consent; self::assertSame($expected, $this->subject->getConsent()); } + + /** + * @test + */ + public function getResponseReturnsResponse(): void + { + self::assertNull($this->subject->getResponse()); + + $response = new Response(); + + self::assertSame($response, $this->subject->setResponse($response)->getResponse()); + } } diff --git a/Tests/Unit/Registry/ConsentManagerRegistryTest.php b/Tests/Unit/Registry/ConsentManagerRegistryTest.php new file mode 100644 index 00000000..9c0c1faf --- /dev/null +++ b/Tests/Unit/Registry/ConsentManagerRegistryTest.php @@ -0,0 +1,118 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Tests\Unit\Registry; + +use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; +use EliasHaeussler\Typo3FormConsent\Registry\ConsentManagerRegistry; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * ConsentManagerRegistryTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +class ConsentManagerRegistryTest extends UnitTestCase +{ + /** + * @var Consent + */ + protected $consent; + + protected function setUp(): void + { + parent::setUp(); + + $this->consent = new Consent(); + $this->consent->setFormPersistenceIdentifier('foo'); + $this->consent->setApproved(true); + } + + /** + * @test + */ + public function registerConsentGloballyRegistersConsent(): void + { + self::assertFalse(ConsentManagerRegistry::isConsentApproved('foo')); + + ConsentManagerRegistry::registerConsent($this->consent); + + self::assertTrue(ConsentManagerRegistry::isConsentApproved('foo')); + } + + /** + * @test + */ + public function unregisterConsentGloballyUnregistersConsent(): void + { + ConsentManagerRegistry::registerConsent($this->consent); + + self::assertTrue(ConsentManagerRegistry::isConsentApproved('foo')); + + ConsentManagerRegistry::unregisterConsent($this->consent); + + self::assertFalse(ConsentManagerRegistry::isConsentApproved('foo')); + } + + /** + * @test + */ + public function isConsentApprovedReturnsTrueIfConsentIsApprovedForGivenForm(): void + { + self::assertFalse(ConsentManagerRegistry::isConsentApproved('foo')); + + ConsentManagerRegistry::registerConsent($this->consent); + + self::assertTrue(ConsentManagerRegistry::isConsentApproved('foo')); + + $this->consent->setApproved(false); + + self::assertFalse(ConsentManagerRegistry::isConsentApproved('foo')); + } + + /** + * @test + */ + public function isConsentDismissedReturnsTrueIfConsentIsDismissedForGivenForm(): void + { + $this->consent->setApproved(false); + + self::assertFalse(ConsentManagerRegistry::isConsentDismissed('foo')); + + ConsentManagerRegistry::registerConsent($this->consent); + + self::assertTrue(ConsentManagerRegistry::isConsentDismissed('foo')); + + $this->consent->setApproved(true); + + self::assertFalse(ConsentManagerRegistry::isConsentDismissed('foo')); + } + + protected function tearDown(): void + { + ConsentManagerRegistry::unregisterConsent($this->consent); + + parent::tearDown(); + } +} diff --git a/Tests/Unit/Registry/Dto/ConsentStateTest.php b/Tests/Unit/Registry/Dto/ConsentStateTest.php new file mode 100644 index 00000000..fc993ffb --- /dev/null +++ b/Tests/Unit/Registry/Dto/ConsentStateTest.php @@ -0,0 +1,87 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Tests\Unit\Registry\Dto; + +use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; +use EliasHaeussler\Typo3FormConsent\Registry\Dto\ConsentState; +use EliasHaeussler\Typo3FormConsent\Type\JsonType; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * ConsentStateTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +class ConsentStateTest extends UnitTestCase +{ + /** + * @var Consent + */ + protected $consent; + + /** + * @var ConsentState + */ + protected $subject; + + protected function setUp(): void + { + parent::setUp(); + + $this->consent = new Consent(); + $this->subject = new ConsentState($this->consent); + } + + /** + * @test + */ + public function isApprovedReturnsConsentApprovalState(): void + { + self::assertFalse($this->subject->isApproved()); + + $this->consent->setApproved(true); + + self::assertTrue($this->subject->isApproved()); + } + + /** + * @test + */ + public function isDismissedReturnsConsentDismissalState(): void + { + $this->consent->setApproved(true); + + self::assertFalse($this->subject->isDismissed()); + + $this->consent->setOriginalRequestParameters(JsonType::fromArray([])); + $this->consent->setApproved(false); + + self::assertFalse($this->subject->isDismissed()); + + $this->consent->setOriginalRequestParameters(null); + + self::assertTrue($this->subject->isDismissed()); + } +} diff --git a/Tests/Unit/Type/JsonTypeTest.php b/Tests/Unit/Type/JsonTypeTest.php new file mode 100644 index 00000000..6ebae166 --- /dev/null +++ b/Tests/Unit/Type/JsonTypeTest.php @@ -0,0 +1,101 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Tests\Unit\Type; + +use EliasHaeussler\Typo3FormConsent\Type\JsonType; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * JsonTypeTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +class JsonTypeTest extends UnitTestCase +{ + /** + * @var JsonType + */ + private JsonType $subject; + + protected function setUp(): void + { + parent::setUp(); + $this->subject = new JsonType('{"foo":"baz"}'); + } + + /** + * @test + */ + public function fromArrayReturnsObjectWithJsonEncodedData(): void + { + self::assertEquals($this->subject, JsonType::fromArray(['foo' => 'baz'])); + } + + /** + * @test + */ + public function objectIsJsonSerializable(): void + { + self::assertSame('{"foo":"baz"}', json_encode($this->subject, JSON_THROW_ON_ERROR)); + } + + /** + * @test + */ + public function stringRepresentationEqualsJsonRepresentation(): void + { + self::assertSame('{"foo":"baz"}', (string)$this->subject); + } + + /** + * @test + */ + public function toArrayReturnsArrayRepresentation(): void + { + self::assertSame(['foo' => 'baz'], $this->subject->toArray()); + } + + /** + * @test + */ + public function objectCanBeAccessedAsArray(): void + { + // offsetExists() + self::assertTrue(isset($this->subject['foo'])); + self::assertFalse(isset($this->subject['baz'])); + + // offsetGet() + self::assertSame('baz', $this->subject['foo']); + self::assertNull($this->subject['baz']); + + // offsetSet() + $this->subject['baz'] = 'foo'; + self::assertSame('foo', $this->subject['baz']); + + // offsetUnset() + unset($this->subject['baz']); + self::assertNull($this->subject['baz']); + } +} diff --git a/Tests/Unit/Type/Transformer/TypeTransformerFactoryTest.php b/Tests/Unit/Type/Transformer/TypeTransformerFactoryTest.php new file mode 100644 index 00000000..c43c4d89 --- /dev/null +++ b/Tests/Unit/Type/Transformer/TypeTransformerFactoryTest.php @@ -0,0 +1,97 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace EliasHaeussler\Typo3FormConsent\Tests\Unit\Type\Transformer; + +use EliasHaeussler\Typo3FormConsent\Exception\UnsupportedTypeException; +use EliasHaeussler\Typo3FormConsent\Type\Transformer\FormRequestTypeTransformer; +use EliasHaeussler\Typo3FormConsent\Type\Transformer\TypeTransformerFactory; +use Symfony\Component\DependencyInjection\ServiceLocator; +use TYPO3\CMS\Extbase\Security\Cryptography\HashService; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * TypeTransformerFactoryTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +class TypeTransformerFactoryTest extends UnitTestCase +{ + /** + * @var FormRequestTypeTransformer + */ + protected $formRequestTypeTransformer; + + /** + * @var TypeTransformerFactory + */ + protected $subject; + + protected function setUp(): void + { + parent::setUp(); + + $this->formRequestTypeTransformer = new FormRequestTypeTransformer(new HashService()); + $this->subject = new TypeTransformerFactory(new ServiceLocator([ + FormRequestTypeTransformer::getName() => function (): FormRequestTypeTransformer { + return $this->formRequestTypeTransformer; + }, + 'invalid' => function (): self { + return $this; + }, + ])); + } + + /** + * @test + */ + public function getThrowsExceptionIfRequestedTypeTransformerIsNotAvailable(): void + { + $this->expectException(UnsupportedTypeException::class); + $this->expectExceptionCode(1645774926); + $this->expectExceptionMessage('The type "foo" is not supported.'); + + $this->subject->get('foo'); + } + + /** + * @test + */ + public function getThrowsExceptionIfRequestedTypeTransformerIsInvalid(): void + { + $this->expectException(UnsupportedTypeException::class); + $this->expectExceptionCode(1645774926); + $this->expectExceptionMessage('The type "invalid" is not supported.'); + + $this->subject->get('invalid'); + } + + /** + * @test + */ + public function getReturnsRequestedTypeTransformer(): void + { + self::assertSame($this->formRequestTypeTransformer, $this->subject->get(FormRequestTypeTransformer::getName())); + } +} diff --git a/composer.json b/composer.json index ab7ded5d..6e8d6ec8 100644 --- a/composer.json +++ b/composer.json @@ -12,21 +12,24 @@ } ], "require": { - "php": ">= 7.2 < 8.2", + "php": ">= 7.4 < 8.2", "ext-json": "*", "doctrine/dbal": "^2.13 || ^3.0", "psr/event-dispatcher": "^1.0", + "psr/http-factory": "^1.0", "psr/http-message": "^1.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/dependency-injection": "^4.4 || ^5.0", + "symfony/expression-language": "^4.4 || ^5.0", "symfony/mailer": "^4.4 || ^5.0", "symfony/mime": "^4.4 || ^5.0", - "typo3/cms-backend": "~10.4.0 || ~11.5.0", - "typo3/cms-core": "~10.4.0 || ~11.5.0", - "typo3/cms-extbase": "~10.4.0 || ~11.5.0", - "typo3/cms-fluid": "~10.4.0 || ~11.5.0", - "typo3/cms-form": "~10.4.0 || ~11.5.0", - "typo3/cms-frontend": "~10.4.0 || ~11.5.0" + "symfony/polyfill-php80": "^1.24", + "typo3/cms-backend": "~11.5.0", + "typo3/cms-core": "~11.5.0", + "typo3/cms-extbase": "~11.5.0", + "typo3/cms-fluid": "~11.5.0", + "typo3/cms-form": "~11.5.0", + "typo3/cms-frontend": "~11.5.0" }, "require-dev": { "armin/editorconfig-cli": "^1.5", @@ -34,23 +37,23 @@ "ergebnis/composer-normalize": "^2.15", "helmich/typo3-typoscript-lint": "^2.5", "jangregor/phpstan-prophecy": "^1.0", + "nikic/php-parser": "^4.12", "phpspec/prophecy-phpunit": "^2.0", "phpstan/phpstan": "^1.2", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpcov": "^8.2", "phpunit/phpunit": "^9.3", "saschaegerer/phpstan-typo3": "^1.0", - "typo3/cms-dashboard": "~10.4.0 || ~11.5.0", - "typo3/cms-filelist": "~10.4.0 || ~11.5.0", - "typo3/cms-install": "~10.4.0 || ~11.5.0", - "typo3/cms-lowlevel": "~10.4.0 || ~11.5.0", - "typo3/cms-scheduler": "~10.4.0 || ~11.5.0", + "typo3/cms-dashboard": "~11.5.0", + "typo3/cms-filelist": "~11.5.0", + "typo3/cms-lowlevel": "~11.5.0", + "typo3/cms-scheduler": "~11.5.0", "typo3/coding-standards": "^0.5.0", "typo3/testing-framework": "^6.14" }, "suggest": { - "typo3/cms-dashboard": "Adds a custom form consent widget to the TYPO3 dashboard (~10.4.0 || ~11.5.0)", - "typo3/cms-scheduler": "Allows garbage collection of expired consents (~10.4.0 || ~11.5.0)" + "typo3/cms-dashboard": "Adds a custom form consent widget to the TYPO3 dashboard (~11.5.0)", + "typo3/cms-scheduler": "Allows garbage collection of expired consents (~11.5.0)" }, "autoload": { "psr-4": { diff --git a/ext_emconf.php b/ext_emconf.php index 0297eec9..fb8212bd 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -33,7 +33,7 @@ 'version' => '0.2.2', 'constraints' => [ 'depends' => [ - 'typo3' => '10.4.0-11.5.99', + 'typo3' => '11.5.0-11.5.99', ], ], ]; diff --git a/ext_tables.sql b/ext_tables.sql index 393627c5..aed6a79b 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -3,6 +3,8 @@ CREATE TABLE tx_formconsent_domain_model_consent( date int(11) DEFAULT '0' NOT NULL, data BLOB, form_persistence_identifier text, + original_request_parameters BLOB, + original_content_element_uid int(11) DEFAULT '0' NOT NULL, approved tinyint(4) DEFAULT '0' NOT NULL, approval_date int(11) DEFAULT '0' NOT NULL, valid_until int(11), From 68574ccacd57920dedc78274e026b54cefd33fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Mon, 28 Feb 2022 13:00:05 +0100 Subject: [PATCH 06/46] [TASK] Assure compatibility with TYPO3 v10 --- .ddev/config.yaml | 4 +-- .github/workflows/tests.yaml | 19 ++++++++--- Classes/Configuration/Icon.php | 5 +-- Classes/Controller/ConsentController.php | 29 +++++++--------- .../DashboardServicesConfigurator.php | 5 +-- Classes/Domain/Finishers/ConsentFinisher.php | 33 ++++++++++++------ Classes/Domain/Finishers/FinisherOptions.php | 5 +-- Classes/Event/ApproveConsentEvent.php | 11 ++---- Classes/Event/DismissConsentEvent.php | 5 +-- Classes/Event/GenerateHashEvent.php | 22 ++++-------- Classes/Event/ModifyConsentEvent.php | 5 +-- Classes/Event/ModifyConsentMailEvent.php | 5 +-- Classes/Http/StringableResponse.php | 6 +++- Classes/Http/StringableResponseFactory.php | 5 +-- Classes/Registry/ConsentManagerRegistry.php | 2 +- Classes/Registry/Dto/ConsentState.php | 5 +-- Classes/Service/HashService.php | 5 +-- .../FormRequestTypeTransformer.php | 5 +-- .../Transformer/FormValuesTypeTransformer.php | 5 +-- .../Transformer/TypeTransformerFactory.php | 5 +-- .../Provider/ConsentChartDataProvider.php | 5 +-- Configuration/Services.yaml | 6 ++++ Tests/Functional/Configuration/IconTest.php | 5 +-- .../Domain/Finishers/FinisherOptionsTest.php | 7 ++-- .../Repository/ConsentRepositoryTest.php | 5 +-- Tests/Functional/Service/HashServiceTest.php | 34 +++++-------------- .../FormRequestTypeTransformerTest.php | 16 +-------- .../FormValuesTypeTransformerTest.php | 24 ++++++++----- .../Provider/ConsentChartDataProviderTest.php | 16 +++------ Tests/Unit/Domain/Model/ConsentTest.php | 5 +-- Tests/Unit/Event/ApproveConsentEventTest.php | 11 ++---- Tests/Unit/Event/DismissConsentEventTest.php | 11 ++---- Tests/Unit/Event/GenerateHashEventTest.php | 12 +++---- Tests/Unit/Event/ModifyConsentEventTest.php | 11 ++---- .../Unit/Event/ModifyConsentMailEventTest.php | 11 ++---- .../Registry/ConsentManagerRegistryTest.php | 5 +-- Tests/Unit/Registry/Dto/ConsentStateTest.php | 11 ++---- Tests/Unit/Type/JsonTypeTest.php | 2 +- .../TypeTransformerFactoryTest.php | 11 ++---- composer.json | 24 ++++++------- ext_emconf.php | 4 +-- 41 files changed, 154 insertions(+), 268 deletions(-) diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 57c673f6..99e673c5 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -1,7 +1,7 @@ name: typo3-ext-form-consent type: typo3 docroot: .Build/web -php_version: "8.1" +php_version: "7.4" webserver_type: nginx-fpm router_http_port: "80" router_https_port: "443" @@ -13,7 +13,7 @@ mysql_version: "" nfs_mount_enabled: false mutagen_enabled: false omit_containers: [dba] -webimage_extra_packages: [php8.1-pcov] +webimage_extra_packages: [php7.4-pcov] use_dns_when_possible: true composer_version: "" web_environment: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d18f5565..bd50f945 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -8,9 +8,13 @@ jobs: strategy: fail-fast: false matrix: - typo3-version: [11.5] - php-version: [7.4, 8.0] include: + - typo3-version: 10.4 + php-version: 7.4 + - typo3-version: 11.5 + php-version: 7.4 + - typo3-version: 11.5 + php-version: 8.0 - typo3-version: 11.5 php-version: 8.1 coverage: 1 @@ -82,8 +86,15 @@ jobs: strategy: fail-fast: false matrix: - typo3-version: [11.5] - php-version: [7.4, 8.0, 8.1] + include: + - typo3-version: 10.4 + php-version: 7.4 + - typo3-version: 11.5 + php-version: 7.4 + - typo3-version: 11.5 + php-version: 8.0 + - typo3-version: 11.5 + php-version: 8.1 env: typo3DatabaseName: typo3 typo3DatabaseHost: '127.0.0.1' diff --git a/Classes/Configuration/Icon.php b/Classes/Configuration/Icon.php index 3fa0116d..6a6b5d6d 100644 --- a/Classes/Configuration/Icon.php +++ b/Classes/Configuration/Icon.php @@ -34,10 +34,7 @@ */ final class Icon { - /** - * @var IconRegistry - */ - protected static $iconRegistry; + protected static ?IconRegistry $iconRegistry = null; public static function forTable(string $tableName, string $type = 'svg'): string { diff --git a/Classes/Controller/ConsentController.php b/Classes/Controller/ConsentController.php index 9fb8212c..1b8496d8 100644 --- a/Classes/Controller/ConsentController.php +++ b/Classes/Controller/ConsentController.php @@ -29,7 +29,7 @@ use EliasHaeussler\Typo3FormConsent\Http\StringableResponseFactory; use EliasHaeussler\Typo3FormConsent\Registry\ConsentManagerRegistry; use Psr\Http\Message\ResponseInterface; -use TYPO3\CMS\Core\Http\PropagateResponseException; +use TYPO3\CMS\Core\Http\ImmediateResponseException; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException; use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException; @@ -43,20 +43,9 @@ */ class ConsentController extends ActionController { - /** - * @var ConsentRepository - */ - protected $consentRepository; - - /** - * @var PersistenceManagerInterface - */ - protected $persistenceManager; - - /** - * @var StringableResponseFactory - */ - protected $stringableResponseFactory; + protected ConsentRepository $consentRepository; + protected PersistenceManagerInterface $persistenceManager; + protected StringableResponseFactory $stringableResponseFactory; public function __construct( ConsentRepository $consentRepository, @@ -70,6 +59,7 @@ public function __construct( /** * @throws IllegalObjectTypeException + * @throws ImmediateResponseException * @throws UnknownObjectException */ public function approveAction(string $hash, string $email): ResponseInterface @@ -152,6 +142,9 @@ public function dismissAction(string $hash, string $email): ResponseInterface return $this->createResponse(); } + /** + * @throws ImmediateResponseException + */ protected function createErrorResponse(string $reason): ResponseInterface { $this->view->assign('error', true); @@ -160,6 +153,9 @@ protected function createErrorResponse(string $reason): ResponseInterface return $this->createHtmlResponse(); } + /** + * @throws ImmediateResponseException + */ protected function createHtmlResponse(ResponseInterface $previous = null): ResponseInterface { if (null === $previous) { @@ -167,7 +163,8 @@ protected function createHtmlResponse(ResponseInterface $previous = null): Respo } if ($previous->getStatusCode() >= 300) { - throw new PropagateResponseException($previous, 1645646663); + // @todo Use PropagateResponseException once v10 support is dropped + throw new ImmediateResponseException($previous, 1645646663); } $content = (string)$previous->getBody(); diff --git a/Classes/DependencyInjection/DashboardServicesConfigurator.php b/Classes/DependencyInjection/DashboardServicesConfigurator.php index b4098d8e..3deb5e6b 100644 --- a/Classes/DependencyInjection/DashboardServicesConfigurator.php +++ b/Classes/DependencyInjection/DashboardServicesConfigurator.php @@ -46,10 +46,7 @@ final class DashboardServicesConfigurator private const APPROVED_CONSENTS_DATA_PROVIDER = 'form_consent.widget.approved_consents.data_provider'; private const CONSENT_CONNECTION = 'form_consent.connection.consent'; - /** - * @var ServicesConfigurator - */ - private $services; + private ServicesConfigurator $services; public function __construct(ServicesConfigurator $services) { diff --git a/Classes/Domain/Finishers/ConsentFinisher.php b/Classes/Domain/Finishers/ConsentFinisher.php index 69d66b9e..d99c1137 100644 --- a/Classes/Domain/Finishers/ConsentFinisher.php +++ b/Classes/Domain/Finishers/ConsentFinisher.php @@ -54,16 +54,30 @@ class ConsentFinisher extends AbstractFinisher implements LoggerAwareInterface protected ConsentFactory $consentFactory; protected EventDispatcherInterface $eventDispatcher; + protected Mailer $mailer; protected PersistenceManagerInterface $persistenceManager; - /** @noinspection PhpMissingParentConstructorInspection */ - public function __construct( - ConsentFactory $consentFactory, - EventDispatcherInterface $eventDispatcher, - PersistenceManagerInterface $persistenceManager - ) { + // @todo move to constructor once v10 support is dropped + public function injectConsentFactory(ConsentFactory $consentFactory): void + { $this->consentFactory = $consentFactory; + } + + // @todo move to constructor once v10 support is dropped + public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { $this->eventDispatcher = $eventDispatcher; + } + + // @todo move to constructor once v10 support is dropped + public function injectMailer(Mailer $mailer): void + { + $this->mailer = $mailer; + } + + // @todo move to constructor once v10 support is dropped + public function injectPersistenceManager(PersistenceManagerInterface $persistenceManager): void + { $this->persistenceManager = $persistenceManager; } @@ -88,9 +102,7 @@ protected function executeInternal(): ?string protected function executeConsent(): void { $formRuntime = $this->finisherContext->getFormRuntime(); - $finisherOptions = new FinisherOptions(function (string $optionName) { - return $this->parseOption($optionName); - }); + $finisherOptions = new FinisherOptions(fn (string $optionName) => $this->parseOption($optionName)); // Create consent $consent = $this->consentFactory->createFromForm($finisherOptions, $formRuntime); @@ -119,8 +131,7 @@ protected function executeConsent(): void // Send mail try { - $mailer = GeneralUtility::makeInstance(Mailer::class); - $mailer->send($mail); + $this->mailer->send($mail); } catch (TransportExceptionInterface $e) { throw new FinisherException( Localization::forKey('consentMail.error', null, true), diff --git a/Classes/Domain/Finishers/FinisherOptions.php b/Classes/Domain/Finishers/FinisherOptions.php index eea9f437..76393e61 100644 --- a/Classes/Domain/Finishers/FinisherOptions.php +++ b/Classes/Domain/Finishers/FinisherOptions.php @@ -42,10 +42,7 @@ */ final class FinisherOptions implements \ArrayAccess { - /** - * @var PageRepository - */ - private static $pageRepository; + private static ?PageRepository $pageRepository = null; /** * @var callable(string): mixed diff --git a/Classes/Event/ApproveConsentEvent.php b/Classes/Event/ApproveConsentEvent.php index fb336c26..e379e214 100644 --- a/Classes/Event/ApproveConsentEvent.php +++ b/Classes/Event/ApproveConsentEvent.php @@ -34,15 +34,8 @@ */ class ApproveConsentEvent { - /** - * @var Consent - */ - protected $consent; - - /** - * @var ResponseInterface|null - */ - protected $response; + protected Consent $consent; + protected ?ResponseInterface $response = null; public function __construct(Consent $consent) { diff --git a/Classes/Event/DismissConsentEvent.php b/Classes/Event/DismissConsentEvent.php index 8a8a75d9..1ddf1b75 100644 --- a/Classes/Event/DismissConsentEvent.php +++ b/Classes/Event/DismissConsentEvent.php @@ -33,10 +33,7 @@ */ class DismissConsentEvent { - /** - * @var Consent - */ - protected $consent; + protected Consent $consent; public function __construct(Consent $consent) { diff --git a/Classes/Event/GenerateHashEvent.php b/Classes/Event/GenerateHashEvent.php index 19ed1440..3688a47b 100644 --- a/Classes/Event/GenerateHashEvent.php +++ b/Classes/Event/GenerateHashEvent.php @@ -34,22 +34,14 @@ class GenerateHashEvent { /** - * @var mixed[] + * @var list */ - protected $components; + protected array $components; + protected Consent $consent; + protected ?string $hash = null; /** - * @var Consent - */ - protected $consent; - - /** - * @var string|null - */ - protected $hash; - - /** - * @param mixed[] $components + * @param list $components */ public function __construct(array $components, Consent $consent) { @@ -58,7 +50,7 @@ public function __construct(array $components, Consent $consent) } /** - * @return mixed[] + * @return list */ public function getComponents(): array { @@ -66,7 +58,7 @@ public function getComponents(): array } /** - * @param mixed[] $components + * @param list $components */ public function setComponents(array $components): self { diff --git a/Classes/Event/ModifyConsentEvent.php b/Classes/Event/ModifyConsentEvent.php index 9bfed85c..97493d8b 100644 --- a/Classes/Event/ModifyConsentEvent.php +++ b/Classes/Event/ModifyConsentEvent.php @@ -33,10 +33,7 @@ */ class ModifyConsentEvent { - /** - * @var Consent - */ - protected $consent; + protected Consent $consent; public function __construct(Consent $consent) { diff --git a/Classes/Event/ModifyConsentMailEvent.php b/Classes/Event/ModifyConsentMailEvent.php index 2557a0a2..d7997e32 100644 --- a/Classes/Event/ModifyConsentMailEvent.php +++ b/Classes/Event/ModifyConsentMailEvent.php @@ -33,10 +33,7 @@ */ class ModifyConsentMailEvent { - /** - * @var FluidEmail - */ - protected $mail; + protected FluidEmail $mail; public function __construct(FluidEmail $mail) { diff --git a/Classes/Http/StringableResponse.php b/Classes/Http/StringableResponse.php index 49dcf82a..9576a12a 100644 --- a/Classes/Http/StringableResponse.php +++ b/Classes/Http/StringableResponse.php @@ -35,6 +35,10 @@ final class StringableResponse extends Response { public function __toString(): string { - return (string)$this->body; + if (null === $this->body) { + return ''; + } + + return $this->body->__toString(); } } diff --git a/Classes/Http/StringableResponseFactory.php b/Classes/Http/StringableResponseFactory.php index baafb1a6..6ee20b73 100644 --- a/Classes/Http/StringableResponseFactory.php +++ b/Classes/Http/StringableResponseFactory.php @@ -36,10 +36,7 @@ */ final class StringableResponseFactory implements ResponseFactoryInterface { - /** - * @var bool - */ - private $useCompatibilityLayer; + private bool $useCompatibilityLayer; public function __construct() { diff --git a/Classes/Registry/ConsentManagerRegistry.php b/Classes/Registry/ConsentManagerRegistry.php index 95d01b93..4c20a5cf 100644 --- a/Classes/Registry/ConsentManagerRegistry.php +++ b/Classes/Registry/ConsentManagerRegistry.php @@ -38,7 +38,7 @@ final class ConsentManagerRegistry /** * @var array> */ - private static $states = []; + private static array $states = []; public static function registerConsent(Consent $consent): ConsentState { diff --git a/Classes/Registry/Dto/ConsentState.php b/Classes/Registry/Dto/ConsentState.php index 56e9bf14..ce8258aa 100644 --- a/Classes/Registry/Dto/ConsentState.php +++ b/Classes/Registry/Dto/ConsentState.php @@ -34,10 +34,7 @@ */ final class ConsentState { - /** - * @var Consent - */ - private $consent; + private Consent $consent; public function __construct(Consent $consent) { diff --git a/Classes/Service/HashService.php b/Classes/Service/HashService.php index c2fd3a55..cdab49cc 100644 --- a/Classes/Service/HashService.php +++ b/Classes/Service/HashService.php @@ -36,10 +36,7 @@ */ class HashService { - /** - * @var EventDispatcherInterface - */ - protected $eventDispatcher; + protected EventDispatcherInterface $eventDispatcher; public function __construct(EventDispatcherInterface $eventDispatcher) { diff --git a/Classes/Type/Transformer/FormRequestTypeTransformer.php b/Classes/Type/Transformer/FormRequestTypeTransformer.php index d1fa001d..a07a9bc3 100644 --- a/Classes/Type/Transformer/FormRequestTypeTransformer.php +++ b/Classes/Type/Transformer/FormRequestTypeTransformer.php @@ -38,10 +38,7 @@ */ final class FormRequestTypeTransformer implements TypeTransformerInterface { - /** - * @var HashService - */ - private $hashService; + private HashService $hashService; public function __construct(HashService $hashService) { diff --git a/Classes/Type/Transformer/FormValuesTypeTransformer.php b/Classes/Type/Transformer/FormValuesTypeTransformer.php index e30fc9c7..03286b4a 100644 --- a/Classes/Type/Transformer/FormValuesTypeTransformer.php +++ b/Classes/Type/Transformer/FormValuesTypeTransformer.php @@ -39,10 +39,7 @@ */ final class FormValuesTypeTransformer implements TypeTransformerInterface { - /** - * @var Context - */ - private $context; + private Context $context; public function __construct(Context $context) { diff --git a/Classes/Type/Transformer/TypeTransformerFactory.php b/Classes/Type/Transformer/TypeTransformerFactory.php index 340bbd01..0421f635 100644 --- a/Classes/Type/Transformer/TypeTransformerFactory.php +++ b/Classes/Type/Transformer/TypeTransformerFactory.php @@ -34,10 +34,7 @@ */ final class TypeTransformerFactory { - /** - * @var ServiceLocator - */ - private $typeTransformers; + private ServiceLocator $typeTransformers; public function __construct(ServiceLocator $transformers) { diff --git a/Classes/Widget/Provider/ConsentChartDataProvider.php b/Classes/Widget/Provider/ConsentChartDataProvider.php index a68e50bc..48118ec2 100644 --- a/Classes/Widget/Provider/ConsentChartDataProvider.php +++ b/Classes/Widget/Provider/ConsentChartDataProvider.php @@ -38,10 +38,7 @@ */ class ConsentChartDataProvider implements ChartDataProviderInterface { - /** - * @var Connection - */ - protected $connection; + protected Connection $connection; public function __construct(Connection $connection) { diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 8d468b0a..a5b10525 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -11,10 +11,16 @@ services: - '../Classes/Domain/Finishers/FinisherOptions.php' - '../Classes/Domain/Model/*' + # @todo Remove once v10 support is dropped EliasHaeussler\Typo3FormConsent\Domain\Finishers\ConsentFinisher: public: true shared: false + # @todo Remove once v10 support is dropped + TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface: + alias: TYPO3\CMS\Extbase\Configuration\ConfigurationManager + public: true + EliasHaeussler\Typo3FormConsent\Type\Transformer\TypeTransformerFactory: arguments: $transformers: !tagged_locator { tag: 'form_consent.type_transformer', default_index_method: 'getName' } diff --git a/Tests/Functional/Configuration/IconTest.php b/Tests/Functional/Configuration/IconTest.php index 938d6d9b..572d378e 100644 --- a/Tests/Functional/Configuration/IconTest.php +++ b/Tests/Functional/Configuration/IconTest.php @@ -36,10 +36,7 @@ */ class IconTest extends FunctionalTestCase { - /** - * @var IconRegistry - */ - protected $iconRegistry; + protected IconRegistry $iconRegistry; protected function setUp(): void { diff --git a/Tests/Functional/Domain/Finishers/FinisherOptionsTest.php b/Tests/Functional/Domain/Finishers/FinisherOptionsTest.php index 4a4e3c71..017aac4b 100644 --- a/Tests/Functional/Domain/Finishers/FinisherOptionsTest.php +++ b/Tests/Functional/Domain/Finishers/FinisherOptionsTest.php @@ -46,15 +46,12 @@ class FinisherOptionsTest extends FunctionalTestCase 'typo3conf/ext/form_consent', ]; - /** - * @var FinisherOptions - */ - protected $subject; + protected FinisherOptions $subject; /** * @var array */ - protected $options = []; + protected array $options = []; protected function setUp(): void { diff --git a/Tests/Functional/Domain/Repository/ConsentRepositoryTest.php b/Tests/Functional/Domain/Repository/ConsentRepositoryTest.php index cf6931d8..f3374d51 100644 --- a/Tests/Functional/Domain/Repository/ConsentRepositoryTest.php +++ b/Tests/Functional/Domain/Repository/ConsentRepositoryTest.php @@ -43,10 +43,7 @@ class ConsentRepositoryTest extends FunctionalTestCase 'typo3conf/ext/form_consent', ]; - /** - * @var ConsentRepository - */ - protected $subject; + protected ConsentRepository $subject; protected function setUp(): void { diff --git a/Tests/Functional/Service/HashServiceTest.php b/Tests/Functional/Service/HashServiceTest.php index 8d0e533c..fec33334 100644 --- a/Tests/Functional/Service/HashServiceTest.php +++ b/Tests/Functional/Service/HashServiceTest.php @@ -27,7 +27,6 @@ use EliasHaeussler\Typo3FormConsent\Event\GenerateHashEvent; use EliasHaeussler\Typo3FormConsent\Service\HashService; use EliasHaeussler\Typo3FormConsent\Type\JsonType; -use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\DependencyInjection\Container; use TYPO3\CMS\Core\EventDispatcher\ListenerProvider; @@ -41,25 +40,9 @@ */ class HashServiceTest extends FunctionalTestCase { - /** - * @var Consent - */ - protected $consent; - - /** - * @var ListenerProvider - */ - protected $listenerProvider; - - /** - * @var ContainerInterface - */ - protected $container; - - /** - * @var HashService - */ - protected $subject; + protected Consent $consent; + protected ListenerProvider $listenerProvider; + protected HashService $subject; protected function setUp(): void { @@ -71,9 +54,8 @@ protected function setUp(): void ->setData(JsonType::fromArray(['foo' => 'baz'])) ->setValidUntil(new \DateTime()); - $this->container = $this->getContainer(); - $this->listenerProvider = $this->container->get(ListenerProvider::class); - $this->subject = new HashService($this->container->get(EventDispatcherInterface::class)); + $this->listenerProvider = $this->getContainer()->get(ListenerProvider::class); + $this->subject = new HashService($this->getContainer()->get(EventDispatcherInterface::class)); } /** @@ -194,9 +176,11 @@ public function __invoke(GenerateHashEvent $event): void private function addEventListener(string $event, string $service, object $object): void { - self::assertInstanceOf(Container::class, $this->container); + $container = $this->getContainer(); + + self::assertInstanceOf(Container::class, $container); - $this->container->set($service, $object); + $container->set($service, $object); $this->listenerProvider->addListener($event, $service); } } diff --git a/Tests/Functional/Type/Transformer/FormRequestTypeTransformerTest.php b/Tests/Functional/Type/Transformer/FormRequestTypeTransformerTest.php index 946d0bec..5a7864b8 100644 --- a/Tests/Functional/Type/Transformer/FormRequestTypeTransformerTest.php +++ b/Tests/Functional/Type/Transformer/FormRequestTypeTransformerTest.php @@ -25,7 +25,6 @@ use EliasHaeussler\Typo3FormConsent\Type\Transformer\FormRequestTypeTransformer; use TYPO3\CMS\Extbase\Security\Cryptography\HashService; -use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; /** @@ -36,26 +35,13 @@ */ class FormRequestTypeTransformerTest extends FunctionalTestCase { - protected $coreExtensionsToLoad = [ - 'form', - ]; - - /** - * @var FormRequestTypeTransformer - */ - protected $subject; - - /** - * @var FormRuntime - */ - protected $formRuntime; + protected FormRequestTypeTransformer $subject; protected function setUp(): void { parent::setUp(); $this->subject = new FormRequestTypeTransformer($this->getContainer()->get(HashService::class)); - $this->formRuntime = $this->getContainer()->get(FormRuntime::class); } /** diff --git a/Tests/Functional/Type/Transformer/FormValuesTypeTransformerTest.php b/Tests/Functional/Type/Transformer/FormValuesTypeTransformerTest.php index 3f12fde2..72cb0c6a 100644 --- a/Tests/Functional/Type/Transformer/FormValuesTypeTransformerTest.php +++ b/Tests/Functional/Type/Transformer/FormValuesTypeTransformerTest.php @@ -25,6 +25,8 @@ use EliasHaeussler\Typo3FormConsent\Type\JsonType; use EliasHaeussler\Typo3FormConsent\Type\Transformer\FormValuesTypeTransformer; +use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; @@ -37,26 +39,30 @@ */ class FormValuesTypeTransformerTest extends FunctionalTestCase { + use ProphecyTrait; + protected $coreExtensionsToLoad = [ 'form', ]; - /** - * @var FormValuesTypeTransformer - */ - protected $subject; + protected $testExtensionsToLoad = [ + 'typo3conf/ext/form_consent', + ]; + + protected FormValuesTypeTransformer $subject; /** - * @var FormRuntime + * @var FormRuntime|ObjectProphecy */ - protected $formRuntime; + protected $formRuntimeProphecy; protected function setUp(): void { parent::setUp(); $this->subject = new FormValuesTypeTransformer(new Context()); - $this->formRuntime = $this->getContainer()->get(FormRuntime::class); + // @todo Replace with $this->getContainer()->get(FormRuntime::class) once v10 support is dropped + $this->formRuntimeProphecy = $this->prophesize(FormRuntime::class); } /** @@ -76,6 +82,8 @@ public function transformThrowsExceptionIfFormRuntimeIsNotGiven(): void */ public function transformReturnsJsonTypeWithEmptyArrayIfFormIsUninitialized(): void { - self::assertEquals(JsonType::fromArray([]), $this->subject->transform($this->formRuntime)); + $this->formRuntimeProphecy->getFormState()->willReturn(null); + + self::assertEquals(JsonType::fromArray([]), $this->subject->transform($this->formRuntimeProphecy->reveal())); } } diff --git a/Tests/Functional/Widget/Provider/ConsentChartDataProviderTest.php b/Tests/Functional/Widget/Provider/ConsentChartDataProviderTest.php index a3ccfebb..4e9360f4 100644 --- a/Tests/Functional/Widget/Provider/ConsentChartDataProviderTest.php +++ b/Tests/Functional/Widget/Provider/ConsentChartDataProviderTest.php @@ -45,15 +45,9 @@ class ConsentChartDataProviderTest extends FunctionalTestCase 'typo3conf/ext/form_consent', ]; - /** - * @var ConsentChartDataProvider - */ - protected $subject; + private static string $languagePrefix = 'LLL:EXT:form_consent/Resources/Private/Language/locallang_be.xlf:'; - /** - * @var string - */ - private $languagePrefix = 'LLL:EXT:form_consent/Resources/Private/Language/locallang_be.xlf:'; + protected ConsentChartDataProvider $subject; /** * @throws DBALException @@ -82,9 +76,9 @@ public function getChartDataReturnsCorrectChartData(): void $chartData = $this->subject->getChartData(); $labels = $chartData['labels']; - self::assertSame($this->languagePrefix . 'charts.approved', $labels[0]); - self::assertSame($this->languagePrefix . 'charts.nonApproved', $labels[1]); - self::assertSame($this->languagePrefix . 'charts.dismissed', $labels[2]); + self::assertSame(self::$languagePrefix . 'charts.approved', $labels[0]); + self::assertSame(self::$languagePrefix . 'charts.nonApproved', $labels[1]); + self::assertSame(self::$languagePrefix . 'charts.dismissed', $labels[2]); $data = $chartData['datasets'][0]['data']; self::assertSame($expectedApprovedCount, $data[0]); diff --git a/Tests/Unit/Domain/Model/ConsentTest.php b/Tests/Unit/Domain/Model/ConsentTest.php index e5b8368f..839623b4 100644 --- a/Tests/Unit/Domain/Model/ConsentTest.php +++ b/Tests/Unit/Domain/Model/ConsentTest.php @@ -35,10 +35,7 @@ */ class ConsentTest extends UnitTestCase { - /** - * @var Consent - */ - protected $subject; + protected Consent $subject; protected function setUp(): void { diff --git a/Tests/Unit/Event/ApproveConsentEventTest.php b/Tests/Unit/Event/ApproveConsentEventTest.php index 8f16eecd..960e12d0 100644 --- a/Tests/Unit/Event/ApproveConsentEventTest.php +++ b/Tests/Unit/Event/ApproveConsentEventTest.php @@ -36,15 +36,8 @@ */ class ApproveConsentEventTest extends UnitTestCase { - /** - * @var ApproveConsentEvent - */ - protected $subject; - - /** - * @var Consent - */ - protected $consent; + protected ApproveConsentEvent $subject; + protected Consent $consent; protected function setUp(): void { diff --git a/Tests/Unit/Event/DismissConsentEventTest.php b/Tests/Unit/Event/DismissConsentEventTest.php index a4dfb7c4..f0229d44 100644 --- a/Tests/Unit/Event/DismissConsentEventTest.php +++ b/Tests/Unit/Event/DismissConsentEventTest.php @@ -35,15 +35,8 @@ */ class DismissConsentEventTest extends UnitTestCase { - /** - * @var DismissConsentEvent - */ - protected $subject; - - /** - * @var Consent - */ - protected $consent; + protected DismissConsentEvent $subject; + protected Consent $consent; protected function setUp(): void { diff --git a/Tests/Unit/Event/GenerateHashEventTest.php b/Tests/Unit/Event/GenerateHashEventTest.php index 0c665b5b..b926740c 100644 --- a/Tests/Unit/Event/GenerateHashEventTest.php +++ b/Tests/Unit/Event/GenerateHashEventTest.php @@ -35,16 +35,14 @@ */ class GenerateHashEventTest extends UnitTestCase { - /** - * @var GenerateHashEvent - */ - protected $subject; + protected GenerateHashEvent $subject; /** - * @var mixed[] + * @var list */ - protected $components = [ - 'foo' => 'baz', + protected array $components = [ + 'foo', + 'baz', ]; protected function setUp(): void diff --git a/Tests/Unit/Event/ModifyConsentEventTest.php b/Tests/Unit/Event/ModifyConsentEventTest.php index 0537f8dd..21cc46a9 100644 --- a/Tests/Unit/Event/ModifyConsentEventTest.php +++ b/Tests/Unit/Event/ModifyConsentEventTest.php @@ -35,15 +35,8 @@ */ class ModifyConsentEventTest extends UnitTestCase { - /** - * @var ModifyConsentEvent - */ - protected $subject; - - /** - * @var Consent - */ - protected $consent; + protected ModifyConsentEvent $subject; + protected Consent $consent; protected function setUp(): void { diff --git a/Tests/Unit/Event/ModifyConsentMailEventTest.php b/Tests/Unit/Event/ModifyConsentMailEventTest.php index f4027f9c..88962426 100644 --- a/Tests/Unit/Event/ModifyConsentMailEventTest.php +++ b/Tests/Unit/Event/ModifyConsentMailEventTest.php @@ -38,15 +38,8 @@ class ModifyConsentMailEventTest extends UnitTestCase { use ProphecyTrait; - /** - * @var ModifyConsentMailEvent - */ - protected $subject; - - /** - * @var FluidEmail - */ - protected $mail; + protected ModifyConsentMailEvent $subject; + protected FluidEmail $mail; protected function setUp(): void { diff --git a/Tests/Unit/Registry/ConsentManagerRegistryTest.php b/Tests/Unit/Registry/ConsentManagerRegistryTest.php index 9c0c1faf..a04e20cb 100644 --- a/Tests/Unit/Registry/ConsentManagerRegistryTest.php +++ b/Tests/Unit/Registry/ConsentManagerRegistryTest.php @@ -35,10 +35,7 @@ */ class ConsentManagerRegistryTest extends UnitTestCase { - /** - * @var Consent - */ - protected $consent; + protected Consent $consent; protected function setUp(): void { diff --git a/Tests/Unit/Registry/Dto/ConsentStateTest.php b/Tests/Unit/Registry/Dto/ConsentStateTest.php index fc993ffb..cde56ac9 100644 --- a/Tests/Unit/Registry/Dto/ConsentStateTest.php +++ b/Tests/Unit/Registry/Dto/ConsentStateTest.php @@ -36,15 +36,8 @@ */ class ConsentStateTest extends UnitTestCase { - /** - * @var Consent - */ - protected $consent; - - /** - * @var ConsentState - */ - protected $subject; + protected Consent $consent; + protected ConsentState $subject; protected function setUp(): void { diff --git a/Tests/Unit/Type/JsonTypeTest.php b/Tests/Unit/Type/JsonTypeTest.php index 6ebae166..4331ded6 100644 --- a/Tests/Unit/Type/JsonTypeTest.php +++ b/Tests/Unit/Type/JsonTypeTest.php @@ -37,7 +37,7 @@ class JsonTypeTest extends UnitTestCase /** * @var JsonType */ - private JsonType $subject; + protected JsonType $subject; protected function setUp(): void { diff --git a/Tests/Unit/Type/Transformer/TypeTransformerFactoryTest.php b/Tests/Unit/Type/Transformer/TypeTransformerFactoryTest.php index c43c4d89..85da6e2e 100644 --- a/Tests/Unit/Type/Transformer/TypeTransformerFactoryTest.php +++ b/Tests/Unit/Type/Transformer/TypeTransformerFactoryTest.php @@ -38,15 +38,8 @@ */ class TypeTransformerFactoryTest extends UnitTestCase { - /** - * @var FormRequestTypeTransformer - */ - protected $formRequestTypeTransformer; - - /** - * @var TypeTransformerFactory - */ - protected $subject; + protected FormRequestTypeTransformer $formRequestTypeTransformer; + protected TypeTransformerFactory $subject; protected function setUp(): void { diff --git a/composer.json b/composer.json index 6e8d6ec8..df6298eb 100644 --- a/composer.json +++ b/composer.json @@ -24,12 +24,12 @@ "symfony/mailer": "^4.4 || ^5.0", "symfony/mime": "^4.4 || ^5.0", "symfony/polyfill-php80": "^1.24", - "typo3/cms-backend": "~11.5.0", - "typo3/cms-core": "~11.5.0", - "typo3/cms-extbase": "~11.5.0", - "typo3/cms-fluid": "~11.5.0", - "typo3/cms-form": "~11.5.0", - "typo3/cms-frontend": "~11.5.0" + "typo3/cms-backend": "~10.4.0 || ~11.5.0", + "typo3/cms-core": "~10.4.0 || ~11.5.0", + "typo3/cms-extbase": "~10.4.0 || ~11.5.0", + "typo3/cms-fluid": "~10.4.0 || ~11.5.0", + "typo3/cms-form": "~10.4.0 || ~11.5.0", + "typo3/cms-frontend": "~10.4.0 || ~11.5.0" }, "require-dev": { "armin/editorconfig-cli": "^1.5", @@ -44,16 +44,16 @@ "phpunit/phpcov": "^8.2", "phpunit/phpunit": "^9.3", "saschaegerer/phpstan-typo3": "^1.0", - "typo3/cms-dashboard": "~11.5.0", - "typo3/cms-filelist": "~11.5.0", - "typo3/cms-lowlevel": "~11.5.0", - "typo3/cms-scheduler": "~11.5.0", + "typo3/cms-dashboard": "~10.4.0 || ~11.5.0", + "typo3/cms-filelist": "~10.4.0 || ~11.5.0", + "typo3/cms-lowlevel": "~10.4.0 || ~11.5.0", + "typo3/cms-scheduler": "~10.4.0 || ~11.5.0", "typo3/coding-standards": "^0.5.0", "typo3/testing-framework": "^6.14" }, "suggest": { - "typo3/cms-dashboard": "Adds a custom form consent widget to the TYPO3 dashboard (~11.5.0)", - "typo3/cms-scheduler": "Allows garbage collection of expired consents (~11.5.0)" + "typo3/cms-dashboard": "Adds a custom form consent widget to the TYPO3 dashboard (~10.4.0 || ~11.5.0)", + "typo3/cms-scheduler": "Allows garbage collection of expired consents (~10.4.0 || ~11.5.0)" }, "autoload": { "psr-4": { diff --git a/ext_emconf.php b/ext_emconf.php index fb8212bd..88db8660 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -27,13 +27,11 @@ 'author' => 'Elias Häußler', 'author_email' => 'elias@haeussler.dev', 'state' => 'alpha', - 'uploadfolder' => false, - 'createDirs' => '', 'clearCacheOnLoad' => false, 'version' => '0.2.2', 'constraints' => [ 'depends' => [ - 'typo3' => '11.5.0-11.5.99', + 'typo3' => '10.4.0-11.5.99', ], ], ]; From cb48d1aafd1379856d97d8de9a535e0a24d5730f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Mon, 28 Feb 2022 18:10:33 +0100 Subject: [PATCH 07/46] [DOCS] Document events and invocation of finishers on consent approval --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 544b0d27..e5689316 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,11 @@ compliance with the GDPR. * Stores all submitted form data as JSON in database * System-dependent hash-based validation system (using TYPO3's HMAC functionality) * Plugin to approve or dismiss a consent -* Several events for better customization +* Possibility to [invoke finishers on consent approval](#invoke-finishers-on-consent-approval) +* Several [events](#events) for better customization * Scheduler garbage collection task for expired consents * Dashboard widget for approved, non-approved and dismissed consents -* Compatible with TYPO3 10.4 LTS and 11.5 LTS +* Compatible with TYPO3 10.4 and 11.5 LTS ## :fire: Installation @@ -89,6 +90,67 @@ The following options are available to the `Consent` finisher: options are only applied to the appropriate form. They are merged with the default template paths configured via TypoScript. +## :writing_hand: Customization + +The lifecycle of the entire consent process can be influenced in several +ways. This leads to high flexibility in customization while maintaining +high stability of the core components. + +### Events + +PSR-14 events can be used to modify different areas in the consent process. +The following events are available: + +* [`ApproveConsentEvent`](Classes/Event/ApproveConsentEvent.php) +* [`DismissConsentEvent`](Classes/Event/DismissConsentEvent.php) +* [`GenerateHashEvent`](Classes/Event/GenerateHashEvent.php) +* [`ModifyConsentEvent`](Classes/Event/ModifyConsentEvent.php) +* [`ModifyConsentMailEvent`](Classes/Event/ModifyConsentMailEvent.php) + +### Invoke finishers on consent approval + +After a user has given consent, it is often necessary to execute certain +form finishers. For example, to send an admin email or redirect to a +specific page. + +To achieve this, after the user gives consent, the originally completed +form is resubmitted. During this resubmission of the form, the selected +finishers can now be overwritten using the `isConsentApproved()` condition +in a form variant. + +#### Requirements + +The following requirements must be met for the form to be resubmitted: + +1. Form variant at the root level of the form must exist +2. Form variant must redefine the finishers used +3. Condition `isConsentApproved()` must exist in the variant + +#### Example + +The following form variant is stored directly on the root level of the +form definition (that is, your `.form.yaml` file). It specifies the form +finishers to be executed in case of successful approval by the user. + +```yaml +variants: + - + identifier: post-consent-approval-variant-1 + condition: 'isConsentApproved()' + finishers: + - + identifier: EmailToReceiver + options: + # ... + - + identifier: Redirect + options: + # ... +``` + +In this example, an admin email would be sent after the consent has been +given and a redirect to the configured confirmation page would take place. + ## :gem: Credits Icons made by [Google](https://www.flaticon.com/authors/google) from From e2f3a2e325d8c20ab2d39fa8023fc117e09d873e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Mon, 28 Feb 2022 18:14:16 +0100 Subject: [PATCH 08/46] [DOCS] Add several badges to README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e5689316..a7c9877d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,11 @@ [![Tests](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/tests.yaml/badge.svg)](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/tests.yaml) [![CGL](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/cgl.yaml/badge.svg)](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/cgl.yaml) [![Latest Stable Version](http://poser.pugx.org/eliashaeussler/typo3-form-consent/v)](https://packagist.org/packages/eliashaeussler/typo3-form-consent) -[![License](http://poser.pugx.org/eliashaeussler/typo3-form-consent/license)](LICENSE.md) +[![License](http://poser.pugx.org/eliashaeussler/typo3-form-consent/license)](LICENSE.md)\ +![Version](https://shields.io/endpoint?url=https://typo3-badges.dev/badge/form_consent/version/shields) +![Downloads](https://shields.io/endpoint?url=https://typo3-badges.dev/badge/form_consent/downloads/shields) +![Extension stability](https://shields.io/endpoint?url=https://typo3-badges.dev/badge/form_consent/stability/shields) +![TYPO3 badge](https://shields.io/endpoint?url=https://typo3-badges.dev/badge/typo3/shields) :package: [Packagist](https://packagist.org/packages/eliashaeussler/typo3-form-consent) | :hatched_chick: [TYPO3 extension repository](https://extensions.typo3.org/extension/form_consent) | From 6f67e128d3fe5207b0d35ca80030178be343faec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Mon, 28 Feb 2022 18:30:58 +0100 Subject: [PATCH 09/46] [!!!][TASK] Mark several classes as final --- Classes/Configuration/Icon.php | 6 +++--- Classes/Controller/ConsentController.php | 14 +++++++------- Classes/Domain/Finishers/ConsentFinisher.php | 18 +++++++++--------- Classes/Domain/Model/Consent.php | 1 + Classes/Event/ApproveConsentEvent.php | 6 +++--- Classes/Event/DismissConsentEvent.php | 4 ++-- Classes/Event/GenerateHashEvent.php | 8 ++++---- Classes/Event/ModifyConsentEvent.php | 4 ++-- Classes/Event/ModifyConsentMailEvent.php | 4 ++-- Classes/Form/Element/ConsentDataElement.php | 8 ++++---- Classes/Service/HashService.php | 4 ++-- Classes/Widget/ApprovedConsentsWidget.php | 2 +- Tests/Functional/Configuration/IconTest.php | 2 +- .../Configuration/LocalizationTest.php | 2 +- .../Domain/Finishers/FinisherOptionsTest.php | 2 +- .../Repository/ConsentRepositoryTest.php | 2 +- Tests/Functional/Service/HashServiceTest.php | 2 +- .../FormRequestTypeTransformerTest.php | 2 +- .../FormValuesTypeTransformerTest.php | 2 +- .../Provider/ConsentChartDataProviderTest.php | 2 +- Tests/Unit/Configuration/IconTest.php | 2 +- Tests/Unit/Domain/Model/ConsentTest.php | 2 +- Tests/Unit/Event/ApproveConsentEventTest.php | 2 +- Tests/Unit/Event/DismissConsentEventTest.php | 2 +- Tests/Unit/Event/GenerateHashEventTest.php | 2 +- Tests/Unit/Event/ModifyConsentEventTest.php | 2 +- .../Unit/Event/ModifyConsentMailEventTest.php | 2 +- .../Registry/ConsentManagerRegistryTest.php | 2 +- Tests/Unit/Registry/Dto/ConsentStateTest.php | 2 +- Tests/Unit/Type/JsonTypeTest.php | 2 +- .../Transformer/TypeTransformerFactoryTest.php | 2 +- 31 files changed, 59 insertions(+), 58 deletions(-) diff --git a/Classes/Configuration/Icon.php b/Classes/Configuration/Icon.php index 6a6b5d6d..bdf6fe92 100644 --- a/Classes/Configuration/Icon.php +++ b/Classes/Configuration/Icon.php @@ -34,7 +34,7 @@ */ final class Icon { - protected static ?IconRegistry $iconRegistry = null; + private static ?IconRegistry $iconRegistry = null; public static function forTable(string $tableName, string $type = 'svg'): string { @@ -97,7 +97,7 @@ public static function registerForWidgetIdentifier(string $widgetName, string $t ); } - protected static function buildIconPath(string $fileName, string $type = 'svg'): string + private static function buildIconPath(string $fileName, string $type = 'svg'): string { $fileName = trim($fileName); $type = trim($type) ?: 'svg'; @@ -109,7 +109,7 @@ protected static function buildIconPath(string $fileName, string $type = 'svg'): return 'EXT:' . Extension::KEY . '/Resources/Public/Icons/' . $fileName . '.' . $type; } - protected static function register(string $filename, string $identifier): void + private static function register(string $filename, string $identifier): void { if (self::$iconRegistry === null) { self::$iconRegistry = GeneralUtility::makeInstance(IconRegistry::class); diff --git a/Classes/Controller/ConsentController.php b/Classes/Controller/ConsentController.php index 1b8496d8..958d1495 100644 --- a/Classes/Controller/ConsentController.php +++ b/Classes/Controller/ConsentController.php @@ -41,11 +41,11 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ConsentController extends ActionController +final class ConsentController extends ActionController { - protected ConsentRepository $consentRepository; - protected PersistenceManagerInterface $persistenceManager; - protected StringableResponseFactory $stringableResponseFactory; + private ConsentRepository $consentRepository; + private PersistenceManagerInterface $persistenceManager; + private StringableResponseFactory $stringableResponseFactory; public function __construct( ConsentRepository $consentRepository, @@ -145,7 +145,7 @@ public function dismissAction(string $hash, string $email): ResponseInterface /** * @throws ImmediateResponseException */ - protected function createErrorResponse(string $reason): ResponseInterface + private function createErrorResponse(string $reason): ResponseInterface { $this->view->assign('error', true); $this->view->assign('reason', $reason); @@ -156,7 +156,7 @@ protected function createErrorResponse(string $reason): ResponseInterface /** * @throws ImmediateResponseException */ - protected function createHtmlResponse(ResponseInterface $previous = null): ResponseInterface + private function createHtmlResponse(ResponseInterface $previous = null): ResponseInterface { if (null === $previous) { return $this->createResponse(); @@ -176,7 +176,7 @@ protected function createHtmlResponse(ResponseInterface $previous = null): Respo return $this->createResponse(); } - protected function createResponse(string $html = null): ResponseInterface + private function createResponse(string $html = null): ResponseInterface { // TYPO3 v11+ if (method_exists($this, 'htmlResponse')) { diff --git a/Classes/Domain/Finishers/ConsentFinisher.php b/Classes/Domain/Finishers/ConsentFinisher.php index d99c1137..9bdb68ad 100644 --- a/Classes/Domain/Finishers/ConsentFinisher.php +++ b/Classes/Domain/Finishers/ConsentFinisher.php @@ -48,14 +48,14 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ConsentFinisher extends AbstractFinisher implements LoggerAwareInterface +final class ConsentFinisher extends AbstractFinisher implements LoggerAwareInterface { use LoggerAwareTrait; - protected ConsentFactory $consentFactory; - protected EventDispatcherInterface $eventDispatcher; - protected Mailer $mailer; - protected PersistenceManagerInterface $persistenceManager; + private ConsentFactory $consentFactory; + private EventDispatcherInterface $eventDispatcher; + private Mailer $mailer; + private PersistenceManagerInterface $persistenceManager; // @todo move to constructor once v10 support is dropped public function injectConsentFactory(ConsentFactory $consentFactory): void @@ -99,7 +99,7 @@ protected function executeInternal(): ?string * @throws FinisherException * @throws \Exception */ - protected function executeConsent(): void + private function executeConsent(): void { $formRuntime = $this->finisherContext->getFormRuntime(); $finisherOptions = new FinisherOptions(fn (string $optionName) => $this->parseOption($optionName)); @@ -140,7 +140,7 @@ protected function executeConsent(): void } } - protected function initializeMail(FinisherOptions $finisherOptions): FluidEmail + private function initializeMail(FinisherOptions $finisherOptions): FluidEmail { // Initialize mail $mail = GeneralUtility::makeInstance(FluidEmail::class, $finisherOptions->getTemplatePaths()) @@ -160,7 +160,7 @@ protected function initializeMail(FinisherOptions $finisherOptions): FluidEmail return $mail; } - protected function addFlashMessage(\Exception $exception, bool $cancel = true): void + private function addFlashMessage(\Exception $exception, bool $cancel = true): void { $formDefinition = $this->finisherContext->getFormRuntime()->getFormDefinition(); $flashMessageFinisher = $formDefinition->createFinisher('FlashMessage', [ @@ -181,7 +181,7 @@ protected function addFlashMessage(\Exception $exception, bool $cancel = true): } } - protected function getServerRequest(): ?ServerRequestInterface + private function getServerRequest(): ?ServerRequestInterface { return $GLOBALS['TYPO3_REQUEST'] ?? null; } diff --git a/Classes/Domain/Model/Consent.php b/Classes/Domain/Model/Consent.php index f899bb94..4133d42e 100644 --- a/Classes/Domain/Model/Consent.php +++ b/Classes/Domain/Model/Consent.php @@ -31,6 +31,7 @@ * * @author Elias Häußler * @license GPL-2.0-or-later + * @final */ class Consent extends AbstractEntity { diff --git a/Classes/Event/ApproveConsentEvent.php b/Classes/Event/ApproveConsentEvent.php index e379e214..1a6766d5 100644 --- a/Classes/Event/ApproveConsentEvent.php +++ b/Classes/Event/ApproveConsentEvent.php @@ -32,10 +32,10 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ApproveConsentEvent +final class ApproveConsentEvent { - protected Consent $consent; - protected ?ResponseInterface $response = null; + private Consent $consent; + private ?ResponseInterface $response = null; public function __construct(Consent $consent) { diff --git a/Classes/Event/DismissConsentEvent.php b/Classes/Event/DismissConsentEvent.php index 1ddf1b75..a35ce63c 100644 --- a/Classes/Event/DismissConsentEvent.php +++ b/Classes/Event/DismissConsentEvent.php @@ -31,9 +31,9 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class DismissConsentEvent +final class DismissConsentEvent { - protected Consent $consent; + private Consent $consent; public function __construct(Consent $consent) { diff --git a/Classes/Event/GenerateHashEvent.php b/Classes/Event/GenerateHashEvent.php index 3688a47b..50300ca8 100644 --- a/Classes/Event/GenerateHashEvent.php +++ b/Classes/Event/GenerateHashEvent.php @@ -31,14 +31,14 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class GenerateHashEvent +final class GenerateHashEvent { /** * @var list */ - protected array $components; - protected Consent $consent; - protected ?string $hash = null; + private array $components; + private Consent $consent; + private ?string $hash = null; /** * @param list $components diff --git a/Classes/Event/ModifyConsentEvent.php b/Classes/Event/ModifyConsentEvent.php index 97493d8b..029ddaf7 100644 --- a/Classes/Event/ModifyConsentEvent.php +++ b/Classes/Event/ModifyConsentEvent.php @@ -31,9 +31,9 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ModifyConsentEvent +final class ModifyConsentEvent { - protected Consent $consent; + private Consent $consent; public function __construct(Consent $consent) { diff --git a/Classes/Event/ModifyConsentMailEvent.php b/Classes/Event/ModifyConsentMailEvent.php index d7997e32..ea903412 100644 --- a/Classes/Event/ModifyConsentMailEvent.php +++ b/Classes/Event/ModifyConsentMailEvent.php @@ -31,9 +31,9 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ModifyConsentMailEvent +final class ModifyConsentMailEvent { - protected FluidEmail $mail; + private FluidEmail $mail; public function __construct(FluidEmail $mail) { diff --git a/Classes/Form/Element/ConsentDataElement.php b/Classes/Form/Element/ConsentDataElement.php index 55cf467a..596c0f39 100644 --- a/Classes/Form/Element/ConsentDataElement.php +++ b/Classes/Form/Element/ConsentDataElement.php @@ -33,7 +33,7 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ConsentDataElement extends AbstractFormElement +final class ConsentDataElement extends AbstractFormElement { /** * @return array @@ -51,7 +51,7 @@ public function render(): array return $this->renderFormElement($result, $elementHtml); } - protected function renderRecordDataHtml(): string + private function renderRecordDataHtml(): string { $row = $this->data['databaseRow'] ?? []; $formData = (string)$row['data']; @@ -69,7 +69,7 @@ protected function renderRecordDataHtml(): string * @param array $result * @return array */ - protected function renderFormElement(array $result, string $elementHtml): array + private function renderFormElement(array $result, string $elementHtml): array { $fieldInformationResult = $this->renderFieldInformation(); $fieldInformationHtml = $fieldInformationResult['html']; @@ -90,7 +90,7 @@ protected function renderFormElement(array $result, string $elementHtml): array return $result; } - protected function renderAlert(string $localizationKey): string + private function renderAlert(string $localizationKey): string { $html[] = '