diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 57c673f6..9b19b312 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -1,26 +1,45 @@ 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" xdebug_enabled: false additional_hostnames: [] additional_fqdns: [] -mariadb_version: "10.3" -mysql_version: "" +database: + type: mariadb + version: "10.3" nfs_mount_enabled: false mutagen_enabled: false +hooks: + post-start: + - composer: environment:prepare + - exec-host: ddev import-files --src Tests/Acceptance/Data/Fileadmin omit_containers: [dba] -webimage_extra_packages: [php8.1-pcov] use_dns_when_possible: true -composer_version: "" +composer_version: "2" +disable_settings_management: true web_environment: +- XDEBUG_MODE=coverage +- TYPO3_CONTEXT=Development/Ddev +# DB configuration for functional tests - typo3DatabaseHost=db - typo3DatabaseUsername=root - typo3DatabasePassword=root - typo3DatabaseName=db +# Domain configuration +- TESTING_DOMAIN=$DDEV_HOSTNAME +# Environment variables for TYPO3 installation +- TYPO3_INSTALL_DB_USER=db +- TYPO3_INSTALL_DB_PASSWORD=db +- TYPO3_INSTALL_DB_HOST=db +- TYPO3_INSTALL_DB_USE_EXISTING=0 +- TYPO3_INSTALL_DB_DBNAME=db +- TYPO3_INSTALL_ADMIN_USER=admin +- TYPO3_INSTALL_ADMIN_PASSWORD=password +nodejs_version: "16" # Key features of ddev's config.yaml: @@ -31,21 +50,19 @@ web_environment: # docroot: # Relative path to the directory containing index.php. -# php_version: "7.4" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4" "8.0" +# php_version: "7.4" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1" -# You can explicitly specify the webimage, dbimage, dbaimage lines but this +# You can explicitly specify the webimage but this # is not recommended, as the images are often closely tied to ddev's' behavior, # so this can break upgrades. # webimage: # nginx/php docker image. -# dbimage: # mariadb docker image. -# dbaimage: -# mariadb_version and mysql_version -# ddev can use many versions of mariadb and mysql -# However these directives are mutually exclusive -# mariadb_version: 10.2 -# mysql_version: 8.0 +# database: +# type: # mysql, mariadb +# version: # database version, like "10.3" or "8.0" +# Note that mariadb_version or mysql_version from v1.18 and earlier +# will automatically be converted to this notation with just a "ddev config --auto" # router_http_port: # Port to be used for http (defaults to port 80) # router_https_port: # Port for https (defaults to 443) @@ -68,13 +85,23 @@ web_environment: # see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones # For example Europe/Dublin or MST7MDT -# composer_version: "" -# if composer_version:"" it will use the current ddev default composer release. +# composer_root: +# Relative path to the composer root directory from the project root. This is +# the directory which contains the composer.json and where all Composer related +# commands are executed. + +# composer_version: "2" +# if composer_version:"2" it will use the most recent composer v2 # It can also be set to "1", to get most recent composer v1 -# or "2" for most recent composer v2. +# or "" for the default v2 created at release time. # It can be set to any existing specific composer version. # After first project 'ddev start' this will not be updated until it changes +# nodejs_version: "16" +# change from the default system Node.js version to another supported version, like 12, 14, 17. +# Note that you can use 'ddev nvm' or nvm inside the web container to provide nearly any +# Node.js version, including v6, etc. + # additional_hostnames: # - somename # - someothername @@ -88,7 +115,7 @@ web_environment: # Please take care with this because it can cause great confusion. # upload_dir: custom/upload/dir -# would set the destination path for ddev import-files to custom/upload/dir. +# would set the destination path for ddev import-files to /custom/upload/dir # working_dir: # web: /var/www/html diff --git a/.ddev/docker-compose.selenium.yaml b/.ddev/docker-compose.selenium.yaml new file mode 100644 index 00000000..770604a7 --- /dev/null +++ b/.ddev/docker-compose.selenium.yaml @@ -0,0 +1,19 @@ +version: '3.6' + +services: + web: + depends_on: + - db + - selenium + selenium: + container_name: ddev-${DDEV_SITENAME}-selenium + image: selenium/standalone-chrome-debug:3.13.0-argon + networks: + default: + aliases: + - web + ddev_default: + volumes: + - /dev/shm:/dev/shm + external_links: + - ddev-router:$DDEV_HOSTNAME diff --git a/.gitattributes b/.gitattributes index e50beac9..bec5826b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,9 @@ /.gitignore export-ignore /.php-cs-fixer.php export-ignore /captainhook.json export-ignore +/codeception.yml export-ignore +/codecov.yml export-ignore +/dependency-checker.json export-ignore /packaging_exclude.php export-ignore /phpstan.neon export-ignore /phpstan-baseline.neon export-ignore @@ -15,5 +18,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/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index eaacb0b5..00000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Bug report -about: Create a report to help improve the extension -title: "[BUG]" -labels: bug -assignees: '' - ---- - -**Description** -A clear and concise description of what the bug is. - -**Steps to reproduce** -If possible, describe steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Environment** -- TYPO3 version: [e.g. 10.4.17] -- Extension version: [e.g. 0.3.2] -- Composer Mode: [yes, no] -- OS: [macOS 11.4, Windows 10] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..d5d33c94 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,85 @@ +name: Bug report +description: Create a report to help improve the extension. +title: "[BUG]" +labels: + - bug +assignees: + - eliashaeussler +body: + - type: input + id: typo3-version + attributes: + label: TYPO3 version + description: What TYPO3 version are you using? + placeholder: 'e.g. 10.4.17' + validations: + required: true + - type: input + id: php-version + attributes: + label: PHP version + description: What PHP version are you using? + placeholder: 'e.g. 7.4.27' + validations: + required: true + - type: input + id: extension-version + attributes: + label: Extension version + description: What version of EXT:form_consent are you using? + placeholder: 'e.g. 0.3.2' + validations: + required: true + - type: checkboxes + id: composer-mode + attributes: + label: Composer mode + description: Are you running TYPO3 in composer mode? + options: + - label: I'm running TYPO3 in composer mode. + required: true + - type: input + id: operating-system + attributes: + label: Operating system + description: What operating system are you using? + placeholder: 'e.g. macOS 11.4' + validations: + required: true + - type: textarea + attributes: + label: Current behavior + description: A clear and concise description of what the bug is. + - type: textarea + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + - type: textarea + attributes: + label: Steps to reproduce + description: If possible, describe steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + - type: textarea + attributes: + label: Additional context + description: Add any other context about the problem here. + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: | + By submitting this issue, you agree to follow our + [Code of Conduct](https://github.com/eliashaeussler/typo3-form-consent/blob/main/.github/CODE_OF_CONDUCT.md). + options: + - label: I agree to follow this project's Code of Conduct. + required: true + - type: markdown + attributes: + value: | + :bulb: **Tip:** Have you already looked into our + [Slack channel](https://typo3.slack.com/archives/C03719PJJJD)? Maybe your problem has + already been discussed there. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..9a75bab0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community support + url: https://typo3.slack.com/archives/C03719PJJJD + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 863c2d34..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this extension -title: "[FEATURE]" -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..aba15d2e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,47 @@ +name: Feature request +description: Suggest an idea for this extension. +title: "[FEATURE]" +labels: + - enhancement +assignees: + - eliashaeussler +body: + - type: textarea + attributes: + label: Is your feature request related to a problem? + description: A clear and concise description of what the problem is. + placeholder: I'm always frustrated when [...] + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + attributes: + label: Describe alternatives you've considered + description: | + A clear and concise description of any alternative solutions or features + you've considered. + - type: textarea + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: | + By submitting this issue, you agree to follow our + [Code of Conduct](https://github.com/eliashaeussler/typo3-form-consent/blob/main/.github/CODE_OF_CONDUCT.md). + options: + - label: I agree to follow this project's Code of Conduct. + required: true + - type: markdown + attributes: + value: | + :bulb: **Tip:** Have you already looked into our + [Slack channel](https://typo3.slack.com/archives/C03719PJJJD)? Maybe your problem has + already been discussed there. diff --git a/.github/workflows/cgl.yaml b/.github/workflows/cgl.yaml index 0d2f4e26..9a74fed7 100644 --- a/.github/workflows/cgl.yaml +++ b/.github/workflows/cgl.yaml @@ -3,7 +3,7 @@ on: [push, pull_request] jobs: cgl: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 strategy: fail-fast: false steps: @@ -16,15 +16,24 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: 8.1 - tools: composer:v2 + tools: composer:v2, composer-require-checker + + # Validation + - name: Validate composer.json + run: composer validate --no-check-lock # Install dependencies - name: Install Composer dependencies - run: composer install --no-progress + run: composer require --no-progress --no-plugins typo3/cms-dashboard:"~10.4.11 || ~11.5.0" typo3/cms-scheduler:"~10.4.11 || ~11.5.0" + + # Check Composer dependencies + - name: Check dependencies + run: composer-require-checker check --config-file dependency-checker.json + - run: composer install --no-progress # Linting - name: Lint composer.json - run: composer normalize --dry-run --no-check-lock --no-update-lock + run: composer lint:composer -- --dry-run - name: Lint Editorconfig run: .Build/bin/ec -e .Build - name: Lint PHP diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 18220bbf..e93a8d66 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,7 +8,7 @@ jobs: # Job: Create release release: if: startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 outputs: release-notes-url: ${{ steps.create-release.outputs.url }} steps: @@ -59,7 +59,7 @@ jobs: ter-publish: if: startsWith(github.ref, 'refs/tags/') needs: [release] - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 env: TYPO3_EXTENSION_KEY: form_consent TYPO3_API_TOKEN: ${{ secrets.TYPO3_API_TOKEN }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 101ddc86..5e127fa4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -4,27 +4,20 @@ on: [push, pull_request] jobs: tests: name: PHP ${{ matrix.php-version }} & TYPO3 ${{ matrix.typo3-version }} - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 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 - coverage: 1 - env: - typo3DatabaseName: typo3 - typo3DatabaseHost: '127.0.0.1' - typo3DatabaseUsername: root - typo3DatabasePassword: root + - 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: true steps: - uses: actions/checkout@v2 with: @@ -36,69 +29,71 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer:v2 - coverage: xdebug + coverage: none - # Start MySQL service - - name: Start MySQL - run: sudo /etc/init.d/mysql start - - # Define Composer cache - - name: Get Composer cache directory - id: composer-cache - run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" - - name: Define Composer cache - uses: actions/cache@v2 + # Setup DDEV + - name: Setup DDEV + uses: jonaseberle/github-action-setup-ddev@v1 with: - path: ${{ steps.composer-cache.outputs.dir }} - key: tests-php-${{ matrix.php-version }}-typo3-${{ matrix.typo3-version }} - restore-keys: | - tests-php-${{ matrix.php-version }}-typo3- + autostart: false + - name: Configure and start DDEV + run: | + ddev config --project-type=typo3 --php-version=${{ matrix.php-version }} --xdebug-enabled=true + ddev start # Install dependencies - name: Install TYPO3 and Composer dependencies - run: composer require typo3/cms-core:"^${{ matrix.typo3-version }}" --no-progress + run: ddev composer require typo3/cms-core:"^${{ matrix.typo3-version }}" --no-progress # Run tests - name: Run tests run: | - composer test:ci -- --coverage-text - composer test:ci:merge + ddev composer test:ci + ddev 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 - if: ${{ matrix.coverage }} - - name: Run SonarCloud scan - uses: SonarSource/sonarcloud-github-action@master + run: sed -i 's#/var/www/html#${{ github.workspace }}#g' clover.xml + if: ${{ matrix.coverage && github.event_name == 'pull_request' }} + - name: CodeClimate report + uses: paambaati/codeclimate-action@v3.0.0 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - if: ${{ matrix.coverage }} + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageLocations: | + ${{ github.workspace }}/.Build/log/coverage/clover.xml:clover + if: ${{ matrix.coverage && github.event_name == 'pull_request' }} + - name: codecov report + uses: codecov/codecov-action@v2 + with: + directory: .Build/log/coverage + fail_ci_if_error: true + verbose: true + if: ${{ matrix.coverage && github.event_name == 'pull_request' }} + + # Save acceptance reports + - uses: actions/upload-artifact@v3 + with: + name: acceptance-reports-${{ matrix.php-version }}-${{ matrix.typo3-version }} + path: .Build/log/acceptance-reports + if: failure() tests-lowest: name: '[test-lowest] PHP ${{ matrix.php-version }} & TYPO3 ${{ matrix.typo3-version }}' - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 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 - env: - typo3DatabaseName: typo3 - typo3DatabaseHost: '127.0.0.1' - typo3DatabaseUsername: root - typo3DatabasePassword: root + - 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" steps: - uses: actions/checkout@v2 with: @@ -110,29 +105,29 @@ jobs: with: php-version: ${{ matrix.php-version }} tools: composer:v2 - coverage: xdebug - - # Start MySQL service - - name: Start MySQL - run: sudo /etc/init.d/mysql start + coverage: none - # Define Composer cache - - name: Get Composer cache directory - id: composer-cache - run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" - - name: Define Composer cache - uses: actions/cache@v2 + # Setup DDEV + - name: Setup DDEV + uses: jonaseberle/github-action-setup-ddev@v1 with: - path: ${{ steps.composer-cache.outputs.dir }} - key: tests-lowest-php-${{ matrix.php-version }}-typo3-${{ matrix.typo3-version }} - restore-keys: | - tests-lowest-php-${{ matrix.php-version }}-typo3- + autostart: false + - name: Configure and start DDEV + run: | + ddev config --project-type=typo3 --php-version=${{ matrix.php-version }} + ddev start # Install dependencies - name: Install TYPO3 and Composer dependencies - run: composer require typo3/cms-core:"^${{ matrix.typo3-version }}" --prefer-lowest --no-progress + run: ddev composer require typo3/cms-core:"^${{ matrix.typo3-version }}" --prefer-lowest --no-progress # Run tests - name: Run tests - run: composer test:ci -- --coverage-text + run: ddev composer test + + # Save acceptance reports + - uses: actions/upload-artifact@v3 + with: + name: acceptance-reports-${{ matrix.php-version }}-${{ matrix.typo3-version }}-lowest + path: .Build/log/acceptance-reports + if: failure() diff --git a/.gitignore b/.gitignore index f800c277..1c36624f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ /.Build/ /config/ +/Tests/Acceptance/Support/_generated/ /var/ /.php-cs-fixer.cache /.phpunit.result.cache +/c3.php /composer.lock diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 97e88a85..90674fb2 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -22,7 +22,7 @@ EOM; $config = \TYPO3\CodingStandards\CsFixerConfig::create() - ->setHeader(sprintf($header, \date('Y')), true) + ->setHeader(sprintf($header, date('Y')), true) ->addRules([ 'native_function_invocation' => true, 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], diff --git a/Classes/Configuration/Configuration.php b/Classes/Configuration/Configuration.php new file mode 100644 index 00000000..6820732a --- /dev/null +++ b/Classes/Configuration/Configuration.php @@ -0,0 +1,78 @@ + + * + * 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\Configuration; + +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; +use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Core\Utility\ArrayUtility; +use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Configuration + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class Configuration +{ + /** + * @var array|null + */ + private static ?array $configuration = null; + + /** + * @return list + */ + public static function getExcludedElementsFromPersistence(): array + { + $configurationValue = self::get('persistence/excludedElements'); + + if (!\is_string($configurationValue)) { + return []; + } + + return GeneralUtility::trimExplode(',', $configurationValue, true); + } + + /** + * @return mixed|null + */ + private static function get(string $path) + { + if (null === self::$configuration) { + try { + self::$configuration = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(Extension::KEY); + } catch (Exception $e) { + self::$configuration = []; + } + } + + try { + return ArrayUtility::getValueByPath(self::$configuration, $path); + } catch (MissingArrayPathException $exception) { + return null; + } + } +} diff --git a/Classes/Configuration/Icon.php b/Classes/Configuration/Icon.php index 3fa0116d..bdf6fe92 100644 --- a/Classes/Configuration/Icon.php +++ b/Classes/Configuration/Icon.php @@ -34,10 +34,7 @@ */ final class Icon { - /** - * @var IconRegistry - */ - protected static $iconRegistry; + private static ?IconRegistry $iconRegistry = null; public static function forTable(string $tableName, string $type = 'svg'): string { @@ -100,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'; @@ -112,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/Configuration/Localization.php b/Classes/Configuration/Localization.php index d9c96c2d..081072a9 100644 --- a/Classes/Configuration/Localization.php +++ b/Classes/Configuration/Localization.php @@ -23,6 +23,7 @@ namespace EliasHaeussler\Typo3FormConsent\Configuration; +use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Localization\LanguageService; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; @@ -81,6 +82,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); @@ -125,7 +131,7 @@ private static function buildLocalizationString( string $localizationKey = null, string $language = null ): string { - $fileName = isset(self::FILES[$type]) ? self::FILES[$type] : self::FILES[self::TYPE_DEFAULT]; + $fileName = self::FILES[$type] ?? self::FILES[self::TYPE_DEFAULT]; $language = $language ? ($language . '.') : ''; $localizationKey = $localizationKey ? (':' . $localizationKey) : ''; $extensionKey = self::isCoreType($type) ? 'core' : Extension::KEY; @@ -149,6 +155,10 @@ private static function isCoreType(string $type): bool private static function isEnvironmentInFrontendMode(): bool { + if (isset($GLOBALS['TYPO3_REQUEST'])) { + return ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend(); + } + return isset($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController; } diff --git a/Classes/Controller/ConsentController.php b/Classes/Controller/ConsentController.php index c628b502..7efa6bb8 100644 --- a/Classes/Controller/ConsentController.php +++ b/Classes/Controller/ConsentController.php @@ -26,7 +26,11 @@ 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\ImmediateResponseException; +use TYPO3\CMS\Core\Http\Stream; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException; use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException; @@ -38,29 +42,28 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ConsentController extends ActionController +final class ConsentController extends ActionController { - /** - * @var ConsentRepository - */ - protected $consentRepository; - - /** - * @var PersistenceManagerInterface - */ - protected $persistenceManager; - - public function __construct(ConsentRepository $consentRepository, PersistenceManagerInterface $persistenceManager) - { + private ConsentRepository $consentRepository; + private PersistenceManagerInterface $persistenceManager; + private StringableResponseFactory $stringableResponseFactory; + + public function __construct( + ConsentRepository $consentRepository, + PersistenceManagerInterface $persistenceManager, + StringableResponseFactory $stringableResponseFactory + ) { $this->consentRepository = $consentRepository; $this->persistenceManager = $persistenceManager; + $this->stringableResponseFactory = $stringableResponseFactory; } /** * @throws IllegalObjectTypeException + * @throws ImmediateResponseException * @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 +72,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 +118,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 +140,57 @@ 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 + /** + * @throws ImmediateResponseException + */ + private function createErrorResponse(string $reason): ResponseInterface { $this->view->assign('error', true); $this->view->assign('reason', $reason); - return $this->renderViewAsResponse(); + return $this->createHtmlResponse(); } - protected function renderViewAsResponse(): ?ResponseInterface + /** + * @throws ImmediateResponseException + */ + private function createHtmlResponse(ResponseInterface $previous = null): ResponseInterface { + if (null === $previous) { + return $this->createResponse(); + } + + if ($previous->getStatusCode() >= 300) { + // @todo Use PropagateResponseException once v10 support is dropped + throw new ImmediateResponseException($previous, 1645646663); + } + + $content = (string)$previous->getBody(); + + if ('' !== trim($content)) { + return $this->createResponse($content); + } + + return $this->createResponse(); + } + + private 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; + // @todo Remove once v10 support is dropped + $body = new Stream('php://temp', 'r+'); + $body->write($html ?? $this->view->render()); + + // TYPO3 v10 + return $this->stringableResponseFactory->createResponse() + ->withHeader('Content-Type', 'text/html; charset=utf-8') + ->withBody($body); } } 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/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..808a4584 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; /** @@ -61,101 +48,40 @@ * @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; - /** - * @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; - } + private ConsentFactory $consentFactory; + private EventDispatcherInterface $eventDispatcher; + private Mailer $mailer; + private PersistenceManagerInterface $persistenceManager; + // @todo Move to constructor once v10 support is dropped public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void { $this->eventDispatcher = $eventDispatcher; } - public function injectConsentRepository(ConsentRepository $consentRepository): void + // @todo Move to constructor once v10 support is dropped + public function injectMailer(Mailer $mailer): void { - $this->consentRepository = $consentRepository; - } - - public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void - { - $this->configurationManager = $configurationManager; - $this->configuration = $this->configurationManager->getConfiguration( - ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, - Extension::NAME - ); + $this->mailer = $mailer; } + // @todo Move to constructor once v10 support is dropped public function injectPersistenceManager(PersistenceManagerInterface $persistenceManager): void { $this->persistenceManager = $persistenceManager; } - public function injectFlashMessageFinisher(FlashMessageFinisher $flashMessageFinisher): void + public function __construct(string $finisherIdentifier = '') { - $this->flashMessageFinisher = $flashMessageFinisher; + /* @phpstan-ignore-next-line */ + parent::__construct($finisherIdentifier); - 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; + // @todo Use dependency injection once v10 support is dropped + $this->consentFactory = GeneralUtility::makeInstance(ConsentFactory::class); } /** @@ -174,80 +100,26 @@ protected function executeInternal(): ?string /** * @throws FinisherException - * @throws IllegalObjectTypeException * @throws \Exception */ - protected function executeConsent(): void + private 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); + $finisherOptions = new FinisherOptions(fn (string $optionName) => $this->parseOption($optionName)); - // Build domain model - $consent = GeneralUtility::makeInstance(Consent::class) - ->setEmail($recipientAddress) - ->setDate($date) - ->setData($data) - ->setFormPersistenceIdentifier($formPersistenceIdentifier) - ->setValidUntil($validUntil); - - // 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 @@ -262,8 +134,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), @@ -272,121 +143,16 @@ protected function executeConsent(): void } } - protected function resolveSubject(string $subject): string + private 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,125 +163,21 @@ 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 + private function addFlashMessage(\Exception $exception): 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, ]); - $this->flashMessageFinisher->execute($this->finisherContext); + $flashMessageFinisher->execute($this->finisherContext); // Cancel execution - if ($cancel) { - $this->finisherContext->cancel(); - } + $this->finisherContext->cancel(); } - protected function getServerRequest(): ?ServerRequestInterface + private function getServerRequest(): ?ServerRequestInterface { return $GLOBALS['TYPO3_REQUEST'] ?? null; } diff --git a/Classes/Domain/Finishers/FinisherOptions.php b/Classes/Domain/Finishers/FinisherOptions.php new file mode 100644 index 00000000..76393e61 --- /dev/null +++ b/Classes/Domain/Finishers/FinisherOptions.php @@ -0,0 +1,284 @@ + + * + * 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 +{ + private static ?PageRepository $pageRepository = null; + + /** + * @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..4133d42e 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; /** @@ -30,6 +31,7 @@ * * @author Elias Häußler * @license GPL-2.0-or-later + * @final */ class Consent extends AbstractEntity { @@ -46,15 +48,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 +109,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 +137,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..1a6766d5 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 @@ -31,12 +32,10 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ApproveConsentEvent +final class ApproveConsentEvent { - /** - * @var Consent - */ - protected $consent; + private Consent $consent; + private ?ResponseInterface $response = null; public function __construct(Consent $consent) { @@ -47,4 +46,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/DismissConsentEvent.php b/Classes/Event/DismissConsentEvent.php index 8a8a75d9..a35ce63c 100644 --- a/Classes/Event/DismissConsentEvent.php +++ b/Classes/Event/DismissConsentEvent.php @@ -31,12 +31,9 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class DismissConsentEvent +final class DismissConsentEvent { - /** - * @var Consent - */ - protected $consent; + private Consent $consent; public function __construct(Consent $consent) { diff --git a/Classes/Event/GenerateHashEvent.php b/Classes/Event/GenerateHashEvent.php index 19ed1440..50300ca8 100644 --- a/Classes/Event/GenerateHashEvent.php +++ b/Classes/Event/GenerateHashEvent.php @@ -31,25 +31,17 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class GenerateHashEvent +final class GenerateHashEvent { /** - * @var mixed[] + * @var list */ - protected $components; + private array $components; + private Consent $consent; + private ?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/Listener/InvokeFinishersOnConsentApprovalListener.php b/Classes/Event/Listener/InvokeFinishersOnConsentApprovalListener.php new file mode 100644 index 00000000..ad843926 --- /dev/null +++ b/Classes/Event/Listener/InvokeFinishersOnConsentApprovalListener.php @@ -0,0 +1,177 @@ + + * + * 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; + } + + // Backup original server request object + // @todo Remove once v10 support is dropped + $originalRequest = $GLOBALS['TYPO3_REQUEST']; + $GLOBALS['TYPO3_REQUEST'] = $serverRequest; + + // Build extbase bootstrap object + $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); + // @todo Enable third parameter once v10 support is dropped + $contentObjectRenderer->start($contentElementRecord, 'tt_content'/**, $serverRequest */); + $contentObjectRenderer->setUserObjectType(ContentObjectRenderer::OBJECTTYPE_USER_INT); + $bootstrap = GeneralUtility::makeInstance(Bootstrap::class); + + if (method_exists($bootstrap, 'setContentObjectRenderer')) { + $bootstrap->setContentObjectRenderer($contentObjectRenderer); + } else { + // @todo Remove once v10 support is dropped + /* @phpstan-ignore-next-line */ + $bootstrap->cObj = $contentObjectRenderer; + } + + $configuration = [ + 'extensionName' => 'Form', + 'pluginName' => 'Formframework', + ]; + + try { + // Dispatch extbase request + // @todo Enable third parameter once v10 support is dropped + $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(); + } finally { + // Restore original server request object + $GLOBALS['TYPO3_REQUEST'] = $originalRequest; + } + } + + /** + * @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/Event/ModifyConsentEvent.php b/Classes/Event/ModifyConsentEvent.php index 9bfed85c..029ddaf7 100644 --- a/Classes/Event/ModifyConsentEvent.php +++ b/Classes/Event/ModifyConsentEvent.php @@ -31,12 +31,9 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ModifyConsentEvent +final class ModifyConsentEvent { - /** - * @var Consent - */ - protected $consent; + private Consent $consent; public function __construct(Consent $consent) { diff --git a/Classes/Event/ModifyConsentMailEvent.php b/Classes/Event/ModifyConsentMailEvent.php index 2557a0a2..ea903412 100644 --- a/Classes/Event/ModifyConsentMailEvent.php +++ b/Classes/Event/ModifyConsentMailEvent.php @@ -31,12 +31,9 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ModifyConsentMailEvent +final class ModifyConsentMailEvent { - /** - * @var FluidEmail - */ - protected $mail; + private FluidEmail $mail; public function __construct(FluidEmail $mail) { 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..8f7fb339 --- /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 (): void { + // 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 (): void { + // 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..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']; @@ -85,17 +85,17 @@ protected function renderFormElement(array $result, string $elementHtml): array $html[] = ''; $html[] = ''; - $result['html'] = implode(LF, $html); + $result['html'] = implode(PHP_EOL, $html); return $result; } - protected function renderAlert(string $localizationKey): string + private function renderAlert(string $localizationKey): string { $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..9576a12a --- /dev/null +++ b/Classes/Http/StringableResponse.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\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 + { + if (null === $this->body) { + return ''; + } + + return $this->body->__toString(); + } +} diff --git a/Classes/Http/StringableResponseFactory.php b/Classes/Http/StringableResponseFactory.php new file mode 100644 index 00000000..6ee20b73 --- /dev/null +++ b/Classes/Http/StringableResponseFactory.php @@ -0,0 +1,54 @@ + + * + * 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 +{ + private bool $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..4c20a5cf --- /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 array $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..ce8258aa --- /dev/null +++ b/Classes/Registry/Dto/ConsentState.php @@ -0,0 +1,55 @@ + + * + * 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 +{ + private Consent $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..90890b58 100644 --- a/Classes/Service/HashService.php +++ b/Classes/Service/HashService.php @@ -34,12 +34,9 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class HashService +final class HashService { - /** - * @var EventDispatcherInterface - */ - protected $eventDispatcher; + private EventDispatcherInterface $eventDispatcher; public function __construct(EventDispatcherInterface $eventDispatcher) { @@ -50,9 +47,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..c75e4fc7 --- /dev/null +++ b/Classes/Type/Transformer/FormRequestTypeTransformer.php @@ -0,0 +1,112 @@ + + * + * 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 Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Http\ServerRequestFactory; +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 +{ + private HashService $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, NULL given.', 1646044629); + } + + // @todo Replace with $formRuntime->getRequest() once v10 support is dropped + $request = $this->getServerRequest(); + + // Handle submitted form values + $requestParameters = []; + if (\is_array($request->getParsedBody())) { + $requestParameters = $request->getParsedBody(); + } + + // Handle uploaded files + $uploadedFiles = $request->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()), + ], + ]; + } + + private function getServerRequest(): ServerRequestInterface + { + return $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + } + + 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..7ea23c48 --- /dev/null +++ b/Classes/Type/Transformer/FormValuesTypeTransformer.php @@ -0,0 +1,90 @@ + + * + * 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\Configuration\Configuration; +use EliasHaeussler\Typo3FormConsent\Type\JsonType; +use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference; +use TYPO3\CMS\Extbase\Domain\Model\FileReference as ExtbaseFileReference; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; + +/** + * FormValuesTypeTransformer + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class FormValuesTypeTransformer implements TypeTransformerInterface +{ + /** + * @return JsonType + * @throws \JsonException + */ + public function transform(FormRuntime $formRuntime = null): JsonType + { + if (null === $formRuntime) { + throw new \InvalidArgumentException('Expected a valid FormRuntime object, NULL given.', 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(); + + foreach ($formValues as $elementIdentifier => $value) { + // Remove excluded elements + if ($this->isElementExcluded($elementIdentifier, $formRuntime)) { + unset($formValues[$elementIdentifier]); + continue; + } + + // Resolve file references + if ($value instanceof ExtbaseFileReference) { + $value = $value->getOriginalResource(); + } + if ($value instanceof CoreFileReference) { + $formValues[$elementIdentifier] = $value->getOriginalFile()->getUid(); + } + } + + return JsonType::fromArray($formValues); + } + + private function isElementExcluded(string $elementIdentifier, FormRuntime $formRuntime): bool + { + $excludedElements = Configuration::getExcludedElementsFromPersistence(); + $element = $formRuntime->getFormDefinition()->getElementByIdentifier($elementIdentifier); + + return null !== $element && \in_array($element->getType(), $excludedElements, true); + } + + public static function getName(): string + { + return 'formValues'; + } +} diff --git a/Classes/Type/Transformer/TypeTransformerFactory.php b/Classes/Type/Transformer/TypeTransformerFactory.php new file mode 100644 index 00000000..0421f635 --- /dev/null +++ b/Classes/Type/Transformer/TypeTransformerFactory.php @@ -0,0 +1,61 @@ + + * + * 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 +{ + private ServiceLocator $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/Classes/Widget/ApprovedConsentsWidget.php b/Classes/Widget/ApprovedConsentsWidget.php index 04577c0e..3d2f128b 100644 --- a/Classes/Widget/ApprovedConsentsWidget.php +++ b/Classes/Widget/ApprovedConsentsWidget.php @@ -31,6 +31,6 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ApprovedConsentsWidget extends DoughnutChartWidget +final class ApprovedConsentsWidget extends DoughnutChartWidget { } diff --git a/Classes/Widget/Provider/ConsentChartDataProvider.php b/Classes/Widget/Provider/ConsentChartDataProvider.php index a68e50bc..b2a95542 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) { @@ -49,7 +46,7 @@ public function __construct(Connection $connection) } /** - * @return array{labels: array, datasets: array{0: array{backgroundColor: array, data: array}}} + * @return array{labels: list, datasets: array{array{backgroundColor: list, data: list}}} */ public function getChartData(): array { 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..5a5c3ee1 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) { +return static function (ContainerConfigurator $containerConfigurator, ContainerBuilder $container): void { + $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..92772503 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -8,8 +8,29 @@ services: resource: '../Classes/*' exclude: - '../Classes/DependencyInjection/*' + - '../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 + EliasHaeussler\Typo3FormConsent\Domain\Factory\ConsentFactory: + public: true + + # @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' } + + 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/README.md b/README.md index 7afacd92..150fbd9f 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,16 @@ # 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/main/graph/badge.svg?token=PQ0101QE3S)](https://codecov.io/gh/eliashaeussler/typo3-form-consent) +[![Maintainability](https://api.codeclimate.com/v1/badges/c88c6c0bbc31c02153ef/maintainability)](https://codeclimate.com/github/eliashaeussler/typo3-form-consent/maintainability) [![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) +[![Release](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/release.yaml/badge.svg)](https://github.com/eliashaeussler/typo3-form-consent/actions/workflows/release.yaml) +[![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)](https://extensions.typo3.org/extension/form_consent) +[![Downloads](https://shields.io/endpoint?url=https://typo3-badges.dev/badge/form_consent/downloads/shields)](https://extensions.typo3.org/extension/form_consent) +[![Extension stability](https://shields.io/endpoint?url=https://typo3-badges.dev/badge/form_consent/stability/shields)](https://extensions.typo3.org/extension/form_consent) +[![TYPO3 badge](https://shields.io/endpoint?url=https://typo3-badges.dev/badge/typo3/shields)](https://typo3.org/) :package: [Packagist](https://packagist.org/packages/eliashaeussler/typo3-form-consent) | :hatched_chick: [TYPO3 extension repository](https://extensions.typo3.org/extension/form_consent) | @@ -29,10 +34,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 @@ -52,8 +58,9 @@ finisher settings. ## :open_file_folder: Configuration -Only the TypoScript setup under `EXT:form_consent/Configuration/TypoScript` +The TypoScript setup under `EXT:form_consent/Configuration/TypoScript` needs to be included and the required database changes need to be made. +Additionally, an extension configuration is provided. ### TypoScript @@ -61,10 +68,10 @@ The following TypoScript constants are available: | TypoScript constant | Description | Required | Default | |---------------------|-------------|----------|---------| -| **`plugin.tx_formconsent.persistence.storagePid`** | Default storage PID for new consents | :x: | `0` | -| **`plugin.tx_formconsent.view.templateRootPath`** | Path to template root for consent mail and validation plugin | :x: | – | -| **`plugin.tx_formconsent.view.partialRootPath`** | Path to template partials for consent mail and validation plugin | :x: | – | -| **`plugin.tx_formconsent.view.layoutRootPath`** | Path to template layouts for consent mail and validation plugin | :x: | – | +| **`plugin.tx_formconsent.persistence.storagePid`** | Default storage PID for new consents | – | `0` | +| **`plugin.tx_formconsent.view.templateRootPath`** | Path to template root for consent mail and validation plugin | – | – | +| **`plugin.tx_formconsent.view.partialRootPath`** | Path to template partials for consent mail and validation plugin | – | – | +| **`plugin.tx_formconsent.view.layoutRootPath`** | Path to template layouts for consent mail and validation plugin | – | – | ### Finisher options @@ -72,23 +79,131 @@ The following options are available to the `Consent` finisher: | Finisher option | Description | Required | Default | |-----------------|-------------|----------|---------| -| **`subject`** | Mail subject | :x: | `Approve your consent` | +| **`subject`** | Mail subject | – | `Approve your consent` | | **`recipientAddress`** | Recipient e-mail address | :white_check_mark: | – | -| **`recipientName`** | Recipient name | :x: | – | -| **`senderAddress`** | Sender e-mail address | :x: | _System default sender e-mail address_ | -| **`senderName`** | Sender name | :x: | _System default sender name_ | +| **`recipientName`** | Recipient name | – | – | +| **`senderAddress`** | Sender e-mail address | – | _System default sender e-mail address_ | +| **`senderName`** | Sender name | – | _System default sender name_ | | **`approvalPeriod`** | Approval period | :white_check_mark: | `86400` (1 day), `0` = unlimited | -| **`showDismissLink`** | Show dismiss link in consent mail | :x: | `false` | +| **`showDismissLink`** | Show dismiss link in consent mail | – | `false` | | **`confirmationPid`** | Confirmation page (contains plugin) | :white_check_mark: | – | -| **`storagePid`** | Storage page | :x: | `plugin.tx_formconsent.persistence.storagePid` | -| **`templateRootPaths`** | Additional paths to template root | :x: | – | -| **`partialRootPaths`** | Additional paths to template partials | :x: | – | -| **`layoutRootPaths`** | Additional paths to template layouts | :x: | – | +| **`storagePid`** | Storage page | – | `plugin.tx_formconsent.persistence.storagePid` | +| **`templateRootPaths`** | Additional paths to template root | – | – | +| **`partialRootPaths`** | Additional paths to template partials | – | – | +| **`layoutRootPaths`** | Additional paths to template layouts | – | – | -**Note:** Template paths that are configured via form finisher +:bulb: **Note:** Template paths that are configured via form finisher options are only applied to the appropriate form. They are merged with the default template paths configured via TypoScript. +### Extension configuration + +The following extension configuration options are available: + +| Configuration key | Description | Required | Default | +|-------------------|-------------|----------|---------| +| **`persistence.excludedElements`** | Form element types to be excluded from persistence (comma-separated list) | – | `Honeypot` | + +## :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. + +## :construction: Migration + +### 0.2.x → 0.3.0 + +#### Post-consent approval finishers + +Custom finishers can now be executed after consent was approved. + +* Database field `tx_formconsent_domain_model_consent.original_request_parameters` was added. + - Manual migration required. + - Database field should contain an JSON-encoded string of the parsed body sent with the original + form submit request. +* Database field `tx_formconsent_domain_model_consent.original_content_element_uid` was added. + - Manual migration required. + - Database field should contain the content element UID of the original form plugin. +* Post-approval finishers can now be defined [as described above](#invoke-finishers-on-consent-approval). + - Manual migration required. + - Create form variants and configure the post-approval finishers. + +#### [`Consent`][1] model + +Form values are now represented as an instance of [`JsonType`][2]. + +* Method `getDataArray()` was removed. + - Use `getData()->toArray()` instead. +* Return type of `getData()` was changed to `JsonType|null`. + - If you need the JSON-encoded string, use `json_encode($consent->getData())` instead. +* Parameter `$data` of `setData()` was changed to `JsonType|null`. + - If you need to pass a JSON-encoded string, use `$consent->setData(new JsonType($json))` instead. + - If you need to pass a JSON-decoded array, use `$consent->setData(JsonType::fromArray($array))` instead. + +#### Codebase + +* Minimum PHP version was raised to PHP 7.4. + - Upgrade your codebase to support at least PHP 7.4. +* Several classes were marked as `final`. + - If you still need to extend or override them, consider refactoring + your code or [submit an issue][3]. + ## :gem: Credits Icons made by [Google](https://www.flaticon.com/authors/google) from @@ -97,3 +212,9 @@ 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) + +[1]: Classes/Domain/Model/Consent.php +[2]: Classes/Type/JsonType.php +[3]: https://github.com/eliashaeussler/typo3-form-consent/issues 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..86881af1 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. diff --git a/Tests/Acceptance/Backend/ConsentDataElementCest.php b/Tests/Acceptance/Backend/ConsentDataElementCest.php new file mode 100644 index 00000000..da713be6 --- /dev/null +++ b/Tests/Acceptance/Backend/ConsentDataElementCest.php @@ -0,0 +1,49 @@ + + * + * 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\Acceptance\Backend; + +use EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\AcceptanceTester; +use EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\Helper\Backend; + +/** + * ConsentDataElementCest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class ConsentDataElementCest +{ + public function canSeeConsentInBackendListModule(AcceptanceTester $I, Backend $backend): void + { + $I->amOnPage('/'); + $I->fillAndSubmitForm(); + + $backend->login(); + $backend->openModule('#web_list'); + + $I->seeElement('#t3-table-tx_formconsent_domain_model_consent'); + $I->click('tr[data-table="tx_formconsent_domain_model_consent"]:first-child td.col-title a'); + $I->waitForText('Submitted form data', 5); + } +} diff --git a/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-confirmation-variant.form.yaml b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-confirmation-variant.form.yaml new file mode 100755 index 00000000..405d292d --- /dev/null +++ b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-confirmation-variant.form.yaml @@ -0,0 +1,63 @@ + +renderingOptions: + submitButtonLabel: Submit +identifier: contact-confirmation-variant +label: 'contact confirmation variant' +type: Form +prototypeName: standard +variants: + - + identifier: variant-1 + condition: isConsentApproved() + finishers: + - + options: + message: 'Thanks for your consent.' + contentElementUid: '' + identifier: Confirmation +finishers: + - + options: + subject: '' + recipientAddress: '{email-1}' + recipientName: '' + senderAddress: '' + senderName: '' + approvalPeriod: '86400' + showDismissLink: true + confirmationPid: '2' + storagePid: '' + identifier: Consent + - + options: + message: 'Please approve your consent.' + contentElementUid: '' + identifier: Confirmation +renderables: + - + renderingOptions: + previousButtonLabel: 'Previous step' + nextButtonLabel: 'Next step' + identifier: page-1 + label: 'Contact Form' + type: Page + renderables: + - + defaultValue: '' + type: Email + identifier: email-1 + label: 'Email address' + properties: + fluidAdditionalAttributes: + required: required + validators: + - + identifier: EmailAddress + - + identifier: NotEmpty + - + properties: + saveToFileMount: '1:/user_upload/' + type: FileUpload + identifier: fileupload-1 + label: 'File upload' diff --git a/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-email-variant.form.yaml b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-email-variant.form.yaml new file mode 100755 index 00000000..938103cc --- /dev/null +++ b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-email-variant.form.yaml @@ -0,0 +1,72 @@ + +renderingOptions: + submitButtonLabel: Submit +identifier: contact-email-variant +label: 'contact email variant' +type: Form +prototypeName: standard +variants: + - + identifier: variant-1 + condition: isConsentApproved() + finishers: + - + options: + subject: 'Consent approved' + recipients: + admin@example.com: '' + senderAddress: info@example.com + senderName: '' + addHtmlPart: true + attachUploads: true + translation: + language: Default + useFluidEmail: true + title: '' + identifier: EmailToReceiver +finishers: + - + options: + subject: '' + recipientAddress: '{email-1}' + recipientName: '' + senderAddress: '' + senderName: '' + approvalPeriod: '86400' + showDismissLink: true + confirmationPid: '2' + storagePid: '' + identifier: Consent + - + options: + message: 'Please approve your consent.' + contentElementUid: '' + identifier: Confirmation +renderables: + - + renderingOptions: + previousButtonLabel: 'Previous step' + nextButtonLabel: 'Next step' + identifier: page-1 + label: 'Contact Form' + type: Page + renderables: + - + defaultValue: '' + type: Email + identifier: email-1 + label: 'Email address' + properties: + fluidAdditionalAttributes: + required: required + validators: + - + identifier: EmailAddress + - + identifier: NotEmpty + - + properties: + saveToFileMount: '1:/user_upload/' + type: FileUpload + identifier: fileupload-1 + label: 'File upload' diff --git a/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-invalid.form.yaml b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-invalid.form.yaml new file mode 100755 index 00000000..9f12de3b --- /dev/null +++ b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-invalid.form.yaml @@ -0,0 +1,55 @@ + +renderingOptions: + submitButtonLabel: Submit +identifier: contact-invalid +label: 'contact invalid' +type: Form +prototypeName: standard +finishers: + - + options: + subject: '' + recipientAddress: '' + recipientName: '' + senderAddress: '' + senderName: '' + approvalPeriod: '86400' + showDismissLink: true + confirmationPid: '2' + storagePid: '' + identifier: Consent + - + options: + message: 'Please approve your consent.' + contentElementUid: '' + identifier: Confirmation +renderables: + - + renderingOptions: + previousButtonLabel: 'Previous step' + nextButtonLabel: 'Next step' + identifier: page-1 + label: 'Contact Form' + type: Page + renderables: + - + defaultValue: '' + type: Email + identifier: email-1 + label: 'Email address' + properties: + fluidAdditionalAttributes: + required: required + validators: + - + identifier: EmailAddress + - + identifier: NotEmpty + - + properties: + saveToFileMount: '1:/user_upload/' + allowedMimeTypes: + - image/png + type: FileUpload + identifier: fileupload-1 + label: 'File upload' diff --git a/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-redirect-variant.form.yaml b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-redirect-variant.form.yaml new file mode 100755 index 00000000..7f0ba5bd --- /dev/null +++ b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-redirect-variant.form.yaml @@ -0,0 +1,63 @@ + +renderingOptions: + submitButtonLabel: Submit +identifier: contact-redirect-variant +label: 'contact redirect variant' +type: Form +prototypeName: standard +variants: + - + identifier: variant-1 + condition: isConsentApproved() + finishers: + - + options: + pageUid: '1' + additionalParameters: '' + identifier: Redirect +finishers: + - + options: + subject: '' + recipientAddress: '{email-1}' + recipientName: '' + senderAddress: '' + senderName: '' + approvalPeriod: '86400' + showDismissLink: true + confirmationPid: '2' + storagePid: '' + identifier: Consent + - + options: + message: 'Please approve your consent.' + contentElementUid: '' + identifier: Confirmation +renderables: + - + renderingOptions: + previousButtonLabel: 'Previous step' + nextButtonLabel: 'Next step' + identifier: page-1 + label: 'Contact Form' + type: Page + renderables: + - + defaultValue: '' + type: Email + identifier: email-1 + label: 'Email address' + properties: + fluidAdditionalAttributes: + required: required + validators: + - + identifier: EmailAddress + - + identifier: NotEmpty + - + properties: + saveToFileMount: '1:/user_upload/' + type: FileUpload + identifier: fileupload-1 + label: 'File upload' diff --git a/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-v2.form.yaml b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-v2.form.yaml new file mode 100755 index 00000000..e5faa8f0 --- /dev/null +++ b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact-v2.form.yaml @@ -0,0 +1,55 @@ + +renderingOptions: + submitButtonLabel: Submit +identifier: contact-v2 +label: 'contact v2' +type: Form +prototypeName: standard +finishers: + - + options: + subject: '' + recipientAddress: '{email-1}' + recipientName: '' + senderAddress: 'sender@example.com' + senderName: '' + approvalPeriod: '86400' + showDismissLink: true + confirmationPid: '2' + storagePid: '' + identifier: Consent + - + options: + message: 'Please approve your consent.' + contentElementUid: '' + identifier: Confirmation +renderables: + - + renderingOptions: + previousButtonLabel: 'Previous step' + nextButtonLabel: 'Next step' + identifier: page-1 + label: 'Contact Form' + type: Page + renderables: + - + defaultValue: '' + type: Email + identifier: email-1 + label: 'Email address' + properties: + fluidAdditionalAttributes: + required: required + validators: + - + identifier: EmailAddress + - + identifier: NotEmpty + - + properties: + saveToFileMount: '1:/user_upload/' + allowedMimeTypes: + - image/png + type: FileUpload + identifier: fileupload-1 + label: 'File upload' diff --git a/Tests/Acceptance/Data/Fileadmin/form_definitions/contact.form.yaml b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact.form.yaml new file mode 100755 index 00000000..3e2901f4 --- /dev/null +++ b/Tests/Acceptance/Data/Fileadmin/form_definitions/contact.form.yaml @@ -0,0 +1,55 @@ + +renderingOptions: + submitButtonLabel: Submit +identifier: contact +label: contact +type: Form +prototypeName: standard +finishers: + - + options: + subject: '' + recipientAddress: '{email-1}' + recipientName: '' + senderAddress: '' + senderName: '' + approvalPeriod: '86400' + showDismissLink: true + confirmationPid: '2' + storagePid: '' + identifier: Consent + - + options: + message: 'Please approve your consent.' + contentElementUid: '' + identifier: Confirmation +renderables: + - + renderingOptions: + previousButtonLabel: 'Previous step' + nextButtonLabel: 'Next step' + identifier: page-1 + label: 'Contact Form' + type: Page + renderables: + - + defaultValue: '' + type: Email + identifier: email-1 + label: 'Email address' + properties: + fluidAdditionalAttributes: + required: required + validators: + - + identifier: EmailAddress + - + identifier: NotEmpty + - + properties: + saveToFileMount: '1:/user_upload/' + allowedMimeTypes: + - image/png + type: FileUpload + identifier: fileupload-1 + label: 'File upload' diff --git a/Tests/Acceptance/Data/Fixtures/be_users.sql b/Tests/Acceptance/Data/Fixtures/be_users.sql new file mode 100644 index 00000000..bc9ad4d1 --- /dev/null +++ b/Tests/Acceptance/Data/Fixtures/be_users.sql @@ -0,0 +1,9 @@ +SET @username := 'admin'; +SET @password := '$argon2i$v=19$m=65536,t=16,p=1$UXBGdzN4dTRjNkRDS1FCOQ$l0yX4DO/Zd3wGhvppCeZJeITX/p1dpv36swzyydBoVY'; + +DELETE +FROM `be_users` +WHERE `username` COLLATE utf8mb4_general_ci = @username; + +INSERT INTO `be_users` (`username`, `password`, `admin`) +VALUES (@username, @password, 1); diff --git a/Tests/Acceptance/Data/Fixtures/pages.sql b/Tests/Acceptance/Data/Fixtures/pages.sql new file mode 100644 index 00000000..ffdc80f9 --- /dev/null +++ b/Tests/Acceptance/Data/Fixtures/pages.sql @@ -0,0 +1,5 @@ +DELETE FROM `pages`; + +INSERT INTO `pages` (`uid`, `pid`, `deleted`, `hidden`, `title`, `slug`, `doktype`, `is_siteroot`) +VALUES (1, 0, 0, 0, 'Home', '/', 1, 1), + (2, 1, 0, 0, 'Confirmation', '/confirmation', 1, 0); diff --git a/Tests/Acceptance/Data/Fixtures/sys_file.sql b/Tests/Acceptance/Data/Fixtures/sys_file.sql new file mode 100644 index 00000000..e72d4d0b --- /dev/null +++ b/Tests/Acceptance/Data/Fixtures/sys_file.sql @@ -0,0 +1,2 @@ +DELETE FROM `sys_file`; +ALTER TABLE `sys_file` AUTO_INCREMENT = 1; diff --git a/Tests/Acceptance/Data/Fixtures/sys_template.sql b/Tests/Acceptance/Data/Fixtures/sys_template.sql new file mode 100644 index 00000000..3e8b4b34 --- /dev/null +++ b/Tests/Acceptance/Data/Fixtures/sys_template.sql @@ -0,0 +1,4 @@ +DELETE FROM `sys_template`; + +INSERT INTO `sys_template` (`uid`, `pid`, `deleted`, `hidden`, `title`, `root`, `clear`, `include_static_file`, `constants`, `config`) +VALUES (1, 1, 0, 0, 'Root', 1, 3, 'EXT:fluid_styled_content/Configuration/TypoScript/,EXT:form/Configuration/TypoScript/,EXT:form_consent/Configuration/TypoScript', '', 'page = PAGE\npage.10 < styles.content.get'); diff --git a/Tests/Acceptance/Data/Fixtures/tt_content.sql b/Tests/Acceptance/Data/Fixtures/tt_content.sql new file mode 100644 index 00000000..bc207d0d --- /dev/null +++ b/Tests/Acceptance/Data/Fixtures/tt_content.sql @@ -0,0 +1,10 @@ +DELETE FROM `tt_content`; + +INSERT INTO `tt_content` (`uid`, `pid`, `deleted`, `hidden`, `CType`, `list_type`, `pi_flexform`) +VALUES (1, 1, 0, 0, 'form_formframework', '', '\n\n \n \n \n \n 1:/form_definitions/contact.form.yaml\n \n \n \n \n'), + (2, 1, 0, 0, 'form_formframework', '', '\n\n \n \n \n \n 1:/form_definitions/contact-confirmation-variant.form.yaml\n \n \n \n \n'), + (3, 1, 0, 0, 'form_formframework', '', '\n\n \n \n \n \n 1:/form_definitions/contact-email-variant.form.yaml\n \n \n \n \n'), + (4, 1, 0, 0, 'form_formframework', '', '\n\n \n \n \n \n 1:/form_definitions/contact-redirect-variant.form.yaml\n \n \n \n \n'), + (5, 1, 0, 0, 'form_formframework', '', '\n\n \n \n \n \n 1:/form_definitions/contact-v2.form.yaml\n \n \n \n \n'), + (6, 1, 0, 0, 'form_formframework', '', '\n\n \n \n \n \n 1:/form_definitions/contact-invalid.form.yaml\n \n \n \n \n'), + (7, 2, 0, 0, 'list', 'formconsent_consent', NULL); diff --git a/Tests/Acceptance/Data/Fixtures/tx_formconsent_domain_model_consent.sql b/Tests/Acceptance/Data/Fixtures/tx_formconsent_domain_model_consent.sql new file mode 100644 index 00000000..4b944ce6 --- /dev/null +++ b/Tests/Acceptance/Data/Fixtures/tx_formconsent_domain_model_consent.sql @@ -0,0 +1 @@ +DELETE FROM `tx_formconsent_domain_model_consent`; diff --git a/Tests/Acceptance/Data/dummy.png b/Tests/Acceptance/Data/dummy.png new file mode 100644 index 00000000..c601de8a Binary files /dev/null and b/Tests/Acceptance/Data/dummy.png differ diff --git a/Tests/Acceptance/Frontend/ConsentControllerCest.php b/Tests/Acceptance/Frontend/ConsentControllerCest.php new file mode 100644 index 00000000..b1af3330 --- /dev/null +++ b/Tests/Acceptance/Frontend/ConsentControllerCest.php @@ -0,0 +1,172 @@ + + * + * 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\Acceptance\Frontend; + +use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; +use EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\AcceptanceTester; +use EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\Helper\Form; + +/** + * ConsentControllerCest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class ConsentControllerCest +{ + private string $approveUrl; + private string $dismissUrl; + + public function _before(AcceptanceTester $I): void + { + $this->submitFormAndExtractUrls($I); + } + + public function canApproveConsentViaMail(AcceptanceTester $I): void + { + ['tx_formconsent_consent' => ['hash' => $hash]] = $I->extractQueryParametersFromUrl($this->approveUrl); + + $I->amOnPage($this->approveUrl); + + $I->see('Consent successful'); + + $I->seeInDatabase( + Consent::TABLE_NAME, + [ + 'data' => '{"email-1":"user@example.com","fileupload-1":null}', + 'email' => 'user@example.com', + 'approved' => '1', + 'form_persistence_identifier' => '1:/form_definitions/contact.form.yaml', + 'original_content_element_uid' => 1, + 'original_request_parameters' => null, + 'validation_hash' => $hash, + 'valid_until' => null, + ] + ); + } + + public function canDismissConsentViaMail(AcceptanceTester $I): void + { + ['tx_formconsent_consent' => ['hash' => $hash]] = $I->extractQueryParametersFromUrl($this->dismissUrl); + + $I->amOnPage($this->dismissUrl); + + $I->see('Consent successfully revoked'); + + $I->seeInDatabase( + Consent::TABLE_NAME, + [ + 'deleted' => '1', + 'data' => null, + 'email' => 'user@example.com', + 'approved' => '0', + 'form_persistence_identifier' => '1:/form_definitions/contact.form.yaml', + 'original_content_element_uid' => 1, + 'original_request_parameters' => null, + 'validation_hash' => $hash, + ] + ); + } + + public function canApproveConsentAndInvokeConfirmationFinisher(AcceptanceTester $I): void + { + $this->submitFormAndExtractUrls($I, Form::CONFIRMATION_AFTER_APPROVE); + + $I->amOnPage($this->approveUrl); + + $I->see('Thanks for your consent.'); + } + + public function canApproveConsentAndInvokeEmailFinisher(AcceptanceTester $I): void + { + $this->submitFormAndExtractUrls($I, Form::EMAIL_AFTER_APPROVE); + + $I->amOnPage($this->approveUrl); + + $I->fetchEmails(); + $I->accessInboxFor('admin@example.com'); + + $I->haveNumberOfUnreadEmails(1); + $I->openNextUnreadEmail(); + $I->seeInOpenedEmailSubject('Consent approved'); + } + + public function canApproveConsentAndInvokeRedirectFinisher(AcceptanceTester $I): void + { + $this->submitFormAndExtractUrls($I, Form::REDIRECT_AFTER_APPROVE); + + $I->amOnPage($this->approveUrl); + + $I->seeCurrentUrlEquals('/'); + } + + public function cannotApproveOrDismissAlreadyDismissedConsent(AcceptanceTester $I): void + { + $I->amOnPage($this->dismissUrl); + + $I->amOnPage($this->approveUrl); + $I->see('The link you clicked is no longer valid or has already been clicked. Please fill out the form again.'); + + $I->amOnPage($this->dismissUrl); + $I->see('The link you clicked is no longer valid or has already been clicked.'); + } + + public function cannotApproveOrDismissConsentIfEmailAddressIsInvalid(AcceptanceTester $I): void + { + ['tx_formconsent_consent' => ['hash' => $hash]] = $I->extractQueryParametersFromUrl($this->dismissUrl); + + $I->updateInDatabase( + Consent::TABLE_NAME, + ['email' => 'foo'], + ['validation_hash' => $hash] + ); + + $I->amOnPage($this->approveUrl); + $I->see('The email address sent with the link is not valid. Please fill out the form again.'); + + $I->amOnPage($this->dismissUrl); + $I->see('The email address sent with the link is not valid. Please fill out the form again.'); + } + + public function cannotApproveAlreadyApprovedConsent(AcceptanceTester $I): void + { + $I->amOnPage($this->approveUrl); + + $I->amOnPage($this->approveUrl); + $I->see('The link you clicked has already been clicked. This means that consent has already been given and the link is no longer valid.'); + } + + private function submitFormAndExtractUrls(AcceptanceTester $I, string $form = Form::DEFAULT): void + { + $I->amOnPage('/'); + $I->fillAndSubmitForm($form); + + $I->fetchEmails(); + $I->accessInboxFor('user@example.com'); + $I->openNextUnreadEmail(); + + $this->approveUrl = $I->grabUrlFromEmailBody(0); + $this->dismissUrl = $I->grabUrlFromEmailBody(1); + } +} diff --git a/Tests/Acceptance/Frontend/ConsentFinisherCest.php b/Tests/Acceptance/Frontend/ConsentFinisherCest.php new file mode 100644 index 00000000..bae7975c --- /dev/null +++ b/Tests/Acceptance/Frontend/ConsentFinisherCest.php @@ -0,0 +1,123 @@ + + * + * 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\Acceptance\Frontend; + +use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; +use EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\AcceptanceTester; +use EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\Helper\Form; + +/** + * ConsentFinisherCest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class ConsentFinisherCest +{ + public function _before(AcceptanceTester $I): void + { + $I->amOnPage('/'); + } + + public function canSubmitForm(AcceptanceTester $I): void + { + $I->fillAndSubmitForm(Form::DEFAULT, true); + + $I->waitForText('Please approve your consent.', 5); + + $I->seeInDatabase( + Consent::TABLE_NAME, + [ + 'data' => '{"email-1":"user@example.com","fileupload-1":7}', + 'email' => 'user@example.com', + 'form_persistence_identifier' => '1:/form_definitions/contact.form.yaml', + 'original_content_element_uid' => 1, + ] + ); + } + + public function receiveConsentMail(AcceptanceTester $I): void + { + $I->fillAndSubmitForm(); + + $I->fetchEmails(); + $I->accessInboxFor('user@example.com'); + + $I->haveNumberOfUnreadEmails(1); + $I->openNextUnreadEmail(); + $I->seeInOpenedEmailSubject('Approve your consent'); + } + + public function seeApproveLinkInConsentMail(AcceptanceTester $I): void + { + $I->fillAndSubmitForm(); + + $I->fetchEmails(); + $I->accessInboxFor('user@example.com'); + $I->openNextUnreadEmail(); + + $I->seeUrlsInEmailBody(2); + + $approveUrl = $I->grabUrlFromEmailBody(0); + + $I->assertUrlPathEquals('/confirmation', $approveUrl); + $I->assertQueryParameterEquals('approve', $approveUrl, 'tx_formconsent_consent/action'); + $I->assertQueryParameterEquals('user@example.com', $approveUrl, 'tx_formconsent_consent/email'); + } + + public function seeDismissLinkInConsentMail(AcceptanceTester $I): void + { + $I->fillAndSubmitForm(); + + $I->fetchEmails(); + $I->accessInboxFor('user@example.com'); + $I->openNextUnreadEmail(); + + $I->seeUrlsInEmailBody(2); + + $dismissUrl = $I->grabUrlFromEmailBody(1); + + $I->assertUrlPathEquals('/confirmation', $dismissUrl); + $I->assertQueryParameterEquals('dismiss', $dismissUrl, 'tx_formconsent_consent/action'); + $I->assertQueryParameterEquals('user@example.com', $dismissUrl, 'tx_formconsent_consent/email'); + } + + public function canSubmitFormWithCustomSender(AcceptanceTester $I): void + { + $I->fillAndSubmitForm(Form::V2); + + $I->fetchEmails(); + $I->accessInboxFor('user@example.com'); + $I->openNextUnreadEmail(); + + $I->seeInOpenedEmailSender('sender@example.com'); + } + + public function cannotSubmitInvalidForm(AcceptanceTester $I): void + { + $I->fillAndSubmitForm(Form::INVALID); + + $I->waitForText('The finisher option "recipientAddress" must be set.', 5); + } +} diff --git a/Tests/Acceptance/Support/AcceptanceTester.php b/Tests/Acceptance/Support/AcceptanceTester.php new file mode 100644 index 00000000..92ebae24 --- /dev/null +++ b/Tests/Acceptance/Support/AcceptanceTester.php @@ -0,0 +1,46 @@ + + * + * 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\Acceptance\Support; + +use Codeception\Actor; + +/** + * Inherited Methods + * @method void wantToTest($text) + * @method void wantTo($text) + * @method void execute($callable) + * @method void expectTo($prediction) + * @method void expect($prediction) + * @method void amGoingTo($argumentation) + * @method void am($role) + * @method void lookForwardTo($achieveValue) + * @method void comment($description) + * @method void pause() + * + * @SuppressWarnings(PHPMD) +*/ +final class AcceptanceTester extends Actor +{ + use _generated\AcceptanceTesterActions; +} diff --git a/Tests/Acceptance/Support/Extension/ApplicationEntrypointModifier.php b/Tests/Acceptance/Support/Extension/ApplicationEntrypointModifier.php new file mode 100644 index 00000000..7a11b002 --- /dev/null +++ b/Tests/Acceptance/Support/Extension/ApplicationEntrypointModifier.php @@ -0,0 +1,98 @@ + + * + * 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\Acceptance\Support\Extension; + +use Codeception\Events; +use Codeception\Extension; + +/** + * ApplicationEntrypointModifier + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +abstract class ApplicationEntrypointModifier extends Extension +{ + /** + * @var array + */ + protected static $events = [ + Events::SUITE_BEFORE => 'beforeSuite', + ]; + + protected string $targetDirectory; + protected string $buildDirectory; + protected string $mainEntrypoint; + protected string $appEntrypoint; + protected string $testEntrypoint; + + /** + * @param array $config + * @param array $options + */ + public function __construct($config, $options, string $targetDirectory, string $entrypointFile) + { + parent::__construct($config, $options); + + $this->targetDirectory = $targetDirectory; + $this->buildDirectory = \dirname(__DIR__, 3) . '/Build'; + $this->mainEntrypoint = $this->targetDirectory . '/index.php'; + $this->appEntrypoint = $this->targetDirectory . '/app.php'; + $this->testEntrypoint = $this->buildDirectory . '/' . ltrim($entrypointFile, '/'); + } + + public function beforeSuite(): void + { + $this->assertFilesExist(); + + if ($this->entrypointNeedsUpdate()) { + $this->moveEntrypoint(); + } + } + + protected function assertFilesExist(): void + { + \assert(is_dir($this->targetDirectory)); + \assert(file_exists($this->mainEntrypoint)); + \assert(file_exists($this->testEntrypoint)); + } + + protected function entrypointNeedsUpdate(): bool + { + if (!file_exists($this->appEntrypoint)) { + return true; + } + + return sha1_file($this->mainEntrypoint) !== sha1_file($this->testEntrypoint); + } + + protected function moveEntrypoint(): void + { + if (!file_exists($this->appEntrypoint)) { + rename($this->mainEntrypoint, $this->appEntrypoint); + } + + copy($this->testEntrypoint, $this->mainEntrypoint); + } +} diff --git a/Tests/Acceptance/Support/Extension/BackendEntrypointModifier.php b/Tests/Acceptance/Support/Extension/BackendEntrypointModifier.php new file mode 100644 index 00000000..31157c8c --- /dev/null +++ b/Tests/Acceptance/Support/Extension/BackendEntrypointModifier.php @@ -0,0 +1,45 @@ + + * + * 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\Acceptance\Support\Extension; + +/** + * BackendEntrypointModifier + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class BackendEntrypointModifier extends ApplicationEntrypointModifier +{ + /** + * @param array $config + * @param array $options + */ + public function __construct($config, $options) + { + $targetDirectory = \dirname(__DIR__, 4) . '/.Build/web/typo3'; + $buildPath = 'index-be-test.php'; + + parent::__construct($config, $options, $targetDirectory, $buildPath); + } +} diff --git a/Tests/Acceptance/Support/Extension/FrontendEntrypointModifier.php b/Tests/Acceptance/Support/Extension/FrontendEntrypointModifier.php new file mode 100644 index 00000000..f160e621 --- /dev/null +++ b/Tests/Acceptance/Support/Extension/FrontendEntrypointModifier.php @@ -0,0 +1,45 @@ + + * + * 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\Acceptance\Support\Extension; + +/** + * FrontendEntrypointModifier + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class FrontendEntrypointModifier extends ApplicationEntrypointModifier +{ + /** + * @param array $config + * @param array $options + */ + public function __construct($config, $options) + { + $targetDirectory = \dirname(__DIR__, 4) . '/.Build/web'; + $buildPath = 'index-fe-test.php'; + + parent::__construct($config, $options, $targetDirectory, $buildPath); + } +} diff --git a/Tests/Acceptance/Support/Helper/Backend.php b/Tests/Acceptance/Support/Helper/Backend.php new file mode 100644 index 00000000..15fce724 --- /dev/null +++ b/Tests/Acceptance/Support/Helper/Backend.php @@ -0,0 +1,78 @@ + + * + * 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\Acceptance\Support\Helper; + +use EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\AcceptanceTester; + +/** + * Backend + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class Backend +{ + private const USERNAME = 'admin'; + private const PASSWORD = 'password'; + + private AcceptanceTester $tester; + private ModalDialog $dialog; + + public function __construct(AcceptanceTester $tester, ModalDialog $dialog) + { + $this->tester = $tester; + $this->dialog = $dialog; + } + + public function login(): void + { + $I = $this->tester; + + $I->amOnPage('/typo3/'); + $I->waitForElementVisible('#t3-username'); + $I->waitForElementVisible('#t3-password'); + $I->fillField('#t3-username', self::USERNAME); + $I->fillField('#t3-password', self::PASSWORD); + $I->click('#t3-login-submit'); + $I->dontSeeElement('#typo3-login-form'); + + try { + $this->dialog->clickButtonInDialog('[name=ok]'); + } catch (\Exception $e) { + // If dialog is not present, that's fine... + } + } + + /** + * @throws \Exception + */ + public function openModule(string $identifier): void + { + $I = $this->tester; + + $I->waitForElementClickable($identifier, 5); + $I->click($identifier); + $I->switchToIFrame('#typo3-contentIframe'); + } +} diff --git a/Tests/Acceptance/Support/Helper/Email.php b/Tests/Acceptance/Support/Helper/Email.php new file mode 100644 index 00000000..93110f04 --- /dev/null +++ b/Tests/Acceptance/Support/Helper/Email.php @@ -0,0 +1,108 @@ + + * + * 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\Acceptance\Support\Helper; + +use Codeception\Module; + +/** + * Email + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class Email extends Module +{ + public function grabUrlFromEmailBody(int $index = 0): string + { + $urls = $this->extractUrlsFromEmailBody(); + + if (!isset($urls[$index])) { + $this->fail( + sprintf('Only %d urls were extracted from email body, index %d requested.', \count($urls), $index) + ); + } + + return $urls[$index]; + } + + /** + * @return list + */ + public function grabAllUrlsFromEmailBody(): array + { + return $this->extractUrlsFromEmailBody(); + } + + public function seeUrlsInEmailBody(int $count = null): void + { + if (!$this->hasModule('Asserts')) { + $this->fail('Asserts module is not enabled.'); + } + + /** @var Module\Asserts $I */ + $I = $this->getModule('Asserts'); + + $urls = $this->extractUrlsFromEmailBody(); + + if (null !== $count) { + $I->assertCount($count, $urls); + } else { + $I->assertNotEmpty($urls); + } + } + + /** + * @return list + */ + private function extractUrlsFromEmailBody(): array + { + if (!$this->hasModule('MailHog')) { + $this->fail('MailHog module is not enabled.'); + } + + /** @var Module\MailHog $I */ + $I = $this->getModule('MailHog'); + + $body = quoted_printable_decode($I->grabBodyFromEmail('text/plain')); + $urlPattern = sprintf('~%s(?P\S+)~', preg_quote($this->getCurrentBaseUrl(), '~')); + + if (!preg_match_all($urlPattern, $body, $matches)) { + $this->fail('No urls found in email.'); + } + + return array_map(fn (string $url): string => '/' . $url, array_values(array_filter($matches['url']))); + } + + private function getCurrentBaseUrl(): string + { + if (!$this->hasModule('WebDriver')) { + $this->fail('WebDriver module is not enabled.'); + } + + /** @var Module\WebDriver $webDriver */ + $webDriver = $this->getModule('WebDriver'); + + return $webDriver->_getUrl(); + } +} diff --git a/Tests/Acceptance/Support/Helper/Form.php b/Tests/Acceptance/Support/Helper/Form.php new file mode 100644 index 00000000..25f954a2 --- /dev/null +++ b/Tests/Acceptance/Support/Helper/Form.php @@ -0,0 +1,96 @@ + + * + * 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\Acceptance\Support\Helper; + +use Codeception\Module; +use Facebook\WebDriver\WebDriverBy; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Form + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class Form extends Module +{ + public const DEFAULT = 'contact-1'; + public const CONFIRMATION_AFTER_APPROVE = 'contact-confirmation-variant-2'; + public const EMAIL_AFTER_APPROVE = 'contact-email-variant-3'; + public const REDIRECT_AFTER_APPROVE = 'contact-redirect-variant-4'; + public const V2 = 'contact-v2-5'; + public const INVALID = 'contact-invalid-6'; + + public function fillAndSubmitForm(string $form = self::DEFAULT, bool $attachFile = false): void + { + $I = $this->getWebDriver(); + + $elements = [ + $this->getFormElementName($form, 'email-1') => 'user@example.com', + ]; + + if ($attachFile) { + $I->attachFile( + sprintf('[name="%s"]', $this->getFormElementName($form, 'fileupload-1')), + 'dummy.png' + ); + } + + $I->submitForm( + WebDriverBy::id($this->getFormElementIdentifier($form)), + $elements, + WebDriverBy::name($this->getFormElementName($form, '__currentPage')) + ); + } + + private function getFormElementIdentifier(string $form, string $element = null): string + { + return $form . (null !== $element ? '-' . $element : ''); + } + + private function getFormElementName(string $form, string $element = null): string + { + $nameParts = [ + $form => [], + ]; + + if (null !== $element) { + $nameParts[$form][$element] = null; + } + + return substr(GeneralUtility::implodeArrayForUrl('tx_form_formframework', $nameParts), 1, -1); + } + + private function getWebDriver(): Module\WebDriver + { + if (!$this->hasModule('WebDriver')) { + $this->fail('WebDriver module is not enabled.'); + } + + /** @var Module\WebDriver $webDriver */ + $webDriver = $this->getModule('WebDriver'); + + return $webDriver; + } +} diff --git a/Tests/Acceptance/Support/Helper/ModalDialog.php b/Tests/Acceptance/Support/Helper/ModalDialog.php new file mode 100644 index 00000000..b7486e38 --- /dev/null +++ b/Tests/Acceptance/Support/Helper/ModalDialog.php @@ -0,0 +1,42 @@ + + * + * 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\Acceptance\Support\Helper; + +use EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\AcceptanceTester; +use TYPO3\TestingFramework\Core\Acceptance\Helper\AbstractModalDialog; + +/** + * ModalDialog + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class ModalDialog extends AbstractModalDialog +{ + public function __construct(AcceptanceTester $tester) + { + /* @phpstan-ignore-next-line */ + $this->tester = $tester; + } +} diff --git a/Tests/Acceptance/Support/Helper/Url.php b/Tests/Acceptance/Support/Helper/Url.php new file mode 100644 index 00000000..5ce2dfec --- /dev/null +++ b/Tests/Acceptance/Support/Helper/Url.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\Acceptance\Support\Helper; + +use Codeception\Module; +use TYPO3\CMS\Core\Utility\ArrayUtility; +use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException; + +/** + * Url + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class Url extends Module +{ + /** + * @return array + */ + public function extractQueryParametersFromUrl(string $url): array + { + $queryParams = []; + $parsedUrl = parse_url($url); + + \assert(\is_array($parsedUrl)); + + parse_str($parsedUrl['query'] ?? '', $queryParams); + + return $queryParams; + } + + public function assertUrlPathEquals(string $expected, string $url): void + { + if (!$this->hasModule('Asserts')) { + $this->fail('Asserts module is not enabled.'); + } + + /** @var Module\Asserts $I */ + $I = $this->getModule('Asserts'); + + $actual = parse_url($url, PHP_URL_PATH); + + $I->assertEquals($expected, $actual); + } + + public function assertQueryParameterEquals(string $expected, string $url, string $path): void + { + if (!$this->hasModule('Asserts')) { + $this->fail('Asserts module is not enabled.'); + } + + /** @var Module\Asserts $I */ + $I = $this->getModule('Asserts'); + + try { + $queryParams = $this->extractQueryParametersFromUrl($url); + $actual = ArrayUtility::getValueByPath($queryParams, $path); + + $I->assertEquals($expected, $actual); + } catch (MissingArrayPathException $exception) { + $this->fail( + sprintf('Query parameter "%s" was not found in URL: %s', $path, $exception->getMessage()) + ); + } + } +} diff --git a/Tests/Build/AdditionalConfiguration.php b/Tests/Build/AdditionalConfiguration.php new file mode 100644 index 00000000..726f32ef --- /dev/null +++ b/Tests/Build/AdditionalConfiguration.php @@ -0,0 +1,66 @@ + + * + * 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 . + */ + +if (getenv('IS_DDEV_PROJECT') == 'true') { + $GLOBALS['TYPO3_CONF_VARS'] = array_replace_recursive( + $GLOBALS['TYPO3_CONF_VARS'], + [ + 'BE' => [ + // password + 'installToolPassword' => '$argon2i$v=19$m=65536,t=16,p=1$aVkwaW5iVHR4M0U3TWdaMw$TJQun8VfSLBmGrEuTxpl6B9axxxwfKw1IARSmoImRNo', + ], + 'DB' => [ + 'Connections' => [ + 'Default' => [ + 'charset' => 'utf8mb4', + 'dbname' => 'db', + 'driver' => 'pdo_mysql', + 'host' => 'db', + 'password' => 'db', + 'port' => 3306, + 'tableoptions' => [ + 'charset' => 'utf8mb4', + 'collate' => 'utf8mb4_unicode_ci', + ], + 'user' => 'db', + ], + ], + ], + // This GFX configuration allows processing by installed ImageMagick 6 + 'GFX' => [ + 'processor' => 'ImageMagick', + 'processor_path' => '/usr/bin/', + 'processor_path_lzw' => '/usr/bin/', + ], + // This mail configuration sends all emails to mailhog + 'MAIL' => [ + 'transport' => 'smtp', + 'transport_smtp_encrypt' => false, + 'transport_smtp_server' => 'localhost:1025', + ], + 'SYS' => [ + 'trustedHostsPattern' => '.*.*', + 'devIPmask' => '*', + 'displayErrors' => 1, + ], + ] + ); +} diff --git a/Tests/Build/index-be-test.php b/Tests/Build/index-be-test.php new file mode 100755 index 00000000..c4f0b669 --- /dev/null +++ b/Tests/Build/index-be-test.php @@ -0,0 +1,24 @@ + + * + * 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 . + */ + +require_once \dirname(__DIR__, 2) . '/vendor/autoload.php'; +require_once \dirname(__DIR__, 3) . '/c3.php'; +require_once 'app.php'; diff --git a/Tests/Build/index-fe-test.php b/Tests/Build/index-fe-test.php new file mode 100755 index 00000000..097432ba --- /dev/null +++ b/Tests/Build/index-fe-test.php @@ -0,0 +1,24 @@ + + * + * 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 . + */ + +require_once \dirname(__DIR__) . '/vendor/autoload.php'; +require_once \dirname(__DIR__, 2) . '/c3.php'; +require_once 'app.php'; diff --git a/Tests/Build/sites-config.yaml b/Tests/Build/sites-config.yaml new file mode 100644 index 00000000..85ce45b9 --- /dev/null +++ b/Tests/Build/sites-config.yaml @@ -0,0 +1,17 @@ +base: 'https://typo3-ext-form-consent.ddev.site/' +errorHandling: { } +languages: + - + title: English + enabled: true + languageId: 0 + base: / + typo3Language: default + locale: en_US.UTF-8 + iso-639-1: en + navigationTitle: English + hreflang: en-us + direction: ltr + flag: us +rootPageId: 1 +routes: { } diff --git a/Tests/Functional/Configuration/ConfigurationTest.php b/Tests/Functional/Configuration/ConfigurationTest.php new file mode 100644 index 00000000..9fac67db --- /dev/null +++ b/Tests/Functional/Configuration/ConfigurationTest.php @@ -0,0 +1,117 @@ + + * + * 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\Configuration; + +use EliasHaeussler\Typo3FormConsent\Configuration\Configuration; +use EliasHaeussler\Typo3FormConsent\Configuration\Extension; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; +use TYPO3\CMS\Core\Information\Typo3Version; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +/** + * ConfigurationTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class ConfigurationTest extends FunctionalTestCase +{ + protected ExtensionConfiguration $extensionConfiguration; + + protected function setUp(): void + { + parent::setUp(); + $this->extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class); + } + + /** + * @test + */ + public function getExcludedElementsFromPersistenceReturnsEmptyArrayIfConfigurationOptionDoesNotExist(): void + { + $this->setExtensionConfiguration([]); + + self::assertSame([], Configuration::getExcludedElementsFromPersistence()); + } + + /** + * @test + */ + public function getExcludedElementsFromPersistenceReturnsEmptyArrayIfExtensionConfigurationIsMissing(): void + { + $this->setExtensionConfiguration(null); + + self::assertSame([], Configuration::getExcludedElementsFromPersistence()); + } + + /** + * @test + */ + public function getExcludedElementsFromPersistenceReturnsExcludedElementsFromPersistence(): void + { + $this->setExtensionConfiguration([ + 'persistence' => [ + 'excludedElements' => 'Honeypot, StaticText, , ContentElement', + ], + ]); + + $expected = [ + 'Honeypot', + 'StaticText', + 'ContentElement', + ]; + + self::assertSame($expected, Configuration::getExcludedElementsFromPersistence()); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $reflectionClass = new \ReflectionClass(Configuration::class); + $reflectionProperty = $reflectionClass->getProperty('configuration'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue(null); + } + + /** + * @param array|null $configuration + */ + private function setExtensionConfiguration(?array $configuration): void + { + // @todo Remove condition once v10 support is dropped + if ($this->getMajorTypo3Version() >= 11) { + $this->extensionConfiguration->set(Extension::KEY, $configuration); + } else { + /* @phpstan-ignore-next-line */ + $this->extensionConfiguration->set(Extension::KEY, '', $configuration); + } + } + + private function getMajorTypo3Version(): int + { + return (new Typo3Version())->getMajorVersion(); + } +} diff --git a/Tests/Functional/Configuration/IconTest.php b/Tests/Functional/Configuration/IconTest.php index 3249adb3..082a025a 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; /** @@ -35,8 +34,16 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class IconTest extends FunctionalTestCase +final class IconTest extends FunctionalTestCase { + protected IconRegistry $iconRegistry; + + protected function setUp(): void + { + parent::setUp(); + $this->iconRegistry = $this->getContainer()->get(IconRegistry::class); + } + /** * @test */ @@ -44,7 +51,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 +69,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..8a729973 100644 --- a/Tests/Functional/Configuration/LocalizationTest.php +++ b/Tests/Functional/Configuration/LocalizationTest.php @@ -28,6 +28,8 @@ use EliasHaeussler\Typo3FormConsent\Configuration\Localization; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; @@ -37,7 +39,7 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class LocalizationTest extends FunctionalTestCase +final class LocalizationTest extends FunctionalTestCase { use ProphecyTrait; @@ -81,6 +83,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 */ @@ -131,7 +144,24 @@ public function forKeyReturnsLocalizationKey(string $key, ?string $type, string /** * @test */ - public function translateReturnsTranslationFromTsfeIfEnvironmentIsInFrontendMode(): void + public function translateReturnsTranslationFromTsfeIfEnvironmentIsInFrontendModeAndRequestIsAvailable(): void + { + $this->simulateFrontendEnvironment(); + + $serverRequest = new ServerRequest(); + $GLOBALS['TYPO3_REQUEST'] = $serverRequest->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); + + $localizationKey = Localization::forKey('foo'); + $expected = 'baz'; + self::assertSame($expected, Localization::translate($localizationKey)); + + unset($GLOBALS['TYPO3_REQUEST']); + } + + /** + * @test + */ + public function translateReturnsTranslationFromTsfeIfEnvironmentIsInFrontendModeAndRequestIsNotAvailable(): void { $this->simulateFrontendEnvironment(); diff --git a/Tests/Functional/Domain/Finishers/FinisherOptionsTest.php b/Tests/Functional/Domain/Finishers/FinisherOptionsTest.php new file mode 100644 index 00000000..431e61fa --- /dev/null +++ b/Tests/Functional/Domain/Finishers/FinisherOptionsTest.php @@ -0,0 +1,446 @@ + + * + * 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 + */ +final class FinisherOptionsTest extends FunctionalTestCase +{ + protected $coreExtensionsToLoad = [ + 'form', + ]; + + protected $testExtensionsToLoad = [ + 'typo3conf/ext/form_consent', + ]; + + protected FinisherOptions $subject; + + /** + * @var array + */ + protected array $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..fdb5b3fd 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; /** @@ -38,7 +33,7 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ConsentRepositoryTest extends FunctionalTestCase +final class ConsentRepositoryTest extends FunctionalTestCase { protected $coreExtensionsToLoad = [ 'form', @@ -48,23 +43,14 @@ class ConsentRepositoryTest extends FunctionalTestCase 'typo3conf/ext/form_consent', ]; - /** - * @var ConsentRepository - */ - protected $subject; + protected ConsentRepository $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/ExpressionLanguage/FunctionsProvider/ConsentConditionFunctionsProviderTest.php b/Tests/Functional/ExpressionLanguage/FunctionsProvider/ConsentConditionFunctionsProviderTest.php new file mode 100644 index 00000000..248eb9d5 --- /dev/null +++ b/Tests/Functional/ExpressionLanguage/FunctionsProvider/ConsentConditionFunctionsProviderTest.php @@ -0,0 +1,132 @@ + + * + * 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\ExpressionLanguage\FunctionsProvider; + +use EliasHaeussler\Typo3FormConsent\Domain\Model\Consent; +use EliasHaeussler\Typo3FormConsent\Registry\ConsentManagerRegistry; +use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; +use TYPO3\CMS\Core\ExpressionLanguage\Resolver; +use TYPO3\CMS\Form\Domain\Model\FormDefinition; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +/** + * ConsentConditionFunctionsProviderTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class ConsentConditionFunctionsProviderTest extends FunctionalTestCase +{ + use ProphecyTrait; + + protected $coreExtensionsToLoad = [ + 'form', + ]; + + protected $testExtensionsToLoad = [ + 'typo3conf/ext/form_consent', + ]; + /** + * @var FormRuntime|ObjectProphecy + */ + protected $formRuntimeProphecy; + + protected function setUp(): void + { + parent::setUp(); + + $formDefinition = new FormDefinition('foo', [], 'Form', 'foo'); + + // @todo Replace with $this->getContainer()->get(FormRuntime::class) once v10 support is dropped + $this->formRuntimeProphecy = $this->prophesize(FormRuntime::class); + $this->formRuntimeProphecy->getFormDefinition()->willReturn($formDefinition); + } + + /** + * @test + */ + public function isConsentApprovedReturnsFalseIfFormRuntimeIsNotDefined(): void + { + $resolver = $this->createResolver(); + + self::assertFalse($resolver->evaluate('isConsentApproved()')); + } + + /** + * @test + */ + public function isConsentApprovedFunctionReturnsTrueIfConsentIsRegisteredAndApproved(): void + { + $resolver = $this->createResolver($this->formRuntimeProphecy->reveal()); + + $consent = new Consent(); + $consent->setApproved(true); + $consent->setFormPersistenceIdentifier('foo'); + + ConsentManagerRegistry::registerConsent($consent); + + self::assertTrue($resolver->evaluate('isConsentApproved()')); + } + + /** + * @test + */ + public function isConsentDismissedReturnsFalseIfFormRuntimeIsNotDefined(): void + { + $resolver = $this->createResolver(); + + self::assertFalse($resolver->evaluate('isConsentDismissed()')); + } + + /** + * @test + */ + public function isConsentDismissedFunctionReturnsTrueIfConsentIsRegisteredAndDismissed(): void + { + $resolver = $this->createResolver($this->formRuntimeProphecy->reveal()); + + $consent = new Consent(); + $consent->setApproved(false); + $consent->setData(null); + $consent->setOriginalRequestParameters(null); + $consent->setFormPersistenceIdentifier('foo'); + + ConsentManagerRegistry::registerConsent($consent); + + self::assertTrue($resolver->evaluate('isConsentDismissed()')); + } + + private function createResolver(FormRuntime $formRuntime = null): Resolver + { + $variables = []; + + if (null !== $formRuntime) { + $variables['formRuntime'] = $formRuntime; + } + + return new Resolver('form', $variables); + } +} 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 66% rename from Tests/Unit/Service/HashServiceTest.php rename to Tests/Functional/Service/HashServiceTest.php index 38423840..c421765a 100644 --- a/Tests/Unit/Service/HashServiceTest.php +++ b/Tests/Functional/Service/HashServiceTest.php @@ -21,16 +21,16 @@ * 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\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,24 +38,11 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class HashServiceTest extends UnitTestCase +final class HashServiceTest extends FunctionalTestCase { - use ProphecyTrait; - - /** - * @var Consent - */ - protected $consent; - - /** - * @var ObjectProphecy|EventDispatcherInterface - */ - protected $eventDispatcherProphecy; - - /** - * @var HashService - */ - protected $subject; + protected Consent $consent; + protected ListenerProvider $listenerProvider; + protected HashService $subject; protected function setUp(): void { @@ -64,12 +51,11 @@ 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->listenerProvider = $this->getContainer()->get(ListenerProvider::class); + $this->subject = new HashService($this->getContainer()->get(EventDispatcherInterface::class)); } /** @@ -94,12 +80,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 +103,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 +157,30 @@ 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 + { + $container = $this->getContainer(); + + self::assertInstanceOf(Container::class, $container); + + $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..f1fa71c5 --- /dev/null +++ b/Tests/Functional/Type/Transformer/FormRequestTypeTransformerTest.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\Tests\Functional\Type\Transformer; + +use EliasHaeussler\Typo3FormConsent\Type\Transformer\FormRequestTypeTransformer; +use TYPO3\CMS\Extbase\Security\Cryptography\HashService; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +/** + * FormRequestTypeTransformerTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class FormRequestTypeTransformerTest extends FunctionalTestCase +{ + protected FormRequestTypeTransformer $subject; + + protected function setUp(): void + { + parent::setUp(); + + $this->subject = new FormRequestTypeTransformer($this->getContainer()->get(HashService::class)); + } + + /** + * @test + */ + public function transformThrowsExceptionIfFormRuntimeIsNotGiven(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(1646044629); + $this->expectExceptionMessage('Expected a valid FormRuntime object, NULL given.'); + + $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..f52b23fa --- /dev/null +++ b/Tests/Functional/Type/Transformer/FormValuesTypeTransformerTest.php @@ -0,0 +1,88 @@ + + * + * 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 Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; +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 + */ +final class FormValuesTypeTransformerTest extends FunctionalTestCase +{ + use ProphecyTrait; + + protected $coreExtensionsToLoad = [ + 'form', + ]; + + protected $testExtensionsToLoad = [ + 'typo3conf/ext/form_consent', + ]; + + protected FormValuesTypeTransformer $subject; + + /** + * @var FormRuntime|ObjectProphecy + */ + protected $formRuntimeProphecy; + + protected function setUp(): void + { + parent::setUp(); + + $this->subject = new FormValuesTypeTransformer(); + // @todo Replace with $this->getContainer()->get(FormRuntime::class) once v10 support is dropped + $this->formRuntimeProphecy = $this->prophesize(FormRuntime::class); + } + + /** + * @test + */ + public function transformThrowsExceptionIfFormRuntimeIsNotGiven(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(1646044591); + $this->expectExceptionMessage('Expected a valid FormRuntime object, NULL given.'); + + $this->subject->transform(); + } + + /** + * @test + */ + public function transformReturnsJsonTypeWithEmptyArrayIfFormIsUninitialized(): void + { + $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 ba91bac8..0f2fdf51 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; @@ -37,7 +35,7 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ConsentChartDataProviderTest extends FunctionalTestCase +final class ConsentChartDataProviderTest extends FunctionalTestCase { protected $coreExtensionsToLoad = [ 'form', @@ -47,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 @@ -66,7 +58,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 @@ -84,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/Configuration/IconTest.php b/Tests/Unit/Configuration/IconTest.php index 406ebef4..5403d1dc 100644 --- a/Tests/Unit/Configuration/IconTest.php +++ b/Tests/Unit/Configuration/IconTest.php @@ -32,7 +32,7 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class IconTest extends UnitTestCase +final class IconTest extends UnitTestCase { /** * @test diff --git a/Tests/Unit/Domain/Model/ConsentTest.php b/Tests/Unit/Domain/Model/ConsentTest.php index feec5952..fdd9d388 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; /** @@ -32,12 +33,9 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ConsentTest extends UnitTestCase +final class ConsentTest extends UnitTestCase { - /** - * @var Consent - */ - protected $subject; + protected Consent $subject; protected function setUp(): void { @@ -70,16 +68,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->setOriginalRequestParameters($originalRequestParameters); + + self::assertSame($originalRequestParameters, $this->subject->getOriginalRequestParameters()); + } + + /** + * @test + */ + public function getOriginalContentElementUidReturnsZeroOnInitialState(): void + { + self::assertSame(0, $this->subject->getOriginalContentElementUid()); + } - $this->subject->setData('{"foo":"baz"}'); - self::assertSame($expectedJson, $this->subject->getData()); - self::assertSame($expectedArray, $this->subject->getDataArray()); + /** + * @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..a1912831 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; /** @@ -33,17 +34,10 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ApproveConsentEventTest extends UnitTestCase +final class ApproveConsentEventTest extends UnitTestCase { - /** - * @var ApproveConsentEvent - */ - protected $subject; - - /** - * @var Consent - */ - protected $consent; + protected ApproveConsentEvent $subject; + protected Consent $consent; protected function setUp(): void { @@ -61,4 +55,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/Event/DismissConsentEventTest.php b/Tests/Unit/Event/DismissConsentEventTest.php index a4dfb7c4..4718f05c 100644 --- a/Tests/Unit/Event/DismissConsentEventTest.php +++ b/Tests/Unit/Event/DismissConsentEventTest.php @@ -33,17 +33,10 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class DismissConsentEventTest extends UnitTestCase +final 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..f905df22 100644 --- a/Tests/Unit/Event/GenerateHashEventTest.php +++ b/Tests/Unit/Event/GenerateHashEventTest.php @@ -33,18 +33,16 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class GenerateHashEventTest extends UnitTestCase +final 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..d1e316ee 100644 --- a/Tests/Unit/Event/ModifyConsentEventTest.php +++ b/Tests/Unit/Event/ModifyConsentEventTest.php @@ -33,17 +33,10 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ModifyConsentEventTest extends UnitTestCase +final 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..5bd72037 100644 --- a/Tests/Unit/Event/ModifyConsentMailEventTest.php +++ b/Tests/Unit/Event/ModifyConsentMailEventTest.php @@ -34,19 +34,12 @@ * @author Elias Häußler * @license GPL-2.0-or-later */ -class ModifyConsentMailEventTest extends UnitTestCase +final 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/Exception/NotAllowedExceptionTest.php b/Tests/Unit/Exception/NotAllowedExceptionTest.php new file mode 100644 index 00000000..17e46732 --- /dev/null +++ b/Tests/Unit/Exception/NotAllowedExceptionTest.php @@ -0,0 +1,48 @@ + + * + * 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\Exception; + +use EliasHaeussler\Typo3FormConsent\Exception\NotAllowedException; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * NotAllowedExceptionTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class NotAllowedExceptionTest extends UnitTestCase +{ + /** + * @test + */ + public function forMethodReturnsNotAllowedExceptionForGivenMethod(): void + { + $actual = NotAllowedException::forMethod('foo'); + + self::assertInstanceOf(NotAllowedException::class, $actual); + self::assertSame('Calling the method "foo" is not allowed.', $actual->getMessage()); + self::assertSame(1645781267, $actual->getCode()); + } +} diff --git a/Tests/Unit/Exception/UnsupportedTypeExceptionTest.php b/Tests/Unit/Exception/UnsupportedTypeExceptionTest.php new file mode 100644 index 00000000..8ad7826b --- /dev/null +++ b/Tests/Unit/Exception/UnsupportedTypeExceptionTest.php @@ -0,0 +1,48 @@ + + * + * 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\Exception; + +use EliasHaeussler\Typo3FormConsent\Exception\UnsupportedTypeException; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * UnsupportedTypeExceptionTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class UnsupportedTypeExceptionTest extends UnitTestCase +{ + /** + * @test + */ + public function createReturnsUnsupportedTypeExceptionForGivenType(): void + { + $actual = UnsupportedTypeException::create('foo'); + + self::assertInstanceOf(UnsupportedTypeException::class, $actual); + self::assertSame('The type "foo" is not supported.', $actual->getMessage()); + self::assertSame(1645774926, $actual->getCode()); + } +} diff --git a/Tests/Unit/Http/StringableResponseFactoryTest.php b/Tests/Unit/Http/StringableResponseFactoryTest.php new file mode 100644 index 00000000..f025e609 --- /dev/null +++ b/Tests/Unit/Http/StringableResponseFactoryTest.php @@ -0,0 +1,75 @@ + + * + * 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\Http; + +use EliasHaeussler\Typo3FormConsent\Http\StringableResponse; +use EliasHaeussler\Typo3FormConsent\Http\StringableResponseFactory; +use TYPO3\CMS\Core\Http\Response; +use TYPO3\CMS\Core\Information\Typo3Version; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * StringableResponseFactoryTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class StringableResponseFactoryTest extends UnitTestCase +{ + /** + * @test + */ + public function createResponseReturnsStringableResponseOnTypo3V10(): void + { + // @todo Remove test case once v10 support is dropped + + if ($this->getMajorTypo3Version() > 10) { + self::markTestSkipped(sprintf('Test targets TYPO3 v10, v%d found.', $this->getMajorTypo3Version())); + } + + $subject = new StringableResponseFactory(); + + self::assertInstanceOf(StringableResponse::class, $subject->createResponse()); + } + + /** + * @test + */ + public function createResponseReturnsResponse(): void + { + // @todo Remove condition once v10 support is dropped + if ($this->getMajorTypo3Version() < 11) { + self::markTestSkipped(sprintf('Test targets TYPO3 v11, v%d found.', $this->getMajorTypo3Version())); + } + + $subject = new StringableResponseFactory(); + + self::assertInstanceOf(Response::class, $subject->createResponse()); + } + + private function getMajorTypo3Version(): int + { + return (new Typo3Version())->getMajorVersion(); + } +} diff --git a/Tests/Unit/Http/StringableResponseTest.php b/Tests/Unit/Http/StringableResponseTest.php new file mode 100644 index 00000000..d8093bee --- /dev/null +++ b/Tests/Unit/Http/StringableResponseTest.php @@ -0,0 +1,60 @@ + + * + * 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\Http; + +use EliasHaeussler\Typo3FormConsent\Http\StringableResponse; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * StringableResponseTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class StringableResponseTest extends UnitTestCase +{ + /** + * @test + */ + public function toStringReturnsEmptyStringIfBodyIsNull(): void + { + $subject = new StringableResponse(null); + + self::assertSame('', $subject->__toString()); + } + + /** + * @test + */ + public function toStringReturnsBodyContents(): void + { + $subject = new StringableResponse(); + + $body = $subject->getBody(); + $body->rewind(); + $body->write('hello world!'); + + self::assertSame('hello world!', $subject->__toString()); + } +} diff --git a/Tests/Unit/Registry/ConsentManagerRegistryTest.php b/Tests/Unit/Registry/ConsentManagerRegistryTest.php new file mode 100644 index 00000000..1648b234 --- /dev/null +++ b/Tests/Unit/Registry/ConsentManagerRegistryTest.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\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 + */ +final class ConsentManagerRegistryTest extends UnitTestCase +{ + protected Consent $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..8b23bf9f --- /dev/null +++ b/Tests/Unit/Registry/Dto/ConsentStateTest.php @@ -0,0 +1,80 @@ + + * + * 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 + */ +final class ConsentStateTest extends UnitTestCase +{ + protected Consent $consent; + protected ConsentState $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..56488d2e --- /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 + */ +final class JsonTypeTest extends UnitTestCase +{ + /** + * @var JsonType + */ + protected 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..a8ecc399 --- /dev/null +++ b/Tests/Unit/Type/Transformer/TypeTransformerFactoryTest.php @@ -0,0 +1,90 @@ + + * + * 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 + */ +final class TypeTransformerFactoryTest extends UnitTestCase +{ + protected FormRequestTypeTransformer $formRequestTypeTransformer; + protected TypeTransformerFactory $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/captainhook.json b/captainhook.json index 54565798..20b88bb5 100644 --- a/captainhook.json +++ b/captainhook.json @@ -71,7 +71,7 @@ ] }, { - "action": "composer normalize --dry-run --no-check-lock --no-update-lock", + "action": "composer lint:composer -- --dry-run", "options": [], "conditions": [ { diff --git a/codeception.yml b/codeception.yml new file mode 100644 index 00000000..ee156ad2 --- /dev/null +++ b/codeception.yml @@ -0,0 +1,60 @@ +namespace: EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support + +suites: + Acceptance: + actor: AcceptanceTester + path: . + modules: + enabled: + - WebDriver: + url: https://%TESTING_DOMAIN%/ + browser: chrome + wait: 5 + host: selenium + port: 4444 + capabilities: + acceptInsecureCerts: true + - Db: + dsn: 'mysql:host=db;dbname=db' + user: 'root' + password: 'root' + dump: + - Tests/Acceptance/Data/Fixtures/be_users.sql + - Tests/Acceptance/Data/Fixtures/pages.sql + - Tests/Acceptance/Data/Fixtures/sys_file.sql + - Tests/Acceptance/Data/Fixtures/sys_template.sql + - Tests/Acceptance/Data/Fixtures/tt_content.sql + - Tests/Acceptance/Data/Fixtures/tx_formconsent_domain_model_consent.sql + populate: true + waitlock: 5 + - MailHog: + url: http://%TESTING_DOMAIN%/ + port: 8025 + deleteEmailsAfterScenario: true + - Asserts + - EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\Helper\Email + - EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\Helper\Form + - EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\Helper\Url + +actor_suffix: Tester +extensions: + enabled: + - Codeception\Extension\RunFailed + - Codeception\Extension\Recorder: + delete_successful: true + - EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\Extension\BackendEntrypointModifier + - EliasHaeussler\Typo3FormConsent\Tests\Acceptance\Support\Extension\FrontendEntrypointModifier + +coverage: + enabled: true + include: + - Classes/* + +paths: + tests: Tests/Acceptance + output: .Build/log/acceptance-reports + data: Tests/Acceptance/Data + support: Tests/Acceptance/Support + +params: + - env diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..c14250d2 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +comment: + behavior: new diff --git a/composer.json b/composer.json index ab7ded5d..7956e26b 100644 --- a/composer.json +++ b/composer.json @@ -12,45 +12,59 @@ } ], "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/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/dependency-injection": "^4.4.30 || ^5.3.7", + "symfony/expression-language": "^4.4.30 || ^5.3.7", + "symfony/mailer": "^4.4.30 || ^5.3.7", + "symfony/mime": "^4.4.30 || ^5.3.7", + "symfony/polyfill-php80": "^1.24", + "typo3/cms-backend": "~10.4.11 || ~11.5.1", + "typo3/cms-core": "~10.4.11 || ~11.5.1", + "typo3/cms-extbase": "~10.4.11 || ~11.5.1", + "typo3/cms-fluid": "~10.4.11 || ~11.5.1", + "typo3/cms-form": "~10.4.11 || ~11.5.1", + "typo3/cms-frontend": "~10.4.11 || ~11.5.1" }, "require-dev": { "armin/editorconfig-cli": "^1.5", "captainhook/plugin-composer": "^5.3", + "codeception/c3": "^2.6", + "codeception/codeception": "^4.1.11", + "codeception/module-asserts": "^2.0", + "codeception/module-db": "^2.0", + "codeception/module-webdriver": "^2.0", "ergebnis/composer-normalize": "^2.15", + "helhum/config-loader": "^0.12.5", + "helhum/typo3-console": "^6.6 || ^7.0.5", "helmich/typo3-typoscript-lint": "^2.5", "jangregor/phpstan-prophecy": "^1.0", + "nikic/php-parser": "^4.12", + "oqq/codeception-email-mailhog": "^2.1", "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", + "symfony/process": "^4.4.30 || ^5.3.7", + "typo3/cms-dashboard": "~10.4.11 || ~11.5.1", + "typo3/cms-filelist": "~10.4.11 || ~11.5.1", + "typo3/cms-fluid-styled-content": "~10.4.11 || ~11.5.1", + "typo3/cms-lowlevel": "~10.4.11 || ~11.5.1", + "typo3/cms-scheduler": "~10.4.11 || ~11.5.1", + "typo3/cms-tstemplate": "~10.4.11 || ~11.5.1", "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 (~10.4.11 || ~11.5.1)", + "typo3/cms-scheduler": "Allows garbage collection of expired consents (~10.4.11 || ~11.5.1)" }, "autoload": { "psr-4": { @@ -65,6 +79,7 @@ "config": { "allow-plugins": { "captainhook/plugin-composer": true, + "codeception/c3": true, "ergebnis/composer-normalize": true, "typo3/class-alias-loader": true, "typo3/cms-composer-installers": true @@ -81,14 +96,32 @@ }, "scripts": { "post-autoload-dump": [ + "@environment:prepare", + "codecept build" + ], + "environment:prepare": [ + "@environment:prepare:extension", + "@environment:prepare:sites-config", + "@environment:prepare:additional-config" + ], + "environment:prepare:additional-config": [ + "[ -L .Build/web/typo3conf/AdditionalConfiguration.php ] || ln -snvf ../../../Tests/Build/AdditionalConfiguration.php .Build/web/typo3conf/AdditionalConfiguration.php" + ], + "environment:prepare:extension": [ "mkdir -p .Build/web/typo3conf/ext/", "[ -L .Build/web/typo3conf/ext/form_consent ] || ln -snvf ../../../../. .Build/web/typo3conf/ext/form_consent" ], + "environment:prepare:sites-config": [ + "mkdir -p config/sites/main", + "[ -L config/sites/main/config.yaml ] || ln -snvf ../../../Tests/Build/sites-config.yaml config/sites/main/config.yaml" + ], "lint": [ + "@lint:composer", "@lint:editorconfig", "@lint:php", "@lint:typoscript" ], + "lint:composer": "@composer normalize --no-check-lock --no-update-lock", "lint:editorconfig": "ec --fix -e .Build", "lint:php": "php-cs-fixer fix", "lint:typoscript": "typoscript-lint -c typoscript-lint.yml", @@ -97,13 +130,34 @@ ], "sca:php": "phpstan analyse -c phpstan.neon", "test": [ + "@test:acceptance", "@test:functional", "@test:unit" ], + "test:acceptance": [ + "@test:acceptance:prepare", + "@test:acceptance:run" + ], + "test:acceptance:prepare": [ + "mysql -Nse 'show tables' db | while read table; do mysql -e \"truncate table $table\" db; done", + "typo3cms install:setup --no-interaction --force" + ], + "test:acceptance:run": "codecept run", "test:ci": [ + "@test:ci:acceptance", "@test:ci:functional", "@test:ci:unit" ], + "test:ci:acceptance": [ + "@test:ci:acceptance:prepare", + "@test:ci:acceptance:run" + ], + "test:ci:acceptance:prepare": "@test:acceptance:prepare", + "test:ci:acceptance:run": [ + "@test:acceptance:run --coverage --coverage-html --debug", + "mkdir -p .Build/log/coverage/php", + "cp .Build/log/acceptance-reports/coverage.serialized .Build/log/coverage/php/acceptance.cov" + ], "test:ci:functional": "phpunit -c phpunit.ci.functional.xml", "test:ci:merge": "phpcov merge --html .Build/log/coverage/html/_merged --clover .Build/log/coverage/clover.xml --text php://stdout .Build/log/coverage/php", "test:ci:unit": "phpunit -c phpunit.ci.unit.xml", diff --git a/dependency-checker.json b/dependency-checker.json new file mode 100644 index 00000000..b0304dc0 --- /dev/null +++ b/dependency-checker.json @@ -0,0 +1,8 @@ +{ + "scan-files": [ + "ext_*.php", + "Configuration/TCA/*.php", + "Configuration/TCA/Overrides/*.php", + "Configuration/*.php" + ] +} diff --git a/ext_conf_template.txt b/ext_conf_template.txt new file mode 100644 index 00000000..14852a34 --- /dev/null +++ b/ext_conf_template.txt @@ -0,0 +1,2 @@ +# cat=persistence/10; type=string; label=Excluded elements from persistence:Define all element types that should be excluded from persistence (comma-separated list) +persistence.excludedElements = Honeypot diff --git a/ext_emconf.php b/ext_emconf.php index 0297eec9..46eb738c 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -26,14 +26,12 @@ 'category' => 'fe', 'author' => 'Elias Häußler', 'author_email' => 'elias@haeussler.dev', - 'state' => 'alpha', - 'uploadfolder' => false, - 'createDirs' => '', + 'state' => 'beta', 'clearCacheOnLoad' => false, - 'version' => '0.2.2', + 'version' => '0.3.0', 'constraints' => [ 'depends' => [ - 'typo3' => '10.4.0-11.5.99', + 'typo3' => '10.4.11-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), diff --git a/packaging_exclude.php b/packaging_exclude.php index 82884247..19176ef3 100644 --- a/packaging_exclude.php +++ b/packaging_exclude.php @@ -36,9 +36,13 @@ ], 'files' => [ 'DS_Store', + 'c3.php', 'captainhook.json', + 'codeception.yml', + 'codecov.yml', 'composer.lock', 'crowdin.yaml', + 'dependency-checker.json', 'editorconfig', 'gitattributes', 'gitignore', @@ -50,7 +54,6 @@ 'phpunit.ci.unit.xml', 'phpunit.functional.xml', 'phpunit.unit.xml', - 'sonar-project.properties', 'typoscript-lint.yml', ], ]; diff --git a/phpstan.neon b/phpstan.neon index a69a11b1..70f13839 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,3 +10,5 @@ parameters: - Classes - Configuration - Tests + excludePaths: + - Tests/Acceptance/Support/_generated/* 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/**