From bb417ac146e3dc794a8536264b987e78a0e2d319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Jania?= Date: Tue, 27 Oct 2026 10:42:44 +0100 Subject: [PATCH] feat: add TYPO3 v14 compatibility, release 3.0.0 - Require TYPO3 ^14.0 and PHP 8.2 - 8.5, bump version to 3.0.0 - Register plugin as own CType "riddle_riddle" with flexform via addPlugin() - Add upgrade wizard to migrate list_type plugins and be_groups permissions - Move backend module to Configuration/Backend/Modules.php, use ModuleTemplateFactory - Move icons to Configuration/Icons.php, register TypoScript via addTypoScript() - Replace removed APIs (StandaloneView, Doctrine DBAL 3 methods, FlexFormService, AbstractMessage, TCEforms flexform wrapper, list_type preview handling) - Use constructor DI for API, repository, middleware, controllers and event listener - Mark PluginController::run() as #[AsAllowedCallable] for TypoScript userFunc - Allow cdn.riddle.com images in backend Content-Security-Policy - Fix log module filter, pagination and CSV export (response instead of die) - Replace copied tx_riddle_log TCA with a proper read-only definition - Handle missing API key and unreachable riddle API gracefully in preview and frontend - Replace legacy runTests/docker setup with GitHub Actions CI (composer validate, CGL, lint, unit tests on PHP 8.2 - 8.5, tag/version check) - Rewrite unit tests for PHPUnit 11/12 and testing-framework 9 --- .gitattributes | 5 +- .github/workflows/ci.yml | 80 +++ .github/workflows/core12.yml | 30 -- .gitignore | 12 +- Build/Scripts/runTests.sh | 481 ------------------ Build/php-cs-fixer/php-cs-fixer.php | 74 +-- Build/phpunit/FunctionalTests.xml | 52 -- Build/phpunit/FunctionalTestsBootstrap.php | 30 -- Build/phpunit/UnitTests.xml | 54 +- Build/phpunit/UnitTestsBootstrap.php | 85 ---- Build/testing-docker/.env | 18 - Build/testing-docker/docker-compose.yml | 427 ---------------- Classes/Api/RiddleApi.php | 76 +-- .../Element/SelectSingleElementWithNoIcon.php | 15 +- .../BackendPreviewEventListener.php | 78 ++- Classes/Backend/FieldWizard/ExternalIcons.php | 52 +- Classes/Controller/ManagementController.php | 140 ++--- Classes/Controller/PluginController.php | 48 +- Classes/Domain/Dto/LogDemand.php | 9 +- Classes/Domain/LogItem.php | 9 +- Classes/Domain/Repository/LogRepository.php | 101 ++-- .../ApiConfigurationMissingException.php | 11 +- Classes/ExtensionConfiguration.php | 31 +- Classes/Hooks/ItemsProcFunc.php | 34 +- Classes/Middleware/WebhookEndpoint.php | 36 +- .../Upgrades/PluginListTypeToCTypeUpdate.php | 36 ++ Classes/Utility/RiddleUtility.php | 26 +- Configuration/Backend/Modules.php | 18 + Configuration/ContentSecurityPolicies.php | 17 + Configuration/FlexForms/flexform_riddle.xml | 44 +- Configuration/Icons.php | 14 + Configuration/Services.yaml | 14 +- Configuration/TCA/Overrides/tt_content.php | 25 +- Configuration/TCA/tx_riddle_log.php | 45 +- .../TSconfig/ContentElementWizard.typoscript | 13 - Documentation/Settings.cfg | 4 +- Readme.md | 28 +- Resources/Private/Language/de.locallang.xlf | 46 +- Resources/Private/Language/locallang.xlf | 33 ++ .../Private/Layouts/LogAdministration.html | 2 - Resources/Private/Partials/Pagination.html | 127 ++--- .../Private/Templates/BackendPreview.html | 101 ++-- .../Templates/Management/Overview.html | 91 ++-- Tests/Unit/Api/RiddleApiTest.php | 177 +++---- .../SelectSingleElementWithNoIconTest.php | 80 --- .../Backend/FieldWizard/ExternalIconsTest.php | 67 --- Tests/Unit/Domain/LogItemTest.php | 49 ++ Tests/Unit/ExtensionConfigurationTest.php | 74 --- Tests/Unit/Hooks/ItemsProcFuncTest.php | 99 ++-- Tests/Unit/Hooks/PageLayoutViewTest.php | 94 ---- Tests/Unit/Plugin/RiddlePluginTest.php | 91 ---- Tests/Unit/Utility/RiddleUtilityTest.php | 112 ++-- composer.json | 60 +-- ext_emconf.php | 5 +- ext_localconf.php | 45 +- ext_tables.php | 15 - ext_typoscript_setup.typoscript | 5 - 57 files changed, 1039 insertions(+), 2606 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/core12.yml delete mode 100755 Build/Scripts/runTests.sh delete mode 100644 Build/phpunit/FunctionalTests.xml delete mode 100644 Build/phpunit/FunctionalTestsBootstrap.php delete mode 100644 Build/phpunit/UnitTestsBootstrap.php delete mode 100644 Build/testing-docker/.env delete mode 100644 Build/testing-docker/docker-compose.yml create mode 100644 Classes/Upgrades/PluginListTypeToCTypeUpdate.php create mode 100644 Configuration/Backend/Modules.php create mode 100644 Configuration/ContentSecurityPolicies.php create mode 100644 Configuration/Icons.php delete mode 100644 Configuration/TSconfig/ContentElementWizard.typoscript delete mode 100644 Resources/Private/Layouts/LogAdministration.html delete mode 100644 Tests/Unit/Backend/Element/SelectSingleElementWithNoIconTest.php delete mode 100644 Tests/Unit/Backend/FieldWizard/ExternalIconsTest.php create mode 100644 Tests/Unit/Domain/LogItemTest.php delete mode 100644 Tests/Unit/ExtensionConfigurationTest.php delete mode 100644 Tests/Unit/Hooks/PageLayoutViewTest.php delete mode 100644 Tests/Unit/Plugin/RiddlePluginTest.php delete mode 100644 ext_tables.php delete mode 100644 ext_typoscript_setup.typoscript diff --git a/.gitattributes b/.gitattributes index 78681fe..c1108e7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,5 @@ +/.github/ export-ignore /Build/ export-ignore /Tests/ export-ignore /.gitattributes export-ignore -/.github/ export-ignore /.gitignore export-ignore -/.phplint.yml export-ignore -/.php_cs.cache -/Todos.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2b3f291 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + branches: [ master ] + tags: [ '*' ] + pull_request: + +permissions: + contents: read + +jobs: + composer-validate: + name: Composer validate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + - run: composer validate --strict --no-check-lock + + cgl: + name: Coding guidelines + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + - run: composer update --no-progress --prefer-dist + - run: composer ci:cgl + + tests: + name: TYPO3 ${{ matrix.typo3 }} / PHP ${{ matrix.php }} (${{ matrix.dependencies }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + typo3: [ '^14.0' ] + php: [ '8.2', '8.3', '8.4', '8.5' ] + dependencies: [ 'highest' ] + include: + - typo3: '^14.0' + php: '8.2' + dependencies: 'lowest' + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + - name: Lint PHP + run: composer ci:lint + - name: Install dependencies + run: | + composer require --no-update --no-progress \ + typo3/cms-backend:${{ matrix.typo3 }} \ + typo3/cms-core:${{ matrix.typo3 }} \ + typo3/cms-fluid:${{ matrix.typo3 }} \ + typo3/cms-frontend:${{ matrix.typo3 }} + composer update --no-progress --prefer-dist ${{ matrix.dependencies == 'lowest' && '--prefer-lowest' || '' }} + - name: Unit tests + run: composer ci:tests:unit + + release-version: + name: Tag matches ext_emconf version + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Compare tag with ext_emconf.php + run: | + TAG="${GITHUB_REF_NAME#v}" + EMCONF=$(sed -n "s/.*'version' => '\([^']*\)'.*/\1/p" ext_emconf.php) + echo "tag=$TAG ext_emconf=$EMCONF" + test "$TAG" = "$EMCONF" diff --git a/.github/workflows/core12.yml b/.github/workflows/core12.yml deleted file mode 100644 index ae2c42f..0000000 --- a/.github/workflows/core12.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: core 12 - -on: [ push, pull_request ] - -jobs: - tests: - name: v12 - runs-on: ubuntu-20.04 - strategy: - # This prevents cancellation of matrix job runs, if one/two already failed and let the - # rest matrix jobs be executed anyway. - fail-fast: false - matrix: - php: [ '8.1', '8.2' ] - composerInstall: [ 'composerInstallLowest', 'composerInstallHighest' ] - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install testing system - run: Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php }} -s ${{ matrix.composerInstall }} - - - name: Lint PHP - run: Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php }} -s lint - - - name: Validate code against CGL - run: PHP_CS_FIXER_IGNORE_ENV=1 Build/Scripts/runTests.sh -t 12 -p 8.1 -s cgl -n - -# - name: Unit Tests -# run: Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php }} -s unit \ No newline at end of file diff --git a/.gitignore b/.gitignore index cfbb31e..58bfbec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ -Todos.md -.php_cs.cache -/Documentation-GENERATED-temp -.build/ -Build/.phpunit.result.cache -composer.lock +/.Build/ /.php-cs-fixer.cache -/var \ No newline at end of file +/.phpunit.cache/ +/Documentation-GENERATED-temp/ +/composer.lock +/var/ diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh deleted file mode 100755 index e69fa03..0000000 --- a/Build/Scripts/runTests.sh +++ /dev/null @@ -1,481 +0,0 @@ -#!/usr/bin/env bash - -# -# TYPO3 core test runner based on docker and docker-compose. -# -IMAGE_PREFIX="ghcr.io/typo3/" - -# Function to write a .env file in Build/testing-docker -# This is read by docker-compose and vars defined here are -# used in Build/testing-docker/docker-compose.yml -setUpDockerComposeDotEnv() { - # Delete possibly existing local .env file if exists - [ -e .env ] && rm .env - # Set up a new .env file for docker-compose - { - echo "COMPOSE_PROJECT_NAME=${PROJECT_NAME}" - # To prevent access rights of files created by the testing, the docker image later - # runs with the same user that is currently executing the script. docker-compose can't - # use $UID directly itself since it is a shell variable and not an env variable, so - # we have to set it explicitly here. - echo "HOST_UID=`id -u`" - # Your local user - echo "ROOT_DIR=${ROOT_DIR}" - echo "HOST_USER=${USER}" - echo "TEST_FILE=${TEST_FILE}" - echo "TYPO3_VERSION=${TYPO3_VERSION}" - echo "PHP_XDEBUG_ON=${PHP_XDEBUG_ON}" - echo "PHP_XDEBUG_PORT=${PHP_XDEBUG_PORT}" - echo "DOCKER_PHP_IMAGE=${DOCKER_PHP_IMAGE}" - echo "EXTRA_TEST_OPTIONS=${EXTRA_TEST_OPTIONS}" - echo "SCRIPT_VERBOSE=${SCRIPT_VERBOSE}" - echo "CGLCHECK_DRY_RUN=${CGLCHECK_DRY_RUN}" - echo "DATABASE_DRIVER=${DATABASE_DRIVER}" - echo "MARIADB_VERSION=${MARIADB_VERSION}" - echo "MYSQL_VERSION=${MYSQL_VERSION}" - echo "POSTGRES_VERSION=${POSTGRES_VERSION}" - echo "USED_XDEBUG_MODES=${USED_XDEBUG_MODES}" - echo "IMAGE_PREFIX=${IMAGE_PREFIX}" - } > .env -} - -# Options -a and -d depend on each other. The function -# validates input combinations and sets defaults. -handleDbmsAndDriverOptions() { - case ${DBMS} in - mysql|mariadb) - [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" - if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then - echo "Invalid option -a ${DATABASE_DRIVER} with -d ${DBMS}" >&2 - echo >&2 - echo "call \"./Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 - exit 1 - fi - ;; - postgres|sqlite) - if [ -n "${DATABASE_DRIVER}" ]; then - echo "Invalid option -a ${DATABASE_DRIVER} with -d ${DBMS}" >&2 - echo >&2 - echo "call \"./Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 - exit 1 - fi - ;; - esac -} - -# Load help text into $HELP -read -r -d '' HELP <=20.10 for xdebug break pointing to work reliably, and -a recent docker-compose (tested >=1.21.2) is needed. - -Usage: $0 [options] [file] - -No arguments: Run all unit tests with PHP 7.4 - -Options: - -s <...> - Specifies which test suite to run - - cgl: cgl test and fix all php files - - clean: clean up build and testing related files - - composer: Execute "composer" command, using -e for command arguments pass-through, ex. -e "ci:php:stan" - - composerInstall: "composer update", handy if host has no PHP - - composerInstallLowest: "composer update", handy if host has no PHP - - composerInstallHighest: "composer update", handy if host has no PHP - - functional: functional tests - - lint: PHP linting - - unit: PHP unit tests - - -a - Only with -s acceptance,functional - Specifies to use another driver, following combinations are available: - - mysql - - mysqli (default) - - pdo_mysql - - mariadb - - mysqli (default) - - pdo_mysql - - -d - Only with -s acceptance,functional - Specifies on which DBMS tests are performed - - sqlite: (default) use sqlite - - mariadb: use mariadb - - mysql: use mysql - - postgres: use postgres - - -i <10.2|10.3|10.4|10.5|10.6|10.7> - Only with -d mariadb - Specifies on which version of mariadb tests are performed - - 10.2 (default) - - 10.3 - - 10.4 - - 10.5 - - 10.6 - - 10.7 - - -j <5.5|5.6|5.7|8.0> - Only with -d mysql - Specifies on which version of mysql tests are performed - - 5.5 (default) - - 5.6 - - 5.7 - - 8.0 - - -k <10|11|12|13|14> - Only with -d postgres - Specifies on which version of postgres tests are performed - - 10 (default) - - 11 - - 12 - - 13 - - 14 - - -p <7.4|8.0|8.1|8.2> - Specifies the PHP minor version to be used - - 7.4 (default): use PHP 7.4 - - 8.0: use PHP 8.0 - - 8.1: use PHP 8.1 - - 8.2: use PHP 8.2 - - -t <11|12> - Only with -s composerUpdate - Specifies the TYPO3 core major version to be used - - 11 (default): use TYPO3 core v11 - - 12: use TYPO3 core v12 - - -e "" - Only with -s functional|unit|composer - Additional options to send to phpunit (unit & functional tests) or codeception (acceptance - tests). For phpunit, options starting with "--" must be added after options starting with "-". - Example -e "-v --filter canRetrieveValueWithGP" to enable verbose output AND filter tests - named "canRetrieveValueWithGP" - - -x - Only with -s functional|unit - Send information to host instance for test or system under test break points. This is especially - useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port - can be selected with -y - - -y - Send xdebug information to a different port than default 9003 if an IDE like PhpStorm - is not listening on default port. - - -z - Only with -x and -s functional|unit|acceptance - This sets the used xdebug modes. Defaults to 'debug,develop' - - -n - Only with -s cgl - Activate dry-run in CGL check that does not actively change files and only prints broken ones. - - -u - Update existing ${IMAGE_PREFIX}core-testing-*:latest docker images. Maintenance call to docker pull latest - versions of the main php images. The images are updated once in a while and only the youngest - ones are supported by core testing. Use this if weird test errors occur. Also removes obsolete - image versions of ${IMAGE_PREFIX}core-testing-*. - - -v - Enable verbose script output. Shows variables and docker commands. - - -h - Show this help. - -Examples: - # Run unit tests using PHP 7.4 - ./Build/Scripts/runTests.sh -s unit -EOF - -# Test if docker-compose exists, else exit out with error -if ! type "docker-compose" > /dev/null; then - echo "This script relies on docker and docker-compose. Please install" >&2 - exit 1 -fi - -# Go to the directory this script is located, so everything else is relative -# to this dir, no matter from where this script is called. -THIS_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd "$THIS_SCRIPT_DIR" || exit 1 - -# Go to directory that contains the local docker-compose.yml file -cd ../testing-docker || exit 1 - -# Option defaults -if ! command -v realpath &> /dev/null; then - echo "This script works best with realpath installed" >&2 - ROOT_DIR="${PWD}/../../" -else - ROOT_DIR=`realpath ${PWD}/../../` -fi -TEST_SUITE="" -DBMS="sqlite" -PHP_VERSION="7.4" -TYPO3_VERSION="11" -PHP_XDEBUG_ON=0 -PHP_XDEBUG_PORT=9003 -EXTRA_TEST_OPTIONS="" -SCRIPT_VERBOSE=0 -CGLCHECK_DRY_RUN="" -DATABASE_DRIVER="" -MARIADB_VERSION="10.2" -MYSQL_VERSION="5.5" -POSTGRES_VERSION="10" -USED_XDEBUG_MODES="debug,develop" -#@todo the $$ would add the current process id to the name, keeping as plan b -#PROJECT_NAME="runTests-$(basename $(dirname $ROOT_DIR))-$(basename $ROOT_DIR)-$$" -PROJECT_NAME="runtestsriddle" -PROJECT_NAME="${PROJECT_NAME//[[:blank:]]/}" -echo $PROJECT_NAME - -# Option parsing -# Reset in case getopts has been used previously in the shell -OPTIND=1 -# Array for invalid options -INVALID_OPTIONS=(); -# Simple option parsing based on getopts (! not getopt) -while getopts ":s:a:d:i:j:k:p:t:e:xy:z:nhuv" OPT; do - case ${OPT} in - s) - TEST_SUITE=${OPTARG} - ;; - a) - DATABASE_DRIVER=${OPTARG} - ;; - d) - DBMS=${OPTARG} - ;; - i) - MARIADB_VERSION=${OPTARG} - if ! [[ ${MARIADB_VERSION} =~ ^(10.2|10.3|10.4|10.5|10.6|10.7)$ ]]; then - INVALID_OPTIONS+=("${OPTARG}") - fi - ;; - j) - MYSQL_VERSION=${OPTARG} - if ! [[ ${MYSQL_VERSION} =~ ^(5.5|5.6|5.7|8.0)$ ]]; then - INVALID_OPTIONS+=("${OPTARG}") - fi - ;; - k) - POSTGRES_VERSION=${OPTARG} - if ! [[ ${POSTGRES_VERSION} =~ ^(10|11|12|13|14)$ ]]; then - INVALID_OPTIONS+=("${OPTARG}") - fi - ;; - p) - PHP_VERSION=${OPTARG} - if ! [[ ${PHP_VERSION} =~ ^(7.4|8.0|8.1|8.2)$ ]]; then - INVALID_OPTIONS+=("p ${OPTARG}") - fi - ;; - t) - TYPO3_VERSION=${OPTARG} - if ! [[ ${TYPO3_VERSION} =~ ^(11|12)$ ]]; then - INVALID_OPTIONS+=("p ${OPTARG}") - fi - ;; - e) - EXTRA_TEST_OPTIONS=${OPTARG} - ;; - x) - PHP_XDEBUG_ON=1 - ;; - y) - PHP_XDEBUG_PORT=${OPTARG} - ;; - z) - USED_XDEBUG_MODES=${OPTARG} - ;; - h) - echo "${HELP}" - exit 0 - ;; - n) - CGLCHECK_DRY_RUN="-n" - ;; - u) - TEST_SUITE=update - ;; - v) - SCRIPT_VERBOSE=1 - ;; - \?) - INVALID_OPTIONS+=(${OPTARG}) - ;; - :) - INVALID_OPTIONS+=(${OPTARG}) - ;; - esac -done - -# Exit on invalid options -if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then - echo "Invalid option(s):" >&2 - for I in "${INVALID_OPTIONS[@]}"; do - echo "-"${I} >&2 - done - echo >&2 - echo "${HELP}" >&2 - exit 1 -fi - -# Move "7.2" to "php72", the latter is the docker container name -DOCKER_PHP_IMAGE=`echo "php${PHP_VERSION}" | sed -e 's/\.//'` - -# Set $1 to first mass argument, this is the optional test file or test directory to execute -shift $((OPTIND - 1)) -TEST_FILE=${1} - -if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x -fi - -if [ -z ${TEST_SUITE} ]; then - echo "${HELP}" - exit 0 -fi - -# Suite execution -case ${TEST_SUITE} in - cgl) - # Active dry-run for cgl needs not "-n" but specific options - if [[ ! -z ${CGLCHECK_DRY_RUN} ]]; then - CGLCHECK_DRY_RUN="--dry-run --diff" - fi - setUpDockerComposeDotEnv - docker-compose run cgl - SUITE_EXIT_CODE=$? - docker-compose down - ;; - clean) - rm -rf \ - ../../var/ \ - ../../.cache \ - ../../composer.lock \ - ../../.Build/ \ - ../../Tests/Acceptance/Support/_generated/ \ - ../../composer.json.testing - ;; - composer) - setUpDockerComposeDotEnv - docker-compose run composer - SUITE_EXIT_CODE=$? - docker-compose down - ;; - composerInstall) - setUpDockerComposeDotEnv - cp ../../composer.json ../../composer.json.orig - if [ -f "../../composer.json.testing" ]; then - cp ../../composer.json ../../composer.json.orig - fi - docker-compose run composer_install - cp ../../composer.json ../../composer.json.testing - mv ../../composer.json.orig ../../composer.json - SUITE_EXIT_CODE=$? - docker-compose down - ;; - composerInstallLowest) - setUpDockerComposeDotEnv - cp ../../composer.json ../../composer.json.orig - if [ -f "../../composer.json.testing" ]; then - cp ../../composer.json ../../composer.json.orig - fi - docker-compose run composer_install_lowest - cp ../../composer.json ../../composer.json.testing - mv ../../composer.json.orig ../../composer.json - SUITE_EXIT_CODE=$? - docker-compose down - ;; - composerInstallHighest) - setUpDockerComposeDotEnv - cp ../../composer.json ../../composer.json.orig - if [ -f "../../composer.json.testing" ]; then - cp ../../composer.json ../../composer.json.orig - fi - docker-compose run composer_install_highest - cp ../../composer.json ../../composer.json.testing - mv ../../composer.json.orig ../../composer.json - SUITE_EXIT_CODE=$? - docker-compose down - ;; - coveralls) - setUpDockerComposeDotEnv - docker-compose run coveralls - SUITE_EXIT_CODE=$? - docker-compose down - ;; - functional) - handleDbmsAndDriverOptions - setUpDockerComposeDotEnv - case ${DBMS} in - mariadb) - echo "Using driver: ${DATABASE_DRIVER}" - docker-compose run functional_mariadb - SUITE_EXIT_CODE=$? - ;; - mysql) - echo "Using driver: ${DATABASE_DRIVER}" - docker-compose run functional_mysql - SUITE_EXIT_CODE=$? - ;; - postgres) - docker-compose run functional_postgres - SUITE_EXIT_CODE=$? - ;; - sqlite) - # sqlite has a tmpfs as Web/typo3temp/var/tests/functional-sqlite-dbs/ - # Since docker is executed as root (yay!), the path to this dir is owned by - # root if docker creates it. Thank you, docker. We create the path beforehand - # to avoid permission issues. - mkdir -p ${ROOT_DIR}/Web/typo3temp/var/tests/functional-sqlite-dbs/ - docker-compose run functional_sqlite - SUITE_EXIT_CODE=$? - ;; - *) - echo "Invalid -d option argument ${DBMS}" >&2 - echo >&2 - echo "${HELP}" >&2 - exit 1 - esac - docker-compose down - ;; - lint) - setUpDockerComposeDotEnv - docker-compose run lint - SUITE_EXIT_CODE=$? - docker-compose down - ;; - phpstan) - setUpDockerComposeDotEnv - docker-compose run phpstan - SUITE_EXIT_CODE=$? - docker-compose down - ;; - phpstanGenerateBaseline) - setUpDockerComposeDotEnv - docker-compose run phpstan_generate_baseline - SUITE_EXIT_CODE=$? - docker-compose down - ;; - unit) - setUpDockerComposeDotEnv - docker-compose run unit - SUITE_EXIT_CODE=$? - docker-compose down - ;; - update) - # pull ${IMAGE_PREFIX}core-testing-*:latest versions of those ones that exist locally - docker images ${IMAGE_PREFIX}core-testing-*:latest --format "{{.Repository}}:latest" | xargs -I {} docker pull {} - # remove "dangling" ${IMAGE_PREFIX}core-testing-* images (those tagged as ) - docker images ${IMAGE_PREFIX}core-testing-* --filter "dangling=true" --format "{{.ID}}" | xargs -I {} docker rmi {} - ;; - *) - echo "Invalid -s option argument ${TEST_SUITE}" >&2 - echo >&2 - echo "${HELP}" >&2 - exit 1 -esac - -exit $SUITE_EXIT_CODE diff --git a/Build/php-cs-fixer/php-cs-fixer.php b/Build/php-cs-fixer/php-cs-fixer.php index 9533bde..964479d 100644 --- a/Build/php-cs-fixer/php-cs-fixer.php +++ b/Build/php-cs-fixer/php-cs-fixer.php @@ -8,69 +8,13 @@ LICENSE.txt file that was distributed with this source code.', true ); -$config->setFinder( - (new PhpCsFixer\Finder()) - ->in(realpath(__DIR__ . '/../../')) - ->ignoreVCSIgnored(true) - ->notPath('/^.Build\//') - ->notPath('/^Build\/php-cs-fixer\/php-cs-fixer.php/') - ->notPath('/^Build\/phpunit\/(UnitTestsBootstrap|FunctionalTestsBootstrap).php/') - ->notPath('/^Configuration\//') - ->notPath('/^Documentation\//') - ->notName('/^ext_(emconf|localconf|tables).php/') -) - ->setRiskyAllowed(true) - ->setRules([ - '@DoctrineAnnotation' => true, - '@PER' => true, - 'array_syntax' => ['syntax' => 'short'], - 'blank_line_after_opening_tag' => true, - 'braces' => ['allow_single_line_closure' => true], - 'cast_spaces' => ['space' => 'none'], - 'compact_nullable_typehint' => true, - 'concat_space' => ['spacing' => 'one'], - 'declare_equal_normalize' => ['space' => 'none'], - 'dir_constant' => true, - 'function_typehint_space' => true, - 'lowercase_cast' => true, - 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], - 'modernize_types_casting' => true, - 'native_function_casing' => true, - 'new_with_braces' => true, - 'no_alias_functions' => true, - 'no_blank_lines_after_phpdoc' => true, - 'no_empty_phpdoc' => true, - 'no_empty_statement' => true, - 'no_extra_blank_lines' => true, - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_null_property_initialization' => true, - 'no_short_bool_cast' => true, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_superfluous_elseif' => true, - 'no_trailing_comma_in_singleline' => true, - 'no_unneeded_control_parentheses' => true, - 'no_unused_imports' => true, - 'no_useless_else' => true, - 'no_useless_nullsafe_operator' => true, - 'no_whitespace_in_blank_line' => true, - 'ordered_imports' => true, - 'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']], - 'php_unit_mock_short_will_return' => true, - 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], - 'phpdoc_no_access' => true, - 'phpdoc_no_empty_return' => true, - 'phpdoc_no_package' => true, - 'phpdoc_scalar' => true, - 'phpdoc_trim' => true, - 'phpdoc_types' => true, - 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], - 'return_type_declaration' => ['space_before' => 'none'], - 'single_quote' => true, - 'single_line_comment_style' => ['comment_types' => ['hash']], - 'single_trait_insert_per_statement' => true, - 'trailing_comma_in_multiline' => ['elements' => ['arrays']], - 'whitespace_after_comma_in_array' => ['ensure_single_space' => true], - 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], - ]); +$config->getFinder() + ->in(realpath(__DIR__ . '/../../')) + ->ignoreVCSIgnored(true) + ->notPath('/^.Build\//') + ->notPath('/^Build\/php-cs-fixer\/php-cs-fixer.php/') + ->notPath('/^Configuration\//') + ->notPath('/^Documentation\//') + ->notName('/^ext_(emconf|localconf|tables).php/'); + return $config; diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml deleted file mode 100644 index 80156d2..0000000 --- a/Build/phpunit/FunctionalTests.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - ../../Tests/Functional/ - - - - - - - - - - diff --git a/Build/phpunit/FunctionalTestsBootstrap.php b/Build/phpunit/FunctionalTestsBootstrap.php deleted file mode 100644 index a95bc52..0000000 --- a/Build/phpunit/FunctionalTestsBootstrap.php +++ /dev/null @@ -1,30 +0,0 @@ -defineOriginalRootPath(); - $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); - $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); -})(); diff --git a/Build/phpunit/UnitTests.xml b/Build/phpunit/UnitTests.xml index d09d8ef..673b79a 100644 --- a/Build/phpunit/UnitTests.xml +++ b/Build/phpunit/UnitTests.xml @@ -1,45 +1,31 @@ + - - ../../Tests/Unit/ - - + + diff --git a/Build/phpunit/UnitTestsBootstrap.php b/Build/phpunit/UnitTestsBootstrap.php deleted file mode 100644 index 8b4ead3..0000000 --- a/Build/phpunit/UnitTestsBootstrap.php +++ /dev/null @@ -1,85 +0,0 @@ -getWebRoot(), '/')); - } - if (!getenv('TYPO3_PATH_WEB')) { - putenv('TYPO3_PATH_WEB=' . rtrim($testbase->getWebRoot(), '/')); - } - - $testbase->defineSitePath(); - - $composerMode = defined('TYPO3_COMPOSER_MODE') && TYPO3_COMPOSER_MODE === true; - $requestType = \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_BE | \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_CLI; - \TYPO3\TestingFramework\Core\SystemEnvironmentBuilder::run(0, $requestType, $composerMode); - - $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3conf/ext'); - $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/assets'); - $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/tests'); - $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/transient'); - - // Retrieve an instance of class loader and inject to core bootstrap - $classLoader = require $testbase->getPackagesPath() . '/autoload.php'; - \TYPO3\CMS\Core\Core\Bootstrap::initializeClassLoader($classLoader); - - // Initialize default TYPO3_CONF_VARS - $configurationManager = new \TYPO3\CMS\Core\Configuration\ConfigurationManager(); - $GLOBALS['TYPO3_CONF_VARS'] = $configurationManager->getDefaultConfiguration(); - - $cache = new \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend( - 'core', - new \TYPO3\CMS\Core\Cache\Backend\NullBackend('production', []) - ); - - // Set all packages to active - if (interface_exists(\TYPO3\CMS\Core\Package\Cache\PackageCacheInterface::class)) { - $packageManager = \TYPO3\CMS\Core\Core\Bootstrap::createPackageManager(\TYPO3\CMS\Core\Package\UnitTestPackageManager::class, \TYPO3\CMS\Core\Core\Bootstrap::createPackageCache($cache)); - } else { - // v10 compatibility layer - // @deprecated Will be removed when v10 compat is dropped from testing-framework - $packageManager = \TYPO3\CMS\Core\Core\Bootstrap::createPackageManager(\TYPO3\CMS\Core\Package\UnitTestPackageManager::class, $cache); - } - - \TYPO3\CMS\Core\Utility\GeneralUtility::setSingletonInstance(\TYPO3\CMS\Core\Package\PackageManager::class, $packageManager); - \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::setPackageManager($packageManager); - - $testbase->dumpClassLoadingInformation(); - - \TYPO3\CMS\Core\Utility\GeneralUtility::purgeInstances(); -})(); diff --git a/Build/testing-docker/.env b/Build/testing-docker/.env deleted file mode 100644 index 5381120..0000000 --- a/Build/testing-docker/.env +++ /dev/null @@ -1,18 +0,0 @@ -COMPOSE_PROJECT_NAME=runtestsriddle -HOST_UID=502 -ROOT_DIR=/Users/georg.ringer/projects/priv/t3-12/packages/riddle -HOST_USER=georg.ringer -TEST_FILE= -TYPO3_VERSION=12 -PHP_XDEBUG_ON=0 -PHP_XDEBUG_PORT=9003 -DOCKER_PHP_IMAGE=php82 -EXTRA_TEST_OPTIONS= -SCRIPT_VERBOSE=0 -CGLCHECK_DRY_RUN= -DATABASE_DRIVER= -MARIADB_VERSION=10.2 -MYSQL_VERSION=5.5 -POSTGRES_VERSION=10 -USED_XDEBUG_MODES=debug,develop -IMAGE_PREFIX=ghcr.io/typo3/ diff --git a/Build/testing-docker/docker-compose.yml b/Build/testing-docker/docker-compose.yml deleted file mode 100644 index 8fcbd83..0000000 --- a/Build/testing-docker/docker-compose.yml +++ /dev/null @@ -1,427 +0,0 @@ -version: '2.3' -services: - #--------------------------------------------------------------------------------------------------------------------- - # additional services needed for functional tests to be linked, e.g. databases - #--------------------------------------------------------------------------------------------------------------------- - mysql: - image: mysql:${MYSQL_VERSION} - environment: - MYSQL_ROOT_PASSWORD: funcp - tmpfs: - - /var/lib/mysql/:rw,noexec,nosuid - - mariadb: - image: mariadb:${MARIADB_VERSION} - environment: - MYSQL_ROOT_PASSWORD: funcp - tmpfs: - - /var/lib/mysql/:rw,noexec,nosuid - - postgres: - image: postgres:${POSTGRES_VERSION}-alpine - environment: - POSTGRES_PASSWORD: funcp - POSTGRES_USER: funcu - tmpfs: - - /var/lib/postgresql/data:rw,noexec,nosuid - - #--------------------------------------------------------------------------------------------------------------------- - # composer related services - #--------------------------------------------------------------------------------------------------------------------- - composer: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - working_dir: ${ROOT_DIR} - environment: - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - php -v | grep '^PHP'; - composer ${EXTRA_TEST_OPTIONS}; - " - - composer_install: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - working_dir: ${ROOT_DIR} - environment: - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - php -v | grep '^PHP'; - if [ ${TYPO3_VERSION} -eq 11 ]; then - composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^11.5.24 - fi - if [ ${TYPO3_VERSION} -eq 12 ]; then - composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^12.2 - fi - composer install --no-progress; - " - - composer_install_lowest: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - working_dir: ${ROOT_DIR} - environment: - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - php -v | grep '^PHP'; - if [ ${TYPO3_VERSION} -eq 11 ]; then - composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^11.5.24 - fi - if [ ${TYPO3_VERSION} -eq 12 ]; then - composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^12.0 - fi - composer update --no-ansi --no-interaction --no-progress --with-dependencies --prefer-lowest; - composer show; - " - - composer_install_highest: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - working_dir: ${ROOT_DIR} - environment: - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - if [ ${TYPO3_VERSION} -eq 11 ]; then - composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^11.5.24 - fi - if [ ${TYPO3_VERSION} -eq 12 ]; then - composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^12.2 - fi - composer update --no-progress --no-interaction; - composer show; - " - - #--------------------------------------------------------------------------------------------------------------------- - # unit tests - #--------------------------------------------------------------------------------------------------------------------- - unit: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - working_dir: ${ROOT_DIR} - extra_hosts: - - "host.docker.internal:host-gateway" - environment: - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - php -v | grep '^PHP'; - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - .Build/bin/phpunit -c Build/phpunit/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; - else - XDEBUG_MODE=\"${USED_XDEBUG_MODES}\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_host=host.docker.internal\" \ - .Build/bin/phpunit -c Build/phpunit/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; - fi - " - - lint: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - working_dir: ${ROOT_DIR} - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - php -v | grep '^PHP'; - find . -name \\*.php ! -path "./.Build/\\*" -print0 | xargs -0 -n1 -P4 php -dxdebug.mode=off -l >/dev/null - " - - #--------------------------------------------------------------------------------------------------------------------- - # functional tests against different dbms - #--------------------------------------------------------------------------------------------------------------------- - functional_sqlite: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - tmpfs: - - ${ROOT_DIR}/public/typo3temp/var/tests/functional-sqlite-dbs/:rw,noexec,nosuid,uid=${HOST_UID} - environment: - typo3DatabaseDriver: pdo_sqlite - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - working_dir: ${ROOT_DIR} - extra_hosts: - - "host.docker.internal:host-gateway" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - php -v | grep '^PHP'; - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-sqlite ${TEST_FILE}; - else - XDEBUG_MODE=\"${USED_XDEBUG_MODES}\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ - .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-sqlite ${TEST_FILE}; - fi - " - - functional_postgres: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - links: - - postgres - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - environment: - typo3DatabaseDriver: pdo_pgsql - typo3DatabaseName: bamboo - typo3DatabaseUsername: funcu - typo3DatabaseHost: postgres - typo3DatabasePassword: funcp - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - working_dir: ${ROOT_DIR} - extra_hosts: - - "host.docker.internal:host-gateway" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - echo Waiting for database start...; - while ! nc -z postgres 5432; do - sleep 1; - done; - echo Database is up; - php -v | grep '^PHP'; - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-postgres ${TEST_FILE}; - else - XDEBUG_MODE=\"${USED_XDEBUG_MODES}\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ - .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-postgres ${TEST_FILE}; - fi - " - - functional_mysql: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - links: - - mysql - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - environment: - typo3DatabaseDriver: "${DATABASE_DRIVER}" - typo3DatabaseName: func_test - typo3DatabaseUsername: root - typo3DatabasePassword: funcp - typo3DatabaseHost: mysql - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - working_dir: ${ROOT_DIR} - extra_hosts: - - "host.docker.internal:host-gateway" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - echo Waiting for database start...; - while ! nc -z mysql 3306; do - sleep 1; - done; - echo Database is up; - php -v | grep '^PHP'; - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; - else - XDEBUG_MODE=\"${USED_XDEBUG_MODES}\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ - .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; - fi - " - - functional_mariadb: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - links: - - mariadb - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - environment: - typo3DatabaseDriver: "${DATABASE_DRIVER}" - typo3DatabaseName: func_test - typo3DatabaseUsername: root - typo3DatabasePassword: funcp - typo3DatabaseHost: mariadb - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - working_dir: ${ROOT_DIR} - extra_hosts: - - "host.docker.internal:host-gateway" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - echo Waiting for database start...; - while ! nc -z mariadb 3306; do - sleep 1; - done; - echo Database is up; - php -v | grep '^PHP'; - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; - else - XDEBUG_MODE=\"${USED_XDEBUG_MODES}\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ - .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; - fi - " - - #--------------------------------------------------------------------------------------------------------------------- - # code quality tools - #--------------------------------------------------------------------------------------------------------------------- - cgl: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - working_dir: ${ROOT_DIR} - extra_hosts: - - "host.docker.internal:host-gateway" - environment: - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - php -v | grep '^PHP'; - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - php -dxdebug.mode=off \ - .Build/bin/php-cs-fixer fix \ - -v \ - ${CGLCHECK_DRY_RUN} \ - --config=Build/php-cs-fixer/php-cs-fixer.php \ - --using-cache=no . - else - XDEBUG_MODE=\"debug,develop\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ - PHP_CS_FIXER_ALLOW_XDEBUG=1 \ - .Build/bin/php-cs-fixer fix \ - -v \ - ${CGLCHECK_DRY_RUN} \ - --config=Build/php-cs-fixer/php-cs-fixer.php \ - --using-cache=no . - fi - " - - coveralls: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - working_dir: ${ROOT_DIR} - extra_hosts: - - "host.docker.internal:host-gateway" - environment: - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - php -v | grep '^PHP'; - XDEBUG_MODE=\"coverage\" \ - php -dxdebug.mode=off ./.Build/bin/php-coveralls --coverage_clover=./.Build/logs/clover.xml --json_path=./.Build/logs/coveralls-upload.json -v - " - - phpstan: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - working_dir: ${ROOT_DIR} - extra_hosts: - - "host.docker.internal:host-gateway" - environment: - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - mkdir -p .cache - php -dxdebug.mode=off .Build/bin/phpstan analyze -c ./phpstan.neon --no-progress - " - - phpstan_generate_baseline: - image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${ROOT_DIR}:${ROOT_DIR} - working_dir: ${ROOT_DIR} - extra_hosts: - - "host.docker.internal:host-gateway" - environment: - COMPOSER_HOME: ".cache/composer-home" - COMPOSER_CACHE_DIR: ".cache/composer" - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - mkdir -p .cache - php -dxdebug.mode=off .Build/bin/phpstan analyze -c ./phpstan.neon --generate-baseline=./phpstan-baseline.neon --allow-empty-baseline - " diff --git a/Classes/Api/RiddleApi.php b/Classes/Api/RiddleApi.php index e5c2554..b9ed2f2 100644 --- a/Classes/Api/RiddleApi.php +++ b/Classes/Api/RiddleApi.php @@ -2,32 +2,27 @@ declare(strict_types=1); -namespace StudioMitte\Riddle\Api; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ -use GuzzleHttp\Client; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\HandlerStack; +namespace StudioMitte\Riddle\Api; + +use StudioMitte\Riddle\Exception\ApiConfigurationMissingException; use StudioMitte\Riddle\ExtensionConfiguration; -use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Http\RequestFactory; class RiddleApi { private const ENDPOINT = 'https://www.riddle.com/api/v3/'; - /** @var ExtensionConfiguration */ - protected $extensionConfiguration; - - public function __construct(ExtensionConfiguration $extensionConfiguration = null) - { - $this->extensionConfiguration = $extensionConfiguration ?: GeneralUtility::makeInstance(ExtensionConfiguration::class); - } + public function __construct( + protected readonly ExtensionConfiguration $extensionConfiguration, + protected readonly RequestFactory $requestFactory, + ) {} public function getRiddleList(): array { @@ -37,59 +32,40 @@ public function getRiddleList(): array public function getRiddleItem(string $id): array { foreach ($this->getRiddleList() as $item) { - if ($item['UUID'] === $id) { + if (($item['UUID'] ?? '') === $id) { return $item; } } return []; } - public function getEmbedCode(string $id): ?string + public function getEmbedCode(string $id): string { return $this->request('riddle/embed-code/' . $id, 'GET'); } - /** - * @return array|string - */ - protected function request(string $action, $method = 'POST') + protected function request(string $action, string $method = 'POST'): array|string { - $client = $this->getClient(); - $headers = [ - 'Accept' => 'application/json', - ]; - - $uri = self::ENDPOINT . trim($action, '/'); - $headers['X-RIDDLE-BEARER'] = 'Bearer ' . $this->extensionConfiguration->getApiKey(); + $apiKey = $this->extensionConfiguration->getApiKey(); + if ($apiKey === '') { + throw new ApiConfigurationMissingException('No riddle.com API key configured', 1598035744); + } - $options['headers'] = $headers; - $response = $client->request($method, $uri, $options); + $response = $this->requestFactory->request(self::ENDPOINT . trim($action, '/'), $method, [ + 'headers' => [ + 'Accept' => 'application/json', + 'X-RIDDLE-BEARER' => 'Bearer ' . $apiKey, + ], + ]); if ($response->getStatusCode() === 200) { - if (\str_starts_with($action, 'riddle/embed-code')) { - return $response->getBody()->getContents(); + $content = $response->getBody()->getContents(); + if (str_starts_with($action, 'riddle/embed-code')) { + return $content; } - return json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR)['data']; + return json_decode($content, true, 512, JSON_THROW_ON_ERROR)['data'] ?? []; } throw new \UnexpectedValueException('Error for riddle request: ' . $response->getStatusCode(), 1597956586); } - - /** - * Creates the client to do requests - * @return ClientInterface - */ - private function getClient(): ClientInterface - { - $httpOptions = $GLOBALS['TYPO3_CONF_VARS']['HTTP']; - $httpOptions['verify'] = filter_var($httpOptions['verify'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? $httpOptions['verify']; - if (isset($GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler']) && is_array($GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler'])) { - $stack = HandlerStack::create(); - foreach ($GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler'] ?? [] as $handler) { - $stack->push($handler); - } - $httpOptions['handler'] = $stack; - } - return GeneralUtility::makeInstance(Client::class, $httpOptions); - } } diff --git a/Classes/Backend/Element/SelectSingleElementWithNoIcon.php b/Classes/Backend/Element/SelectSingleElementWithNoIcon.php index e41f016..6a6aca3 100644 --- a/Classes/Backend/Element/SelectSingleElementWithNoIcon.php +++ b/Classes/Backend/Element/SelectSingleElementWithNoIcon.php @@ -2,29 +2,26 @@ declare(strict_types=1); -namespace StudioMitte\Riddle\Backend\Element; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ +namespace StudioMitte\Riddle\Backend\Element; + use TYPO3\CMS\Backend\Form\Element\SelectSingleElement; /** - * Hide the icon which is prefixed to the select by adding a hidden CSS class + * Hide the icon which is prefixed to the select, as riddle icons are full-size remote images */ class SelectSingleElementWithNoIcon extends SelectSingleElement { - /** - * @inheritDoc - */ - public function render() + public function render(): array { $resultArray = parent::render(); - $resultArray['html'] = str_replace('input-group-addon input-group-icon', 'input-group-addon input-group-icon hidden', $resultArray['html']); + $resultArray['html'] = str_replace('input-group-text input-group-icon"', 'input-group-text input-group-icon d-none"', $resultArray['html']); return $resultArray; } } diff --git a/Classes/Backend/EventListener/BackendPreviewEventListener.php b/Classes/Backend/EventListener/BackendPreviewEventListener.php index 3ba1faf..594ec63 100644 --- a/Classes/Backend/EventListener/BackendPreviewEventListener.php +++ b/Classes/Backend/EventListener/BackendPreviewEventListener.php @@ -2,6 +2,13 @@ declare(strict_types=1); +/* + * This file is part of the "riddle" Extension for TYPO3 CMS. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + namespace StudioMitte\Riddle\Backend\EventListener; use StudioMitte\Riddle\Api\RiddleApi; @@ -10,58 +17,49 @@ use TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent; use TYPO3\CMS\Core\Attribute\AsEventListener; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; +use TYPO3\CMS\Core\View\ViewFactoryData; +use TYPO3\CMS\Core\View\ViewFactoryInterface; -#[AsEventListener( - identifier: 'riddle/riddle' -)] -final class BackendPreviewEventListener +#[AsEventListener(identifier: 'riddle/backend-preview')] +final readonly class BackendPreviewEventListener { - /** @var string */ - private $template = 'EXT:riddle/Resources/Private/Templates/BackendPreview.html'; + private const TEMPLATE = 'EXT:riddle/Resources/Private/Templates/BackendPreview.html'; + + public function __construct( + private ViewFactoryInterface $viewFactory, + private RiddleApi $riddleApi, + ) {} public function __invoke(PageContentPreviewRenderingEvent $event): void { - if ($event->getTable() !== 'tt_content') { + if ($event->getTable() !== 'tt_content' || $event->getRecordType() !== 'riddle_riddle') { return; } - if ($event->getRecord()['CType'] === 'list' && $event->getRecord()['list_type'] === 'riddle_riddle') { - $riddleId = RiddleUtility::getRiddleId($event->getRecord()['pi_flexform']); - - $standaloneView = $this->getView(); - try { - $standaloneView->assignMultiple([ - 'riddleId' => $riddleId, - 'riddle' => $this->getRiddleData($riddleId), - ]); - } catch (ApiConfigurationMissingException $exception) { - $standaloneView->assign('apiNotConfigured', true); - } - - $event->setPreviewContent($standaloneView->render()); - } - } + $rawRecord = $event->getRecord()->getRawRecord(); + $flexForm = $rawRecord?->has('pi_flexform') ? (string)$rawRecord->get('pi_flexform') : ''; + $riddleId = RiddleUtility::getRiddleId($flexForm); - protected function getView(): StandaloneView - { - $standaloneView = GeneralUtility::makeInstance(StandaloneView::class); - $standaloneView->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($this->template)); - $standaloneView->assignMultiple([ + $view = $this->viewFactory->create(new ViewFactoryData( + templatePathAndFilename: GeneralUtility::getFileAbsFileName(self::TEMPLATE), + request: $event->getPageLayoutContext()->getCurrentRequest(), + )); + $view->assignMultiple([ 'dateTime' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], 'lll' => 'LLL:EXT:riddle/Resources/Private/Language/locallang.xlf:', + 'riddleId' => $riddleId, ]); - return $standaloneView; - } + if ($riddleId !== '') { + try { + $view->assign('riddle', RiddleUtility::enrichRiddleData($this->riddleApi->getRiddleItem($riddleId))); + } catch (ApiConfigurationMissingException) { + $view->assign('apiNotConfigured', true); + } catch (\Exception) { + // An unreachable API must not break the page module, the template shows a hint instead + $view->assign('riddle', []); + } + } - /** - * @param string|int $id - * @return array - */ - protected function getRiddleData($id): array - { - $api = GeneralUtility::makeInstance(RiddleApi::class); - $item = $api->getRiddleItem($id); - return RiddleUtility::enrichRiddleData($item); + $event->setPreviewContent($view->render()); } } diff --git a/Classes/Backend/FieldWizard/ExternalIcons.php b/Classes/Backend/FieldWizard/ExternalIcons.php index 4de7368..71cbc4a 100644 --- a/Classes/Backend/FieldWizard/ExternalIcons.php +++ b/Classes/Backend/FieldWizard/ExternalIcons.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace StudioMitte\Riddle\Backend\FieldWizard; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ +namespace StudioMitte\Riddle\Backend\FieldWizard; + use TYPO3\CMS\Backend\Form\AbstractNode; /** @@ -18,47 +18,39 @@ */ class ExternalIcons extends AbstractNode { - /** - * Render thumbnails of selected files - * - * @return array - */ public function render(): array { $selectIcons = []; $result = $this->initializeResultArray(); $parameterArray = $this->data['parameterArray']; - $selectItems = $parameterArray['fieldConf']['config']['items']; - $selectItemCounter = 0; - foreach ($selectItems as $item) { - if ($item['value'] === '') { + $selectItems = $parameterArray['fieldConf']['config']['items'] ?? []; + $fieldValue = $parameterArray['itemFormElValue'] ?? ''; + $fieldValue = (string)(is_array($fieldValue) ? ($fieldValue[0] ?? '') : $fieldValue); + + foreach ($selectItems as $index => $item) { + $icon = (string)($item['icon'] ?? ''); + if ((string)($item['value'] ?? '') === '' || $icon === '') { continue; } - $icon = $item['icon'] ?? ''; - if ($icon) { - $fieldValue = $this->data['databaseRow'][$this->data['fieldName']]['data']['sDEF']['lDEF']['riddle']['vDEF'][0] ?? ''; - $selectIcons[] = [ - 'title' => $item['label'], - 'active' => ($fieldValue === (string)$item['value']) ? true : false, - 'icon' => $icon, - 'index' => $selectItemCounter + 1, - ]; - } - $selectItemCounter++; + $selectIcons[] = [ + 'title' => (string)$item['label'], + 'active' => $fieldValue === (string)$item['value'], + 'icon' => $icon, + 'index' => $index, + ]; } $html = []; if (!empty($selectIcons)) { $html[] = '
'; - foreach ($selectIcons as $i => $selectIcon) { - $active = $selectIcon['active'] ? ' active' : ''; + foreach ($selectIcons as $selectIcon) { $html[] = '
'; - if (is_array($selectIcon)) { - $html[] = ''; - $html[] = sprintf('', $selectIcon['icon']); - $html[] = ''; - } + $html[] = ''; $html[] = '
'; } $html[] = '
'; diff --git a/Classes/Controller/ManagementController.php b/Classes/Controller/ManagementController.php index 7efd57e..de960a1 100644 --- a/Classes/Controller/ManagementController.php +++ b/Classes/Controller/ManagementController.php @@ -2,110 +2,84 @@ declare(strict_types=1); +/* + * This file is part of the "riddle" Extension for TYPO3 CMS. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + namespace StudioMitte\Riddle\Controller; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use StudioMitte\Riddle\Domain\Dto\LogDemand; use StudioMitte\Riddle\Domain\Repository\LogRepository; -use TYPO3\CMS\Backend\Template\ModuleTemplate; -use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; -use TYPO3\CMS\Core\Http\HtmlResponse; -use TYPO3\CMS\Core\Localization\LanguageService; -use TYPO3\CMS\Core\Page\PageRenderer; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; -use TYPO3Fluid\Fluid\View\ViewInterface; +use TYPO3\CMS\Backend\Attribute\AsController; +use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; +use TYPO3\CMS\Core\Http\Response; -class ManagementController +#[AsController] +final readonly class ManagementController { - /** - * ModuleTemplate object - * - * @var ModuleTemplate - */ - protected $moduleTemplate; - - /** - * @var ViewInterface - */ - protected $view; - - /** - * @var ServerRequestInterface - */ - protected $request; - - /** @var LogRepository */ - protected $logRepository; - - public function __construct(private PageRenderer $pageRenderer) - { - $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class); - $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal'); - $this->getLanguageService()->includeLLFile('EXT:riddle/Resources/Private/Language/locallang.xlf'); - $this->logRepository = GeneralUtility::makeInstance(LogRepository::class); - } + public function __construct( + private ModuleTemplateFactory $moduleTemplateFactory, + private LogRepository $logRepository, + ) {} public function handleRequest(ServerRequestInterface $request): ResponseInterface - { - $this->request = $request; - $action = $request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'overview'; - $this->initializeView($action); - - $result = call_user_func_array([$this, $action . 'Action'], [$request]); - if ($result instanceof ResponseInterface) { - return $result; - } - $this->moduleTemplate->setContent($this->view->render()); - return new HtmlResponse($this->moduleTemplate->renderContent()); - } - - protected function overviewAction(ServerRequestInterface $request): void { $demand = LogDemand::initialize($request); - $count = $this->logRepository->countByDemand($demand); if ($count && $demand->isExport()) { - $this->exportLogs($demand); + return $this->exportLogs($demand); } - $this->view->assignMultiple([ + $view = $this->moduleTemplateFactory->create($request); + $view->setTitle('Riddle'); + $view->assignMultiple([ 'riddles' => $this->logRepository->findByDemand($demand), 'filter' => $this->logRepository->getGroupingInformation(), 'demand' => $demand, 'pagination' => $this->preparePagination($demand, $count), ]); + return $view->renderResponse('Management/Overview'); } - protected function exportLogs(LogDemand $demand) + private function exportLogs(LogDemand $demand): ResponseInterface { $demand ->setLimit(PHP_INT_MAX) ->setPage(1); $riddles = $this->logRepository->findByDemandForExport($demand); - $fp = fopen('php://output', 'wb'); - header('Content-Type: text/csv'); - header('Content-Disposition: attachment; filename="export.csv"'); - header('Pragma: no-cache'); - header('Expires: 0'); - fputcsv($fp, array_keys($riddles[0])); + $columns = []; foreach ($riddles as $row) { - fputcsv($fp, array_values($row)); + $columns += array_fill_keys(array_keys($row), ''); } - fclose($fp); - die; + + $stream = fopen('php://temp', 'w+b'); + fputcsv($stream, array_keys($columns), ',', '"', ''); + foreach ($riddles as $row) { + fputcsv($stream, array_values(array_replace($columns, $row)), ',', '"', ''); + } + rewind($stream); + $csv = (string)stream_get_contents($stream); + fclose($stream); + + $response = (new Response()) + ->withHeader('Content-Type', 'text/csv; charset=utf-8') + ->withHeader('Content-Disposition', 'attachment; filename="riddle-export.csv"') + ->withHeader('Cache-Control', 'no-cache, no-store'); + $response->getBody()->write($csv); + return $response; } - protected function preparePagination(LogDemand $demand, int $count): array + private function preparePagination(LogDemand $demand, int $count): array { - $numberOfPages = ceil($count / $demand->getLimit()); - $endRecord = $demand->getOffset() + $demand->getLimit(); - if ($endRecord > $count) { - $endRecord = $count; - } + $numberOfPages = (int)ceil($count / $demand->getLimit()); + $endRecord = min($demand->getOffset() + $demand->getLimit(), $count); $pagination = [ 'current' => $demand->getPage(), @@ -123,32 +97,4 @@ protected function preparePagination(LogDemand $demand, int $count): array } return $pagination; } - - /** - * @param string $templateName - */ - protected function initializeView(string $templateName): void - { - $this->view = GeneralUtility::makeInstance(StandaloneView::class); - $this->view->setTemplate($templateName); - $this->view->setTemplateRootPaths(['EXT:riddle/Resources/Private/Templates/Management']); - $this->view->setPartialRootPaths(['EXT:riddle/Resources/Private/Partials']); - $this->view->setLayoutRootPaths(['EXT:riddle/Resources/Private/Layouts']); - } - - /** - * @return LanguageService - */ - protected function getLanguageService(): LanguageService - { - return $GLOBALS['LANG']; - } - - /** - * @return BackendUserAuthentication - */ - protected function getBackendUserAuthentication(): BackendUserAuthentication - { - return $GLOBALS['BE_USER']; - } } diff --git a/Classes/Controller/PluginController.php b/Classes/Controller/PluginController.php index aa7d1e3..acea587 100644 --- a/Classes/Controller/PluginController.php +++ b/Classes/Controller/PluginController.php @@ -2,45 +2,49 @@ declare(strict_types=1); -namespace StudioMitte\Riddle\Controller; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ +namespace StudioMitte\Riddle\Controller; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Log\LoggerInterface; use StudioMitte\Riddle\Api\RiddleApi; use StudioMitte\Riddle\Utility\RiddleUtility; -use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; -class PluginController extends ContentObjectRenderer +/** + * Renders the riddle embed code, called as USER userFunc + */ +final readonly class PluginController { - protected ContentObjectRenderer $cObj; + public function __construct( + private RiddleApi $riddleApi, + private LoggerInterface $logger, + ) {} - public function setContentObjectRenderer(ContentObjectRenderer $cObj): void + #[AsAllowedCallable] + public function run(string $content, array $conf, ServerRequestInterface $request): string { - $this->cObj = $cObj; - } - - public function run(): string - { - $flexforms = (string)$this->cObj->data['pi_flexform']; - if (!$flexforms) { + $contentObject = $request->getAttribute('currentContentObject'); + if (!$contentObject instanceof ContentObjectRenderer) { return ''; } - $riddleId = RiddleUtility::getRiddleId($flexforms); - if (!$riddleId) { + $riddleId = RiddleUtility::getRiddleId((string)($contentObject->data['pi_flexform'] ?? '')); + if ($riddleId === '') { return ''; } - return $this->getRiddleHtml($riddleId); - } - - protected function getRiddleHtml(string $id): string - { - return GeneralUtility::makeInstance(RiddleApi::class)->getEmbedCode($id); + try { + return $this->riddleApi->getEmbedCode($riddleId); + } catch (\Exception $e) { + $this->logger->error('Riddle "{id}" could not be embedded: {message}', ['id' => $riddleId, 'message' => $e->getMessage()]); + return ''; + } } } diff --git a/Classes/Domain/Dto/LogDemand.php b/Classes/Domain/Dto/LogDemand.php index 6413031..33d1b35 100644 --- a/Classes/Domain/Dto/LogDemand.php +++ b/Classes/Domain/Dto/LogDemand.php @@ -2,6 +2,13 @@ declare(strict_types=1); +/* + * This file is part of the "riddle" Extension for TYPO3 CMS. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + namespace StudioMitte\Riddle\Domain\Dto; use Psr\Http\Message\ServerRequestInterface; @@ -28,7 +35,7 @@ public static function initialize(ServerRequestInterface $request): self $demand = new self(); $demand->type = (string)($request->getQueryParams()['type'] ?? $request->getParsedBody()['type'] ?? 0); $demand->id = (int)($request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0); - $demand->page = (int)($request->getQueryParams()['page'] ?? $request->getParsedBody()['page'] ?? 1); + $demand->page = max(1, (int)($request->getQueryParams()['page'] ?? $request->getParsedBody()['page'] ?? 1)); $demand->export = (bool)($request->getQueryParams()['export'] ?? $request->getParsedBody()['export'] ?? false); return $demand; diff --git a/Classes/Domain/LogItem.php b/Classes/Domain/LogItem.php index ba49775..c3c664d 100644 --- a/Classes/Domain/LogItem.php +++ b/Classes/Domain/LogItem.php @@ -2,13 +2,20 @@ declare(strict_types=1); +/* + * This file is part of the "riddle" Extension for TYPO3 CMS. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + namespace StudioMitte\Riddle\Domain; class LogItem { public static function convert(array $raw): array { - $raw = $raw['data']; + $raw = $raw['data'] ?? []; $data = [ 'riddle_id' => $raw['riddle']['id'] ?? 0, 'riddle_title' => $raw['riddle']['title'] ?? '', diff --git a/Classes/Domain/Repository/LogRepository.php b/Classes/Domain/Repository/LogRepository.php index 233329b..0f82a2d 100644 --- a/Classes/Domain/Repository/LogRepository.php +++ b/Classes/Domain/Repository/LogRepository.php @@ -2,18 +2,28 @@ declare(strict_types=1); +/* + * This file is part of the "riddle" Extension for TYPO3 CMS. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + namespace StudioMitte\Riddle\Domain\Repository; use StudioMitte\Riddle\Domain\Dto\LogDemand; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; -use TYPO3\CMS\Core\Utility\GeneralUtility; class LogRepository { private const TABLE = 'tx_riddle_log'; + public function __construct( + private readonly ConnectionPool $connectionPool, + ) {} + public function add(array $item): void { if (!empty($item)) { @@ -24,44 +34,23 @@ public function add(array $item): void public function countByDemand(LogDemand $demand): int { - return $this->getQueryBuilderForDemand($demand)->execute()->rowCount(); + $queryBuilder = $this->getConnection()->createQueryBuilder(); + $queryBuilder->count('uid')->from(self::TABLE); + $this->applyDemandConstraints($queryBuilder, $demand); + return (int)$queryBuilder->executeQuery()->fetchOne(); } - protected function getQueryBuilderForDemand(LogDemand $demand): QueryBuilder + public function findByDemand(LogDemand $demand): array { $queryBuilder = $this->getConnection()->createQueryBuilder(); $queryBuilder ->select('*') ->from(self::TABLE) - ->orderBy('uid', 'desc'); - $constraints = []; - if ($demand->getType()) { - $constraints[] = $queryBuilder->expr()->eq( - 'riddle_type', - $queryBuilder->createNamedParameter($demand->getType(), \PDO::PARAM_STR) - ); - } - - if ($demand->getId()) { - $constraints[] = $queryBuilder->expr()->eq( - 'riddle_id', - $queryBuilder->createNamedParameter($demand->getId(), \PDO::PARAM_INT) - ); - } - - if (!empty($constraints)) { - $queryBuilder->where(...$constraints); - } - return $queryBuilder; - } - - public function findByDemand(LogDemand $demand): array - { - return $this->getQueryBuilderForDemand($demand) + ->orderBy('uid', 'desc') ->setMaxResults($demand->getLimit()) - ->setFirstResult($demand->getOffset()) - ->execute() - ->fetchAll(); + ->setFirstResult($demand->getOffset()); + $this->applyDemandConstraints($queryBuilder, $demand); + return $queryBuilder->executeQuery()->fetchAllAssociative(); } public function findByDemandForExport(LogDemand $demand): array @@ -69,17 +58,17 @@ public function findByDemandForExport(LogDemand $demand): array $rows = $this->findByDemand($demand); foreach ($rows as &$row) { - $row['crdate'] = date('d.m.Y H:i', $row['crdate']); - $json = json_decode($row['full_json'], true); + $row['crdate'] = date('d.m.Y H:i', (int)$row['crdate']); + $json = json_decode((string)$row['full_json'], true) ?: []; foreach ($json['lead2'] ?? [] as $leadFieldName => $leadFields) { - $row['raw_lead_' . $leadFieldName] = $leadFields['value']; + $row['raw_lead_' . $leadFieldName] = $leadFields['value'] ?? ''; } foreach ($json['answers'] ?? [] as $answer) { - $questionId = $answer['questionId']; - $row['question_' . $questionId] = $answer['question']; - $row['question_' . $questionId . '_answer'] = $answer['answer']; - $row['question_' . $questionId . '_correct'] = $answer['correct']; + $questionId = $answer['questionId'] ?? ''; + $row['question_' . $questionId] = $answer['question'] ?? ''; + $row['question_' . $questionId . '_answer'] = $answer['answer'] ?? ''; + $row['question_' . $questionId . '_correct'] = $answer['correct'] ?? ''; } unset($row['full_json']); @@ -93,12 +82,12 @@ public function getGroupingInformation(): array $groupingInformation = []; $columns = ['riddle_type' => 'type', 'riddle_id' => 'id']; foreach ($columns as $field => $label) { - $queryBuilder = $this->getConnection()->createQueryBuilder(); - - $rows = $queryBuilder + $rows = $this->getConnection()->createQueryBuilder() ->select($field) - ->from(self::TABLE)->groupBy($field)->executeQuery() - ->fetchAll(); + ->from(self::TABLE) + ->groupBy($field) + ->executeQuery() + ->fetchAllAssociative(); foreach ($rows as $row) { $groupingInformation[$label][] = $row[$field]; @@ -108,8 +97,30 @@ public function getGroupingInformation(): array return $groupingInformation; } - protected function getConnection(): Connection + private function applyDemandConstraints(QueryBuilder $queryBuilder, LogDemand $demand): void + { + $constraints = []; + if ($demand->getType()) { + $constraints[] = $queryBuilder->expr()->eq( + 'riddle_type', + $queryBuilder->createNamedParameter($demand->getType()) + ); + } + + if ($demand->getId()) { + $constraints[] = $queryBuilder->expr()->eq( + 'riddle_id', + $queryBuilder->createNamedParameter($demand->getId(), Connection::PARAM_INT) + ); + } + + if (!empty($constraints)) { + $queryBuilder->where(...$constraints); + } + } + + private function getConnection(): Connection { - return GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::TABLE); + return $this->connectionPool->getConnectionForTable(self::TABLE); } } diff --git a/Classes/Exception/ApiConfigurationMissingException.php b/Classes/Exception/ApiConfigurationMissingException.php index f641b58..49337e3 100644 --- a/Classes/Exception/ApiConfigurationMissingException.php +++ b/Classes/Exception/ApiConfigurationMissingException.php @@ -2,14 +2,13 @@ declare(strict_types=1); -namespace StudioMitte\Riddle\Exception; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ -class ApiConfigurationMissingException extends \RuntimeException -{ -} + +namespace StudioMitte\Riddle\Exception; + +class ApiConfigurationMissingException extends \RuntimeException {} diff --git a/Classes/ExtensionConfiguration.php b/Classes/ExtensionConfiguration.php index f351b6f..2b56425 100644 --- a/Classes/ExtensionConfiguration.php +++ b/Classes/ExtensionConfiguration.php @@ -2,36 +2,29 @@ declare(strict_types=1); -namespace StudioMitte\Riddle; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ -use TYPO3\CMS\Core\SingletonInterface; -use TYPO3\CMS\Core\Utility\GeneralUtility; +namespace StudioMitte\Riddle; -class ExtensionConfiguration implements SingletonInterface +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration as CoreExtensionConfiguration; + +final readonly class ExtensionConfiguration { - protected string $apiKey = ''; + public function __construct( + private CoreExtensionConfiguration $extensionConfiguration, + ) {} - public function __construct() + public function getApiKey(): string { try { - $configuration = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\ExtensionConfiguration::class)->get('riddle'); - - if (is_array($configuration)) { - $this->apiKey = (string)($configuration['apiKey'] ?? ''); - } - } catch (\Exception $e) { + return (string)($this->extensionConfiguration->get('riddle', 'apiKey') ?? ''); + } catch (\Exception) { + return ''; } } - - public function getApiKey(): string - { - return $this->apiKey; - } } diff --git a/Classes/Hooks/ItemsProcFunc.php b/Classes/Hooks/ItemsProcFunc.php index 74a1fea..311a732 100644 --- a/Classes/Hooks/ItemsProcFunc.php +++ b/Classes/Hooks/ItemsProcFunc.php @@ -2,20 +2,21 @@ declare(strict_types=1); -namespace StudioMitte\Riddle\Hooks; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ + +namespace StudioMitte\Riddle\Hooks; + use StudioMitte\Riddle\Api\RiddleApi; use StudioMitte\Riddle\Utility\RiddleUtility; use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Messaging\AbstractMessage; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessageService; +use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\Utility\GeneralUtility; /** @@ -26,9 +27,8 @@ class ItemsProcFunc public function riddleList(array &$config): void { try { - $riddles = $this->getAllRiddles(); - foreach ($riddles as $item) { - $config['items'][] = $this->getSelectItem($item, false); + foreach ($this->getAllRiddles() as $item) { + $config['items'][] = $this->getSelectItem($item); } // @codeCoverageIgnoreStart } catch (\Exception $e) { @@ -36,7 +36,7 @@ public function riddleList(array &$config): void FlashMessage::class, $e->getMessage(), '', - AbstractMessage::ERROR + ContextualFeedbackSeverity::ERROR ); GeneralUtility::makeInstance(FlashMessageService::class) ->getMessageQueueByIdentifier() @@ -45,20 +45,24 @@ public function riddleList(array &$config): void // @codeCoverageIgnoreEnd } - protected function getSelectItem(array $item, bool $isV2 = true): array + /** + * @return array{label: string, value: string, icon: string} + */ + protected function getSelectItem(array $item): array { $item = RiddleUtility::enrichRiddleData($item); - $date = $item['_enriched']['datepublished'] ? BackendUtility::date($item['_enriched']['datepublished']) : $item['created']['at'] ?? ''; + $date = isset($item['_enriched']['datepublished']) + ? BackendUtility::date($item['_enriched']['datepublished']) + : ($item['created']['at'] ?? ''); return [ - sprintf('%s [%s] - %s', $item['title'], $item['type'] ?? '-', $date), - $item['UUID'], - $item['image'] ?? '', + 'label' => sprintf('%s [%s] - %s', $item['title'] ?? '', $item['type'] ?? '-', $date), + 'value' => (string)($item['UUID'] ?? ''), + 'icon' => (string)($item['image'] ?? ''), ]; } protected function getAllRiddles(): array { - $api = GeneralUtility::makeInstance(RiddleApi::class); - return $api->getRiddleList(); + return GeneralUtility::makeInstance(RiddleApi::class)->getRiddleList(); } } diff --git a/Classes/Middleware/WebhookEndpoint.php b/Classes/Middleware/WebhookEndpoint.php index 3ff9bc6..1b6b611 100644 --- a/Classes/Middleware/WebhookEndpoint.php +++ b/Classes/Middleware/WebhookEndpoint.php @@ -2,6 +2,13 @@ declare(strict_types=1); +/* + * This file is part of the "riddle" Extension for TYPO3 CMS. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + */ + namespace StudioMitte\Riddle\Middleware; use Psr\Http\Message\ResponseInterface; @@ -11,33 +18,26 @@ use StudioMitte\Riddle\Domain\LogItem; use StudioMitte\Riddle\Domain\Repository\LogRepository; use TYPO3\CMS\Core\Http\JsonResponse; -use TYPO3\CMS\Core\Utility\GeneralUtility; -class WebhookEndpoint implements MiddlewareInterface +final readonly class WebhookEndpoint implements MiddlewareInterface { private const ENDPOINT = '/riddle-endpoint'; - /** @var LogRepository */ - protected $logRepository; + public function __construct( + private LogRepository $logRepository, + ) {} - public function __construct() + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $this->logRepository = GeneralUtility::makeInstance(LogRepository::class); - } - - public function process( - ServerRequestInterface $request, - RequestHandlerInterface $handler - ): ResponseInterface { - if (!\str_starts_with($request->getUri()->getPath(), self::ENDPOINT)) { + if (!str_starts_with($request->getUri()->getPath(), self::ENDPOINT)) { return $handler->handle($request); } - $json = $request->getParsedBody()['data'] ?? ''; - if ($json) { + $parsedBody = $request->getParsedBody(); + $json = is_array($parsedBody) ? (string)($parsedBody['data'] ?? '') : ''; + if ($json !== '') { $data = json_decode($json, true); - if ($data['riddleId'] ?? false) { - $logItem = LogItem::convert($data); - $this->logRepository->add($logItem); + if (is_array($data) && ($data['riddleId'] ?? false)) { + $this->logRepository->add(LogItem::convert($data)); } } return new JsonResponse(['status' => 'ok']); diff --git a/Classes/Upgrades/PluginListTypeToCTypeUpdate.php b/Classes/Upgrades/PluginListTypeToCTypeUpdate.php new file mode 100644 index 0000000..275a4e2 --- /dev/null +++ b/Classes/Upgrades/PluginListTypeToCTypeUpdate.php @@ -0,0 +1,36 @@ + 'riddle_riddle', + ]; + } + + public function getTitle(): string + { + return 'Migrate "riddle" plugins to content elements'; + } + + public function getDescription(): string + { + return 'The riddle plugin is now registered as its own content element type (CType). This wizard migrates existing plugin records and backend user group permissions.'; + } +} diff --git a/Classes/Utility/RiddleUtility.php b/Classes/Utility/RiddleUtility.php index e55bd14..958f3f9 100644 --- a/Classes/Utility/RiddleUtility.php +++ b/Classes/Utility/RiddleUtility.php @@ -2,34 +2,31 @@ declare(strict_types=1); -namespace StudioMitte\Riddle\Utility; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ -use TYPO3\CMS\Core\Service\FlexFormService; +namespace StudioMitte\Riddle\Utility; + +use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools; use TYPO3\CMS\Core\Utility\GeneralUtility; class RiddleUtility { /** * Enrich the API riddle response - * - * @param array $item - * @return array */ public static function enrichRiddleData(array $item): array { if (isset($item['created']) && is_array($item['created'])) { - $date = \DateTime::createFromFormat('Y-m-d H:i:s', $item['created']['at']); + $date = \DateTime::createFromFormat('Y-m-d H:i:s', (string)($item['created']['at'] ?? '')); if ($date) { $item['_enriched']['dateCreated'] = $date->getTimestamp(); } - $date = \DateTime::createFromFormat('Y-m-d H:i:s', $item['modified']['at']); + $date = \DateTime::createFromFormat('Y-m-d H:i:s', (string)($item['modified']['at'] ?? '')); if ($date) { $item['_enriched']['datepublished'] = $date->getTimestamp(); } @@ -50,16 +47,13 @@ public static function enrichRiddleData(array $item): array /** * Get riddle ID from given flexform XML - * - * @param string $flexforms - * @return string */ - public static function getRiddleId(string $flexforms) + public static function getRiddleId(string $flexforms): string { - if (!$flexforms) { + if ($flexforms === '') { return ''; } - $data = GeneralUtility::makeInstance(FlexFormService::class)->convertFlexFormContentToArray($flexforms); - return $data['riddle'] ?? ''; + $data = GeneralUtility::makeInstance(FlexFormTools::class)->convertFlexFormContentToArray($flexforms); + return (string)($data['riddle'] ?? ''); } } diff --git a/Configuration/Backend/Modules.php b/Configuration/Backend/Modules.php new file mode 100644 index 0000000..ef93d65 --- /dev/null +++ b/Configuration/Backend/Modules.php @@ -0,0 +1,18 @@ + [ + 'parent' => 'site', + 'access' => 'user', + 'path' => '/module/site/riddle', + 'iconIdentifier' => 'ext-riddle-module', + 'labels' => 'LLL:EXT:riddle/Resources/Private/Language/locallang.xlf', + 'routes' => [ + '_default' => [ + 'target' => ManagementController::class . '::handleRequest', + ], + ], + ], +]; diff --git a/Configuration/ContentSecurityPolicies.php b/Configuration/ContentSecurityPolicies.php new file mode 100644 index 0000000..d389459 --- /dev/null +++ b/Configuration/ContentSecurityPolicies.php @@ -0,0 +1,17 @@ + - - - LLL:EXT:riddle/Resources/Private/Language/locallang.xlf:plugin.flexform.settings - + LLL:EXT:riddle/Resources/Private/Language/locallang.xlf:plugin.flexform.settings array - - - - select - StudioMitte\Riddle\Hooks\ItemsProcFunc->riddleList - selectSingleWithNoIconPrefix - - - - - - - - - ExternalIcons - 0 - - - - + + + select + StudioMitte\Riddle\Hooks\ItemsProcFunc->riddleList + selectSingleWithNoIconPrefix + + + + + + + + + ExternalIcons + 0 + + + - diff --git a/Configuration/Icons.php b/Configuration/Icons.php new file mode 100644 index 0000000..01e7082 --- /dev/null +++ b/Configuration/Icons.php @@ -0,0 +1,14 @@ + [ + 'provider' => BitmapIconProvider::class, + 'source' => 'EXT:riddle/Resources/Public/Icons/Plugin.png', + ], + 'ext-riddle-module' => [ + 'provider' => BitmapIconProvider::class, + 'source' => 'EXT:riddle/Resources/Public/Icons/Extension.png', + ], +]; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 532edd0..c569eb1 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -2,11 +2,17 @@ services: _defaults: autowire: true autoconfigure: true + public: false StudioMitte\Riddle\: resource: '../Classes/*' - StudioMitte\Riddle\Backend\EventListener\BackendPreviewEventListener: - tags: - - name: event.listener - identifier: 'riddle/riddle' \ No newline at end of file + # Instantiated via GeneralUtility::makeInstance() from itemsProcFunc / userFunc contexts + StudioMitte\Riddle\Api\RiddleApi: + public: true + + StudioMitte\Riddle\Controller\PluginController: + public: true + + StudioMitte\Riddle\Middleware\WebhookEndpoint: + public: true diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php index 3363034..c9e941b 100644 --- a/Configuration/TCA/Overrides/tt_content.php +++ b/Configuration/TCA/Overrides/tt_content.php @@ -1,17 +1,14 @@ 'LLL:EXT:riddle/Resources/Private/Language/locallang.xlf:plugin.title', + 'description' => 'LLL:EXT:riddle/Resources/Private/Language/locallang.xlf:plugin.description', + 'value' => 'riddle_riddle', + 'icon' => 'ext-riddle-plugin', + 'group' => 'plugins', + ], + 'FILE:EXT:riddle/Configuration/FlexForms/flexform_riddle.xml' +); diff --git a/Configuration/TCA/tx_riddle_log.php b/Configuration/TCA/tx_riddle_log.php index b8a044e..6ff5d9e 100644 --- a/Configuration/TCA/tx_riddle_log.php +++ b/Configuration/TCA/tx_riddle_log.php @@ -1,26 +1,49 @@ [ - 'title' => 'LLL:EXT:plain_faq/Resources/Private/Language/locallang_db.xlf:tx_plainfaq_domain_model_faq', - 'label' => 'uid', + 'title' => $ll . 'tx_riddle_log', + 'label' => 'riddle_title', + 'label_alt' => 'lead_email', 'crdate' => 'crdate', - 'sortby' => 'sorting', - 'searchFields' => 'question,answer,keywords,images,files', + 'default_sortby' => 'crdate DESC', 'typeicon_classes' => [ - 'default' => 'ext-plainfaq-faq', + 'default' => 'ext-riddle-plugin', ], 'readOnly' => true, + 'hideTable' => true, ], 'types' => [ - '1' => ['showitem' => ' - question, slug, answer, keywords,', + '0' => [ + 'showitem' => 'riddle_title, riddle_type, riddle_id, lead_name, lead_email, result_score_percentage', ], ], - 'palettes' => [ - 'language' => ['showitem' => 'sys_language_uid, l10n_parent, l10n_diffsource'], - ], 'columns' => [ - + 'riddle_title' => [ + 'label' => $ll . 'tx_riddle_log.riddle_title', + 'config' => ['type' => 'input', 'readOnly' => true], + ], + 'riddle_type' => [ + 'label' => $ll . 'riddle.type', + 'config' => ['type' => 'input', 'readOnly' => true], + ], + 'riddle_id' => [ + 'label' => $ll . 'riddle.id', + 'config' => ['type' => 'number', 'readOnly' => true], + ], + 'lead_name' => [ + 'label' => $ll . 'tx_riddle_log.lead_name', + 'config' => ['type' => 'input', 'readOnly' => true], + ], + 'lead_email' => [ + 'label' => $ll . 'tx_riddle_log.lead_email', + 'config' => ['type' => 'input', 'readOnly' => true], + ], + 'result_score_percentage' => [ + 'label' => $ll . 'tx_riddle_log.result', + 'config' => ['type' => 'number', 'readOnly' => true], + ], ], ]; diff --git a/Configuration/TSconfig/ContentElementWizard.typoscript b/Configuration/TSconfig/ContentElementWizard.typoscript deleted file mode 100644 index 184476b..0000000 --- a/Configuration/TSconfig/ContentElementWizard.typoscript +++ /dev/null @@ -1,13 +0,0 @@ -mod.wizards.newContentElement.wizardItems.plugins { - elements { - riddle { - iconIdentifier = ext-riddle-plugin - title = LLL:EXT:riddle/Resources/Private/Language/locallang.xlf:plugin.title - description = LLL:EXT:riddle/Resources/Private/Language/locallang.xlf:plugin.description - tt_content_defValues { - CType = list - list_type = riddle_riddle - } - } - } -} diff --git a/Documentation/Settings.cfg b/Documentation/Settings.cfg index e460e74..e285965 100644 --- a/Documentation/Settings.cfg +++ b/Documentation/Settings.cfg @@ -18,8 +18,8 @@ [general] project = riddle -version = 1.0 -release = 1.0.0 +version = 3.0 +release = 3.0.0 t3author = Georg Ringer copyright = Georg Ringer diff --git a/Readme.md b/Readme.md index 97f93bf..7a8ce9f 100644 --- a/Readme.md +++ b/Readme.md @@ -1,11 +1,25 @@ # TYPO3 Extension `riddle` -![Build Status](https://github.com/studiomitte/riddle/workflows/CI/badge.svg) +[![CI](https://github.com/studiomitte/riddle/actions/workflows/ci.yml/badge.svg)](https://github.com/studiomitte/riddle/actions/workflows/ci.yml) This extension embeds rich media from [riddle.com](https://www.riddle.com) ![Plugin in Backend](Documentation/Images/backend.png) +## Compatibility + +| Version | TYPO3 | PHP | +|---------|-------|-----------| +| 3.x | 14 | 8.2 - 8.5 | +| 2.x | 12 | 8.1 - 8.2 | + +### Upgrading from 2.x + +Since 3.0 the plugin is a content element type of its own (`CType = riddle_riddle`) instead of a +`list_type` plugin. After updating, run the upgrade wizard *"Migrate riddle plugins to content elements"* +(`vendor/bin/typo3 upgrade:run riddlePluginListTypeToCTypeUpdate`). If you customized the rendering, +move your TypoScript from `tt_content.list.20.riddle_riddle` to `tt_content.riddle_riddle.20`. + ## Installation Install the extension as any other extension. @@ -15,7 +29,7 @@ Now switch to the [riddle.com account page](https://www.riddle.com/creator/accou ![riddle.com API](Documentation/Images/riddleApi.png) -Open the settings in TYPO3 Install Tool and paste the 2 keys there. +Open *Admin Tools > Settings > Extension Configuration > riddle* and paste the API key there. ![Settings in TYPO3](Documentation/Images/settings.png) @@ -33,12 +47,14 @@ Therefore you are not required to ask your website users for permissions display ## Modify rendering -The rendering is controlled with the TypoScript shipped in the `ext_typoscript_setup.typoscript` which is loaded by default. +The rendering is controlled with the TypoScript registered in `ext_localconf.php` which is loaded by default. ```typo3_typoscript -tt_content.list.20.riddle_riddle = USER -tt_content.list.20.riddle_riddle { - userFunc = StudioMitte\Riddle\Controller\PluginController->run +tt_content.riddle_riddle =< lib.contentElement +tt_content.riddle_riddle { + templateName = Generic + 20 = USER + 20.userFunc = StudioMitte\Riddle\Controller\PluginController->run } ``` diff --git a/Resources/Private/Language/de.locallang.xlf b/Resources/Private/Language/de.locallang.xlf index 77accdb..6150ed2 100644 --- a/Resources/Private/Language/de.locallang.xlf +++ b/Resources/Private/Language/de.locallang.xlf @@ -76,7 +76,51 @@ Edit on riddle.com - Edit on riddle.com + Auf riddle.com bearbeiten + + + Riddle logs + Riddle-Protokoll + + + No log entries found. + Keine Einträge gefunden. + + + CSV export + CSV-Export + + + Filter + Filtern + + + Date + Datum + + + Lead + Lead + + + Riddle log + Riddle-Protokoll + + + Title + Titel + + + Lead name + Name + + + Lead email + E-Mail + + + Result + Ergebnis diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 0f0a8f8..3216267 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -68,6 +68,39 @@ Edit on riddle.com + + Riddle logs + + + No log entries found. + + + CSV export + + + Filter + + + Date + + + Lead + + + Riddle log + + + Title + + + Lead name + + + Lead email + + + Result + diff --git a/Resources/Private/Layouts/LogAdministration.html b/Resources/Private/Layouts/LogAdministration.html deleted file mode 100644 index 6072397..0000000 --- a/Resources/Private/Layouts/LogAdministration.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/Resources/Private/Partials/Pagination.html b/Resources/Private/Partials/Pagination.html index f985a46..31e7752 100644 --- a/Resources/Private/Partials/Pagination.html +++ b/Resources/Private/Partials/Pagination.html @@ -1,90 +1,51 @@ - - + diff --git a/Resources/Private/Templates/BackendPreview.html b/Resources/Private/Templates/BackendPreview.html index 26a92cf..e093886 100644 --- a/Resources/Private/Templates/BackendPreview.html +++ b/Resources/Private/Templates/BackendPreview.html @@ -1,56 +1,62 @@ {f:translate(key:'{lll}plugin.title')} - + + {f:translate(key:'{lll}error.apiNotConfigured')} + +

{riddle.title}

- {riddle.title} - - - - - - - - - - - - - - - + + {riddle.title} + +
{f:translate(key:'{lll}riddle.overview')}
{f:translate(key:'{lll}riddle.id')}{riddle.UUID}
{f:translate(key:'{lll}riddle.type')}{riddle.type}
+ + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - -
{f:translate(key:'{lll}riddle.overview')}
{f:translate(key:'{lll}riddle.id')}{riddle.UUID}
{f:translate(key:'{lll}riddle.type')}{riddle.type}
{f:translate(key:'{lll}riddle.created')} + {riddle._enriched.dateCreated -> f:format.date(format:dateTime)}
+ {riddle.created.by} +
{f:translate(key:'{lll}riddle.published')} + {riddle._enriched.datepublished -> f:format.date(format:dateTime)}
+ {riddle.published.by} +
{f:translate(key:'{lll}riddle.created')}{f:translate(key:'{lll}riddle.tags')} - {riddle._enriched.dateCreated -> f:format.date(format:dateTime)}
- {riddle.created.by} +
    + +
  • {tag.name}
  • +
    +
{f:translate(key:'{lll}riddle.published')}{riddle._enriched.datepublished -> f:format.date(format:dateTime)}
- {riddle.published.by} -
{f:translate(key:'{lll}riddle.tags')} -
    - -
  • {tag.name}
  • -
    -
-
+
+ +
@@ -67,18 +73,9 @@

{riddle.title}

{f:translate(key:'{lll}error.noRiddleResponse')} - + - - - - {f:translate(key:'{lll}error.apiNotConfigured')} - - - {f:translate(key:'{lll}error.noRiddleIdProvided')} - - - + {f:translate(key:'{lll}error.noRiddleIdProvided')} diff --git a/Resources/Private/Templates/Management/Overview.html b/Resources/Private/Templates/Management/Overview.html index 2a80ea1..9afa7bf 100644 --- a/Resources/Private/Templates/Management/Overview.html +++ b/Resources/Private/Templates/Management/Overview.html @@ -1,39 +1,18 @@ - + - -

riddles

-
+ +

+ + - - + - - - - - -

-
- - - - - - -

- - - -
-
- + @@ -44,22 +23,22 @@

riddles

- - - - - + + + + + - - - - - - - + + + + + + +
DateTypeTitleLeadResult
{riddle.crdate -> f:format.date(format: 'd.m.Y H:i')}{riddle.riddle_type}{riddle.riddle_title -> f:format.stripTags()}{riddle.lead_name} ({riddle.lead_email}){riddle.result_score_percentage}
{riddle.crdate -> f:format.date(format: 'd.m.Y H:i')}{riddle.riddle_type}{riddle.riddle_title -> f:format.stripTags()}{riddle.lead_name} ({riddle.lead_email}){riddle.result_score_percentage}
@@ -68,29 +47,27 @@

riddles

-
- -
- - - {singleType} + + + +
- -
- - +
+
+ + +
- -
- +
+
diff --git a/Tests/Unit/Api/RiddleApiTest.php b/Tests/Unit/Api/RiddleApiTest.php index 16c4b55..ec0826a 100644 --- a/Tests/Unit/Api/RiddleApiTest.php +++ b/Tests/Unit/Api/RiddleApiTest.php @@ -2,159 +2,104 @@ declare(strict_types=1); -namespace StudioMitte\Riddle\Tests\Unit\Api; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ -use GuzzleHttp\Client; +namespace StudioMitte\Riddle\Tests\Unit\Api; + +use PHPUnit\Framework\Attributes\Test; use StudioMitte\Riddle\Api\RiddleApi; +use StudioMitte\Riddle\Exception\ApiConfigurationMissingException; use StudioMitte\Riddle\ExtensionConfiguration; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration as CoreExtensionConfiguration; +use TYPO3\CMS\Core\Http\RequestFactory; use TYPO3\CMS\Core\Http\Response; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\TestingFramework\Core\BaseTestCase; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; -class RiddleApiTest extends BaseTestCase +final class RiddleApiTest extends UnitTestCase { - private $listOfRiddles = [ - [ - 'title' => 'riddle 1', - 'id' => 123, - ], - [ - 'title' => 'riddle 2', - 'id' => 456, - ], - ]; - - /** - * @test - */ - public function riddleListReturnsResult(): void + private function createSubject(string $apiKey, ?Response $response = null): RiddleApi { - $mockedRiddleApi = $this->getAccessibleMock(RiddleApi::class, ['request'], [], '', false); - $mockedRiddleApi->expects(self::once())->method('request')->with('riddle/list')->willReturn($this->listOfRiddles); - - /** @var ExtensionConfiguration $extensionConfiguration */ - $extensionConfiguration = $this->prophesize(ExtensionConfiguration::class); - $extensionConfiguration->getApiKey()->willReturn('abc'); - $mockedRiddleApi->_set('extensionConfiguration', $extensionConfiguration->reveal()); - - $riddles = $mockedRiddleApi->getRiddleList(); - self::assertEquals($this->listOfRiddles, $riddles); + $coreExtensionConfiguration = $this->createMock(CoreExtensionConfiguration::class); + $coreExtensionConfiguration->method('get')->with('riddle', 'apiKey')->willReturn($apiKey); + + $requestFactory = $this->createMock(RequestFactory::class); + if ($response !== null) { + $requestFactory->expects(self::once())->method('request') + ->with( + self::stringStartsWith('https://www.riddle.com/api/v3/'), + self::anything(), + self::callback(static fn(array $options): bool => $options['headers']['X-RIDDLE-BEARER'] === 'Bearer ' . $apiKey) + ) + ->willReturn($response); + } else { + $requestFactory->expects(self::never())->method('request'); + } + + return new RiddleApi(new ExtensionConfiguration($coreExtensionConfiguration), $requestFactory); } - /** - * @test - * @dataProvider riddleIdReturnsResultDataProvider - * @param int $id - * @param array $expectedResult - */ - public function riddleIdReturnsResult(int $id, array $expectedResult): void + private function createResponse(string $body, int $status = 200): Response { - $response = ['response' => ['items' => $this->listOfRiddles]]; - $mockedRiddleApi = $this->getAccessibleMock(RiddleApi::class, ['request'], [], '', false); - $mockedRiddleApi->expects(self::once())->method('request')->with('riddle/list')->willReturn($response); - - $extensionConfiguration = $this->prophesize(ExtensionConfiguration::class); - $extensionConfiguration->getApiKey()->willReturn('abc'); - $mockedRiddleApi->_set('extensionConfiguration', $extensionConfiguration->reveal()); - - $riddle = $mockedRiddleApi->getRiddleItem($id); - self::assertEquals($expectedResult, $riddle); + $response = new Response('php://temp', $status); + $response->getBody()->write($body); + $response->getBody()->rewind(); + return $response; } - public function riddleIdReturnsResultDataProvider(): array + #[Test] + public function riddleListReturnsDataOfResponse(): void { - return [ - 'riddle found' => [ - 456, [ - 'title' => 'riddle 2', - 'id' => 456, - ], - ], - 'no riddle found' => [ - 123456, [], - ], - ]; + $riddles = [['UUID' => 'abc', 'title' => 'riddle 1']]; + $subject = $this->createSubject('key', $this->createResponse(json_encode(['data' => $riddles]))); + + self::assertSame($riddles, $subject->getRiddleList()); } - /** - * @test - */ - public function embedCodeReturnsResult(): void + #[Test] + public function riddleItemIsFoundByUuid(): void { - $response = ['response' => 'some html']; - $mockedRiddleApi = $this->getAccessibleMock(RiddleApi::class, ['request'], [], '', false); - $mockedRiddleApi->expects(self::once())->method('request')->with('riddle/embed-code/123')->willReturn($response); + $riddles = [['UUID' => 'abc', 'title' => 'riddle 1'], ['UUID' => 'def', 'title' => 'riddle 2']]; + $subject = $this->createSubject('key', $this->createResponse(json_encode(['data' => $riddles]))); - $extensionConfiguration = $this->prophesize(ExtensionConfiguration::class); - $extensionConfiguration->getApiKey()->willReturn('abc'); - $mockedRiddleApi->_set('extensionConfiguration', $extensionConfiguration->reveal()); - - self::assertEquals($response, $mockedRiddleApi->getEmbedCode(123)); + self::assertSame($riddles[1], $subject->getRiddleItem('def')); } - /** - * @test - */ - public function constructorSetsExtensionConfiguration(): void + #[Test] + public function unknownRiddleItemReturnsEmptyArray(): void { - $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['riddle'] = ['apiKey' => '123']; - $configuration = new ExtensionConfiguration(); - - $mockedRiddleApi = $this->getAccessibleMock(RiddleApi::class, ['request'], [], '', true); + $subject = $this->createSubject('key', $this->createResponse(json_encode(['data' => [['UUID' => 'abc']]]))); - /** @var ExtensionConfiguration $extensionConfigurationFromApi */ - $extensionConfigurationFromApi = $mockedRiddleApi->_get('extensionConfiguration'); - self::assertEquals(123, $configuration->getApiKey()); + self::assertSame([], $subject->getRiddleItem('xyz')); } - /** - * @test - */ - public function requestIsProperlyBuilt(): void + #[Test] + public function embedCodeIsReturnedAsString(): void { - $expected = ['response' => ['works' => 1]]; - $response = new Response(); - $response->getBody()->write(json_encode($expected)); - $response->getBody()->rewind(); - - $client = $this->getAccessibleMock(Client::class, ['request'], [], '', true); - $client->expects(self::once())->method('request')->willReturn($response); - GeneralUtility::addInstance(Client::class, $client); - - $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['riddle'] = ['apiKey' => '123', 'apiToken' => 456]; + $subject = $this->createSubject('key', $this->createResponse('
embed
')); - $mockedRiddleApi = $this->getAccessibleMock(RiddleApi::class, ['dummy'], [], '', true); - - self::assertEquals($expected, $mockedRiddleApi->_call('request', 'action')); + self::assertSame('
embed
', $subject->getEmbedCode('abc')); } - /** - * @test - */ - public function requestErrorThrowsException(): void + #[Test] + public function nonSuccessfulResponseThrowsException(): void { $this->expectException(\UnexpectedValueException::class); $this->expectExceptionCode(1597956586); - $expected = ['response' => ['works' => 1]]; - $response = new Response('php://temp', 404); - $response->getBody()->write(json_encode($expected)); - $response->getBody()->rewind(); - - $client = $this->getAccessibleMock(Client::class, ['request'], [], '', true); - $client->expects(self::once())->method('request')->willReturn($response); - GeneralUtility::addInstance(Client::class, $client); - $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['riddle'] = ['apiKey' => '123', 'apiToken' => 456]; + $this->createSubject('key', $this->createResponse('', 204))->getRiddleList(); + } - $mockedRiddleApi = $this->getAccessibleMock(RiddleApi::class, ['dummy'], [], '', true); + #[Test] + public function missingApiKeyThrowsException(): void + { + $this->expectException(ApiConfigurationMissingException::class); + $this->expectExceptionCode(1598035744); - self::assertEquals($expected, $mockedRiddleApi->_call('request', 'action')); + $this->createSubject('')->getRiddleList(); } } diff --git a/Tests/Unit/Backend/Element/SelectSingleElementWithNoIconTest.php b/Tests/Unit/Backend/Element/SelectSingleElementWithNoIconTest.php deleted file mode 100644 index 2e3a785..0000000 --- a/Tests/Unit/Backend/Element/SelectSingleElementWithNoIconTest.php +++ /dev/null @@ -1,80 +0,0 @@ -getMajorVersion() < 10) { - self::markTestSkipped('Does not work on 9'); - } - $data = [ - 'tableName' => 'tt_content', - 'fieldName' => 'field', - 'databaseRow' => [ - 'uid' => 123, - ], - 'parameterArray' => [ - 'itemFormElName' => 'itemFormElName', - 'fieldChangeFunc' => [], - 'fieldConf' => [ - 'config' => [ - 'size' => 1, - 'header' => 'header', - 'items' => [ - [ - 0 => 'label', - 1 => '--div--', - 'icon' => 'some-icon', - ], - [ - 0 => 'label 2', - 1 => 'fo', - 2 => 'another-icon', - ], - ], - ], - ], - ], - 'inlineStructure' => [], - 'isInlineChild' => false, - 'inlineParentUid' => false, - 'size' => 1, - ]; - $iconFactoryProphecy = $this->prophesize(IconFactory::class); - GeneralUtility::addInstance(IconFactory::class, $iconFactoryProphecy->reveal()); - $iconProphecy = $this->prophesize(Icon::class); - $iconProphecy->render()->willReturn('some icon'); - $iconFactoryProphecy->getIcon(Argument::cetera())->willReturn($iconProphecy->reveal()); - - /** @var SelectSingleElementWithNoIcon $mockedSelectSingleElement */ - $mockedSelectSingleElement = $this->getAccessibleMock(SelectSingleElementWithNoIcon::class, ['renderFieldInformation', 'renderFieldControl', 'renderFieldWizard'], [], '', false); - $mockedSelectSingleElement->_set('data', $data); - $mockedSelectSingleElement->expects(self::once())->method('renderFieldInformation')->willReturn(['html' => '']); - $mockedSelectSingleElement->expects(self::once())->method('renderFieldControl')->willReturn(['html' => '']); - $mockedSelectSingleElement->expects(self::once())->method('renderFieldWizard')->willReturn(['html' => '']); - - self::assertStringContainsString('input-group-addon input-group-icon hidden', $mockedSelectSingleElement->render()['html']); - } -} diff --git a/Tests/Unit/Backend/FieldWizard/ExternalIconsTest.php b/Tests/Unit/Backend/FieldWizard/ExternalIconsTest.php deleted file mode 100644 index e68a503..0000000 --- a/Tests/Unit/Backend/FieldWizard/ExternalIconsTest.php +++ /dev/null @@ -1,67 +0,0 @@ - 'tt_content', - 'fieldName' => 'field', - 'databaseRow' => [ - 'uid' => 123, - 'field' => ['fo'], - ], - 'parameterArray' => [ - 'itemFormElName' => 'itemFormElName', - 'fieldChangeFunc' => [], - 'fieldConf' => [ - 'config' => [ - 'size' => 1, - 'header' => 'header', - 'items' => [ - [ - 0 => 'label', - 1 => '--div--', - 'icon' => 'some-icon', - ], - [ - 0 => 'label 2', - 1 => 'fo', - 2 => 'https://some-external.icon.com', - ], - ], - ], - ], - ], - 'inlineStructure' => [], - 'isInlineChild' => false, - 'inlineParentUid' => false, - 'size' => 1, - ]; - - /** @var ExternalIcons $externalIcons */ - $externalIcons = $this->getAccessibleMock(ExternalIcons::class, ['dummy'], [], '', false); - $externalIcons->_set('data', $data); - - $result = $externalIcons->render(); - - self::assertStringContainsString(' ['id' => 42, 'title' => 'Quiz', 'type' => 'quiz'], + 'timeTaken' => ['milliseconds' => 1500], + 'resultData' => ['scorePercentage' => 80, 'title' => 'Great'], + 'lead2' => ['Name' => ['value' => 'Jane'], 'Email' => ['value' => 'jane@example.org']], + ]; + + $result = LogItem::convert(['riddleId' => 42, 'data' => $data]); + + self::assertSame(42, $result['riddle_id']); + self::assertSame('Quiz', $result['riddle_title']); + self::assertSame(1500, $result['time_taken']); + self::assertSame(80, $result['result_score_percentage']); + self::assertSame('Jane', $result['lead_name']); + self::assertSame('jane@example.org', $result['lead_email']); + self::assertSame(json_encode($data), $result['full_json']); + } + + #[Test] + public function missingDataFallsBackToDefaults(): void + { + $result = LogItem::convert(['riddleId' => 42]); + + self::assertSame(0, $result['riddle_id']); + self::assertSame('', $result['lead_email']); + } +} diff --git a/Tests/Unit/ExtensionConfigurationTest.php b/Tests/Unit/ExtensionConfigurationTest.php deleted file mode 100644 index b6b67a0..0000000 --- a/Tests/Unit/ExtensionConfigurationTest.php +++ /dev/null @@ -1,74 +0,0 @@ -getApiKey()); - } - - public function configurationCanBeRetrievedDataProvider(): array - { - return [ - 'working api' => [ - ['apiKey' => '123'], 123, - ], - ]; - } - - /** - * @test - */ - public function emptyConfigurationThrowsException(): void - { - $this->expectException(ApiConfigurationMissingException::class); - $this->expectExceptionCode(1598035744); - - $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['riddle'] = []; - new ExtensionConfiguration(); - } - - /** - * @test - */ - public function exceptionsAreHandledInConstructor(): void - { - $mockedCoreExtensionConfiguration = $this->getAccessibleMock(ExtensionConfigurationCore::class, ['get'], [], '', false); - $mockedCoreExtensionConfiguration->expects(self::once())->method('get')->with('riddle')->willThrowException(new \RuntimeException('faked exception')); - GeneralUtility::addInstance(ExtensionConfigurationCore::class, $mockedCoreExtensionConfiguration); - - $this->expectException(ApiConfigurationMissingException::class); - $this->expectExceptionCode(1598035744); - - $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['riddle'] = []; - new ExtensionConfiguration(); - } -} diff --git a/Tests/Unit/Hooks/ItemsProcFuncTest.php b/Tests/Unit/Hooks/ItemsProcFuncTest.php index 28ae391..83d67b4 100644 --- a/Tests/Unit/Hooks/ItemsProcFuncTest.php +++ b/Tests/Unit/Hooks/ItemsProcFuncTest.php @@ -2,78 +2,55 @@ declare(strict_types=1); -namespace StudioMitte\Riddle\Tests\Unit\Hooks; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ -use PHPUnit\Framework\MockObject\MockObject; -use StudioMitte\Riddle\Api\RiddleApi; +namespace StudioMitte\Riddle\Tests\Unit\Hooks; + +use PHPUnit\Framework\Attributes\Test; use StudioMitte\Riddle\Hooks\ItemsProcFunc; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\TestingFramework\Core\AccessibleObjectInterface; -use TYPO3\TestingFramework\Core\BaseTestCase; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; -class ItemsProcFuncTest extends BaseTestCase +final class ItemsProcFuncTest extends UnitTestCase { - protected $allRiddles = [ - [ - 'id' => 123, - 'title' => 'A riddle', - 'dateCreated' => '2020-08-21 19:21:07', - 'datepublished' => '2020-09-21 19:21:07', - 'thumb' => 'https://cdn.riddle.com/img1.png', - ], - [ - 'id' => 456, - 'title' => 'Another riddle', - 'type' => 'Quiz', - 'dateCreated' => '2019-08-21 19:21:07', - 'datepublished' => '2019-09-21 19:21:07', - ], - ]; - - /** - * @test - */ + #[Test] public function selectListIsFilled(): void { - /** @var MockObject|AccessibleObjectInterface|ItemsProcFunc $mockedProvider */ - $mockedProvider = $this->getAccessibleMock(ItemsProcFunc::class, ['getAllRiddles'], [], '', false); - $mockedProvider->expects(self::once())->method('getAllRiddles')->willReturn($this->allRiddles); - - $config = []; - $mockedProvider->riddleList($config); - $expected = [ - ['A riddle [-] - 21-09-20', 123, 'https://cdn.riddle.com/img1.png'], - ['Another riddle [Quiz] - 21-09-19', 456, ''], - ]; - self::assertEquals($expected, $config['items']); - } - - /** - * @test - */ - public function allRiddlesAreReturned(): void - { - $allRiddles = [ - 'response' => [ - 'items' => [ - ['title' => 'riddle 1'], - ['title' => 'riddle 2'], - ], + $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] = 'd-m-y'; + $subject = new class () extends ItemsProcFunc { + protected function getAllRiddles(): array + { + return [ + [ + 'UUID' => 'abc', + 'title' => 'A riddle', + 'image' => 'https://cdn.riddle.com/img1.png', + 'created' => ['at' => '2020-08-21 19:21:07'], + 'modified' => ['at' => '2020-09-21 19:21:07'], + ], + [ + 'UUID' => 'def', + 'title' => 'Another riddle', + 'type' => 'Quiz', + ], + ]; + } + }; + + $config = ['items' => [['label' => '', 'value' => '']]]; + $subject->riddleList($config); + + self::assertSame( + [ + ['label' => '', 'value' => ''], + ['label' => 'A riddle [-] - 21-09-20', 'value' => 'abc', 'icon' => 'https://cdn.riddle.com/img1.png'], + ['label' => 'Another riddle [Quiz] - ', 'value' => 'def', 'icon' => ''], ], - ]; - /** @var RiddleApi $riddleApiProphecy */ - $riddleApiProphecy = $this->prophesize(RiddleApi::class); - $riddleApiProphecy->getRiddleList()->willReturn($allRiddles); - GeneralUtility::addInstance(RiddleApi::class, $riddleApiProphecy->reveal()); - - $mockedRiddleApi = $this->getAccessibleMock(ItemsProcFunc::class, ['dummy'], [], '', false); - self::assertEquals($allRiddles['response']['items'], $mockedRiddleApi->_call('getAllRiddles')); + $config['items'] + ); } } diff --git a/Tests/Unit/Hooks/PageLayoutViewTest.php b/Tests/Unit/Hooks/PageLayoutViewTest.php deleted file mode 100644 index d1749f1..0000000 --- a/Tests/Unit/Hooks/PageLayoutViewTest.php +++ /dev/null @@ -1,94 +0,0 @@ - 123, 'type' => 'demo', 'title' => 'a demo']; - - /** @var RiddleApi $riddleApiProphecy */ - $riddleApiProphecy = $this->prophesize(RiddleApi::class); - $riddleApiProphecy->getRiddleItem(123)->willReturn($riddle); - GeneralUtility::addInstance(RiddleApi::class, $riddleApiProphecy->reveal()); - - $mockedPageLayoutView = $this->getAccessibleMock(PageLayoutView::class, ['dummy'], [], '', false); - - self::assertEquals($riddle, $mockedPageLayoutView->_call('getRiddleData', 123)); - } - - /** - * @test - */ - public function extensionSummaryReturnsViewContent(): void - { - $standaloneView = $this->getAccessibleMock(StandaloneView::class, ['assignMultiple', 'render'], [], '', false); - $standaloneView->expects(self::once())->method('assignMultiple'); - $standaloneView->expects(self::once())->method('render')->willReturn('some html'); - - $mockedPageLayoutView = $this->getAccessibleMock(PageLayoutView::class, ['getView', 'getRiddleData'], [], '', false); - $mockedPageLayoutView->expects(self::once())->method('getView')->willReturn($standaloneView); - $mockedPageLayoutView->expects(self::once())->method('getRiddleData')->willReturn([]); - - $params = [ - 'row' => [ - 'pi_flexform' => '', - ], - ]; - self::assertEquals('some html', $mockedPageLayoutView->getExtensionSummary($params)); - } - - /** - * @test - */ - public function extensionSummaryTransformsExceptionIntoVariable(): void - { - $standaloneView = $this->getAccessibleMock(StandaloneView::class, ['assign', 'render'], [], '', false); - $standaloneView->expects(self::once())->method('assign')->with('apiNotConfigured', true); - $standaloneView->expects(self::once())->method('render')->willReturn('some html'); - - $mockedPageLayoutView = $this->getAccessibleMock(PageLayoutView::class, ['getView', 'getRiddleData'], [], '', false); - $mockedPageLayoutView->expects(self::once())->method('getView')->willReturn($standaloneView); - $mockedPageLayoutView->expects(self::once())->method('getRiddleData')->willThrowException(new ApiConfigurationMissingException()); - - $params = [ - 'row' => [ - 'pi_flexform' => '', - ], - ]; - self::assertEquals('some html', $mockedPageLayoutView->getExtensionSummary($params)); - } - - /** - * @test - */ - public function viewIsReturned(): void - { - $standaloneView = $this->prophesize(StandaloneView::class); - GeneralUtility::addInstance(StandaloneView::class, $standaloneView->reveal()); - - $mockedPageLayoutView = $this->getAccessibleMock(PageLayoutView::class, ['dummy'], [], '', false); - $view = $mockedPageLayoutView->_call('getView'); - self::assertInstanceOf(StandaloneView::class, $view); - } -} diff --git a/Tests/Unit/Plugin/RiddlePluginTest.php b/Tests/Unit/Plugin/RiddlePluginTest.php deleted file mode 100644 index 6b02caa..0000000 --- a/Tests/Unit/Plugin/RiddlePluginTest.php +++ /dev/null @@ -1,91 +0,0 @@ -prophesize(PhpFrontend::class); - $cacheFrontendProphecy->require(Argument::any())->willReturn(false); - $cacheFrontendProphecy->get(Argument::any())->willReturn([]); - $cacheFrontendProphecy->set(Argument::any(), Argument::any())->willReturn([]); - $cacheManagerProphecy = $this->prophesize(CacheManager::class); - $cacheManagerProphecy->getCache(Argument::any())->willReturn($cacheFrontendProphecy->reveal()); - GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal()); - - $mockedContentObjectRenderer = $this->getAccessibleMock(ContentObjectRenderer::class, ['dummy'], [], '', false); - $mockedContentObjectRenderer->_set('data', [ - 'uid' => 123, - 'pi_flexform' => ' - - - - - - ' . $id . ' - - - - -', - ]); - $mockedProvider = $this->getAccessibleMock(PluginController::class, ['getRiddleHtml'], [], '', false); - $mockedProvider->_set('cObj', $mockedContentObjectRenderer); - if ($html) { - $mockedProvider->expects(self::once())->method('getRiddleHtml')->with($id)->willReturn($html); - } else { - $mockedProvider->expects(self::never())->method('getRiddleHtml')->with($id)->willReturn($html); - } - self::assertEquals($html, $mockedProvider->run('', [])); - } - - public function htmlCodeIsDeliveredDataProvider(): array - { - return [ - 'working flexforms' => [ - 456, 'some html string', - ], - 'non working flexforms' => [ - 0, '', - ], - ]; - } - - /** - * @test - */ - public function properApiCallIsUsed() - { - /** @var RiddleApi $riddleApiProphecy */ - $riddleApiProphecy = $this->prophesize(RiddleApi::class); - $response = ['response' => 'some html']; - $riddleApiProphecy->getEmbedCode(123)->willReturn($response); - GeneralUtility::addInstance(RiddleApi::class, $riddleApiProphecy->reveal()); - - $mockedRiddleApi = $this->getAccessibleMock(PluginController::class, ['dummy'], [], '', false); - self::assertEquals('some html', $mockedRiddleApi->_call('getRiddleHtml', 123)); - } -} diff --git a/Tests/Unit/Utility/RiddleUtilityTest.php b/Tests/Unit/Utility/RiddleUtilityTest.php index 07407bd..c236de2 100644 --- a/Tests/Unit/Utility/RiddleUtilityTest.php +++ b/Tests/Unit/Utility/RiddleUtilityTest.php @@ -2,40 +2,40 @@ declare(strict_types=1); -namespace StudioMitte\Riddle\Tests\Unit\Utility; - -/** +/* * This file is part of the "riddle" Extension for TYPO3 CMS. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. */ -use Prophecy\Argument; +namespace StudioMitte\Riddle\Tests\Unit\Utility; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use StudioMitte\Riddle\Utility\RiddleUtility; -use TYPO3\CMS\Core\Cache\CacheManager; -use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\TestingFramework\Core\BaseTestCase; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; -class RiddleUtilityTest extends BaseTestCase +final class RiddleUtilityTest extends UnitTestCase { - /** - * @test - * @dataProvider apiDataIsEnrichedDataProvider - * @param array $given - * @param array $expected - */ - public function apiDataIsEnriched(array $given, array $expected): void - { - date_default_timezone_set('Europe/Berlin'); - self::assertEquals($expected, RiddleUtility::enrichRiddleData($given)); - } - - public function apiDataIsEnrichedDataProvider(): array + public static function apiDataIsEnrichedDataProvider(): array { return [ - 'both dates are transformed' => [ + 'v3 api dates are transformed' => [ + [ + 'created' => ['at' => '2020-08-21 19:21:07'], + 'modified' => ['at' => '2020-09-21 19:21:07'], + ], + [ + 'created' => ['at' => '2020-08-21 19:21:07'], + 'modified' => ['at' => '2020-09-21 19:21:07'], + '_enriched' => [ + 'dateCreated' => 1598030467, + 'datepublished' => 1600708867, + ], + ], + ], + 'legacy dates are transformed' => [ [ 'dateCreated' => '2020-08-21 19:21:07', 'datepublished' => '2020-09-21 19:21:07', @@ -62,62 +62,22 @@ public function apiDataIsEnrichedDataProvider(): array ]; } - /** - * @test - * @dataProvider idCanBeRetrievedFromFlexformsDataProvider - * @param int $expectedId - * @param string $flexforms - */ - public function idCanBeRetrievedFromFlexforms(string $expectedId, string $flexforms): void + #[Test] + #[DataProvider('apiDataIsEnrichedDataProvider')] + public function apiDataIsEnriched(array $given, array $expected): void { - $cacheFrontendProphecy = $this->prophesize(PhpFrontend::class); - $cacheFrontendProphecy->require(Argument::any())->willReturn(false); - $cacheFrontendProphecy->get(Argument::any())->willReturn([]); - $cacheFrontendProphecy->set(Argument::any(), Argument::any())->willReturn([]); - $cacheManagerProphecy = $this->prophesize(CacheManager::class); - $cacheManagerProphecy->getCache(Argument::any())->willReturn($cacheFrontendProphecy->reveal()); - GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal()); - - self::assertEquals($expectedId, RiddleUtility::getRiddleId($flexforms)); + $timezone = date_default_timezone_get(); + date_default_timezone_set('Europe/Berlin'); + try { + self::assertEquals($expected, RiddleUtility::enrichRiddleData($given)); + } finally { + date_default_timezone_set($timezone); + } } - public function idCanBeRetrievedFromFlexformsDataProvider(): array + #[Test] + public function emptyFlexFormReturnsEmptyId(): void { - return [ - 'working flexform' => [ - '264528', - ' - - - - - - 264528 - - - - -', - ], - 'non working flexform' => [ - '', - ' - - - - - - bar - - - - -', - ], - 'empty flexform' => [ - '', - '', - ], - ]; + self::assertSame('', RiddleUtility::getRiddleId('')); } } diff --git a/composer.json b/composer.json index e43fe86..0570951 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,10 @@ { "name": "studiomitte/riddle", + "description": "Integration of riddle.com into TYPO3", + "license": [ + "GPL-2.0-or-later" + ], + "type": "typo3-cms-extension", "keywords": [ "TYPO3", "extension", @@ -13,31 +18,23 @@ "homepage": "https://studiomitte.com" } ], + "homepage": "https://studiomitte.com", "support": { "issues": "https://github.com/studiomitte/riddle/issues", "source": "https://github.com/studiomitte/riddle" }, - "type": "typo3-cms-extension", - "description": "Integration of riddle.com into TYPO3", - "homepage": "https://studiomitte.com", - "license": [ - "GPL-2.0-or-later" - ], "require": { - "typo3/cms-backend": "^12", - "typo3/cms-core": "^12", - "typo3/cms-frontend": "^12", - "typo3/cms-fluid": "^12" + "php": "^8.2", + "typo3/cms-backend": "^14.0", + "typo3/cms-core": "^14.0", + "typo3/cms-fluid": "^14.0", + "typo3/cms-frontend": "^14.0" }, "require-dev": { - "typo3/cms-composer-installers": "^3.1.3 || 4.0.0-RC1 || ^5.0", - "typo3/testing-framework": "^7.0.1", - "typo3/cms-form": "^12.4.5", - "phpunit/phpunit": "^9", - "typo3/coding-standards": "^0.5.3", - "friendsofphp/php-cs-fixer": "^3.13.0", - "webmozart/assert": "^1.11.0", - "phpspec/prophecy": "^1.17" + "friendsofphp/php-cs-fixer": "^3.64", + "phpunit/phpunit": "^11.5 || ^12.1", + "typo3/coding-standards": "^0.8", + "typo3/testing-framework": "^9.2" }, "autoload": { "psr-4": { @@ -50,26 +47,25 @@ } }, "config": { - "vendor-dir": ".Build/vendor", - "bin-dir": ".Build/bin", "allow-plugins": { "typo3/class-alias-loader": true, - "typo3/cms-composer-installers": true, - "sbuerk/typo3-cmscomposerinstallers-testingframework-bridge": true - } - }, - "scripts": { - "cs": "php ./.Build/bin/php-cs-fixer fix --dry-run -v --config ./Build/php-cs-fixer/php-cs-fixer.php ./", - "csfix": "php ./.Build/bin/php-cs-fixer fix -v --config ./Build/php-cs-fixer/php-cs-fixer.php ./", - "post-autoload-dump": [ - "TYPO3\\TestingFramework\\Composer\\ExtensionTestEnvironment::prepare" - ] + "typo3/cms-composer-installers": true + }, + "bin-dir": ".Build/bin", + "sort-packages": true, + "vendor-dir": ".Build/vendor" }, "extra": { "typo3/cms": { "extension-key": "riddle", - "app-dir": ".Build", - "web-dir": ".Build/Web" + "web-dir": ".Build/public" } + }, + "scripts": { + "ci:cgl": "php-cs-fixer fix --dry-run --diff -v --config ./Build/php-cs-fixer/php-cs-fixer.php", + "ci:lint": "find . -name '*.php' -not -path './.Build/*' -print0 | xargs -0 -n1 php -l", + "ci:tests:unit": "phpunit -c Build/phpunit/UnitTests.xml", + "cs": "@ci:cgl", + "csfix": "php-cs-fixer fix -v --config ./Build/php-cs-fixer/php-cs-fixer.php" } } diff --git a/ext_emconf.php b/ext_emconf.php index e7a93e2..7d96ec7 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -8,12 +8,13 @@ 'author_email' => 'gr@studiomitte.com', 'author_company' => 'studiomitte.com', 'state' => 'stable', - 'version' => '2.0.0', + 'version' => '3.0.0', 'constraints' => [ 'depends' => [ - 'typo3' => '12.4.0-12.4.99', + 'php' => '8.2.0-8.5.99', + 'typo3' => '14.0.0-14.4.99', ], ], 'autoload' => diff --git a/ext_localconf.php b/ext_localconf.php index eec0c18..4e8dc95 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,25 +1,28 @@ - '); +defined('TYPO3') or die(); - $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1598116730] = [ - 'nodeName' => 'ExternalIcons', - 'priority' => 70, - 'class' => \StudioMitte\Riddle\Backend\FieldWizard\ExternalIcons::class, - ]; - $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1598116731] = [ - 'nodeName' => 'selectSingleWithNoIconPrefix', - 'priority' => 70, - 'class' => \StudioMitte\Riddle\Backend\Element\SelectSingleElementWithNoIcon::class, - ]; +$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1598116730] = [ + 'nodeName' => 'ExternalIcons', + 'priority' => 70, + 'class' => \StudioMitte\Riddle\Backend\FieldWizard\ExternalIcons::class, +]; +$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1598116731] = [ + 'nodeName' => 'selectSingleWithNoIconPrefix', + 'priority' => 70, + 'class' => \StudioMitte\Riddle\Backend\Element\SelectSingleElementWithNoIcon::class, +]; - $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class); - $iconRegistry->registerIcon( - 'ext-riddle-plugin', - \TYPO3\CMS\Core\Imaging\IconProvider\BitmapIconProvider::class, - ['source' => 'EXT:riddle/Resources/Public/Icons/Plugin.png'] - ); -}); +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScript( + 'riddle', + 'setup', + ' +tt_content.riddle_riddle =< lib.contentElement +tt_content.riddle_riddle { + templateName = Generic + 20 = USER + 20.userFunc = StudioMitte\Riddle\Controller\PluginController->run +} +', + 'defaultContentRendering' +); diff --git a/ext_tables.php b/ext_tables.php deleted file mode 100644 index e087ce9..0000000 --- a/ext_tables.php +++ /dev/null @@ -1,15 +0,0 @@ - \StudioMitte\Riddle\Controller\ManagementController::class . '::handleRequest', - 'access' => 'group,user', - 'name' => 'site_riddle', - 'icon' => 'EXT:riddle/Resources/Public/Icons/Extension.png', - 'labels' => 'LLL:EXT:riddle/Resources/Private/Language/locallang.xlf', - ] -); diff --git a/ext_typoscript_setup.typoscript b/ext_typoscript_setup.typoscript deleted file mode 100644 index 2711d79..0000000 --- a/ext_typoscript_setup.typoscript +++ /dev/null @@ -1,5 +0,0 @@ - -tt_content.list.20.riddle_riddle = USER -tt_content.list.20.riddle_riddle { - userFunc = StudioMitte\Riddle\Controller\PluginController->run -}