From 62f4b2d9f4f87a1311a405948e580d8937789f12 Mon Sep 17 00:00:00 2001 From: Riny van Tiggelen Date: Tue, 1 Oct 2024 22:39:52 +0200 Subject: [PATCH] [FEATURE] Implemented base structure for the whole build and testing process, executed updated php-cs-fixer settings on all files, restructured lots of classes to make them ready for testing (WIP) --- .Build/.php-cs-fixer.php | 67 -- .Build/phpstan.cms12.neon | 12 - .DS_Store | Bin 6148 -> 6148 bytes .github/workflows/ci.yml | 39 - .github/workflows/{ter.yml => publish.yml} | 2 +- .github/workflows/tests.yml | 54 ++ .gitignore | 13 +- .php-cs-fixer.php | 17 + {.Build => Build}/.phplint.yml | 0 {.Build => Build}/ExcludeFromPackaging.php | 7 +- Build/Scripts/runTests.sh | 668 ++++++++++++++++++ {.Build => Build/phpstan}/phpstan.cms11.neon | 9 +- Build/phpstan/phpstan.cms12.neon | 14 + {.Build => Build/phpstan}/phpstan.cms13.neon | 10 +- Build/phpunit/FunctionalTests.xml | 35 + Build/phpunit/FunctionalTestsBootstrap.php | 30 + Build/phpunit/UnitTests.xml | 29 + Build/phpunit/UnitTestsBootstrap.php | 87 +++ .../ModifyPageLayoutContentListener.php | 8 +- Classes/Backend/PageLayoutHeader.php | 88 +-- .../Controller/AbstractBackendController.php | 29 +- Classes/Controller/AjaxController.php | 160 +---- Classes/Controller/CrawlerController.php | 36 +- Classes/Controller/OverviewController.php | 117 +-- .../AbstractOverviewDataProvider.php | 2 +- .../OrphanedContentDataProvider.php | 6 +- .../OverviewDataProviderInterface.php | 2 +- ...WithoutDescriptionOverviewDataProvider.php | 2 +- Classes/EventListener/AbstractListener.php | 10 + .../EventListener/RecordCanonicalListener.php | 13 +- .../TableDefinitionsListener.php | 3 +- Classes/EventListener/TcaBuiltListener.php | 3 +- Classes/Form/Element/Cornerstone.php | 21 +- Classes/Form/Element/FocusKeywordAnalysis.php | 49 +- Classes/Form/Element/Insights.php | 25 +- .../Element/InternalLinkingSuggestion.php | 96 +-- Classes/Form/Element/ReadabilityAnalysis.php | 28 +- Classes/Form/Element/SnippetPreview.php | 262 +------ Classes/Frontend/AdditionalPreviewData.php | 9 +- ...terCacheableContentIsGeneratedListener.php | 8 +- Classes/Frontend/UsePageCache.php | 8 +- Classes/Hooks/BackendYoastConfig.php | 4 +- .../MetaTag/Generator/AbstractGenerator.php | 70 +- Classes/Middleware/PageRequestMiddleware.php | 8 +- Classes/PageTitle/RecordPageTitleProvider.php | 13 +- Classes/Record/Builder/TcaBuilder.php | 6 +- Classes/Service/Ajax/AbstractAjaxHandler.php | 19 + Classes/Service/Ajax/AjaxHandlerInterface.php | 13 + Classes/Service/Ajax/CrawlerHandler.php | 73 ++ .../InternalLinkingSuggestionsHandler.php | 41 ++ Classes/Service/Ajax/PreviewHandler.php | 45 ++ .../Service/Ajax/ProminentWordsHandler.php | 34 + Classes/Service/Ajax/SaveScoresHandler.php | 53 ++ .../CrawlerJavascriptConfigService.php | 33 + .../Service/{ => Crawler}/CrawlerService.php | 2 +- Classes/Service/Form/NodeTemplateService.php | 32 + .../Service/Javascript/JavascriptService.php | 56 ++ .../Javascript/JsonConfigService.php} | 4 +- Classes/Service/LinkingSuggestionsService.php | 3 +- Classes/Service/LocaleService.php | 51 +- .../Overview/Dto}/DataProviderRequest.php | 2 +- .../Overview/Dto}/LanguageMenuItem.php | 5 +- Classes/Service/Overview/Dto/OverviewData.php | 136 ++++ .../LanguageMenu/LanguageMenuFactory.php | 9 +- Classes/Service/Overview/OverviewService.php | 121 ++++ .../Overview}/Pagination/Pagination.php | 6 +- .../Overview/Pagination/PaginationService.php | 32 + .../PageLayoutHeader/PageDataService.php | 59 ++ .../PageLayoutHeaderRenderer.php | 28 + .../PageLayoutHeader/VisibilityChecker.php | 60 ++ Classes/Service/Preview/ContentParser.php | 159 +++++ Classes/Service/Preview/HttpOptionSetter.php | 28 + Classes/Service/Preview/PreviewService.php | 37 + Classes/Service/Preview/UrlContentFetcher.php | 42 ++ Classes/Service/PreviewService.php | 200 ------ Classes/Service/ProminentWordsService.php | 2 +- .../SnippetPreviewConfigurationBuilder.php | 99 +++ .../SnippetPreviewUrlGenerator.php | 166 +++++ Classes/Service/SnippetPreviewService.php | 33 +- Classes/Service/TcaService.php | 64 +- Classes/Service/UrlService.php | 15 +- Classes/Service/YoastEnvironmentService.php | 39 + .../YoastRequestService.php} | 6 +- .../BreadcrumbStructuredDataProvider.php | 3 +- .../Updates/MigratePremiumFocusKeywords.php | 2 +- Classes/Updates/MigrateRedirects.php | 2 +- Classes/Utility/JavascriptUtility.php | 35 - Classes/Utility/YoastUtility.php | 63 +- .../ViewHelpers/CrawlerProgressViewHelper.php | 2 +- .../MetaInformationViewHelper.php | 36 - Classes/ViewHelpers/RecordIconViewHelper.php | 37 - Classes/ViewHelpers/RecordLinksViewHelper.php | 6 +- .../Provider/OrphanedContentDataProvider.php | 4 +- .../PagesWithoutDescriptionDataProvider.php | 1 - Configuration/Backend/AjaxRoutes.php | 12 +- Configuration/Backend/Modules.php | 8 +- Configuration/Icons.php | 12 +- Configuration/RequestMiddlewares.php | 10 +- Configuration/Services.php | 4 +- Configuration/Services.yaml | 12 +- Configuration/TCA/Overrides/tt_content.php | 6 +- .../TCA/tx_yoastseo_prominent_word.php | 16 +- .../TCA/tx_yoastseo_related_focuskeyword.php | 16 +- Resources/Private/Partials/Overview/View.html | 4 +- Tests/Functional/Fixtures/be_users.csv | 4 + Tests/Functional/Utility/YoastUtilityTest.php | 171 +++++ .../Unit/Controller/CrawlerControllerTest.php | 79 +++ .../Controller/DashboardControllerTest.php | 52 ++ .../Unit/Controller/ModuleControllerTest.php | 41 -- Tests/Unit/Utility/YoastUtilityTest.php | 190 ----- composer.json | 77 +- ext_emconf.php | 5 +- ext_localconf.php | 2 +- ext_tables.php | 14 +- 114 files changed, 3161 insertions(+), 1751 deletions(-) delete mode 100644 .Build/.php-cs-fixer.php delete mode 100644 .Build/phpstan.cms12.neon delete mode 100644 .github/workflows/ci.yml rename .github/workflows/{ter.yml => publish.yml} (85%) create mode 100644 .github/workflows/tests.yml create mode 100644 .php-cs-fixer.php rename {.Build => Build}/.phplint.yml (100%) rename {.Build => Build}/ExcludeFromPackaging.php (95%) create mode 100755 Build/Scripts/runTests.sh rename {.Build => Build/phpstan}/phpstan.cms11.neon (81%) create mode 100644 Build/phpstan/phpstan.cms12.neon rename {.Build => Build/phpstan}/phpstan.cms13.neon (66%) create mode 100644 Build/phpunit/FunctionalTests.xml create mode 100644 Build/phpunit/FunctionalTestsBootstrap.php create mode 100644 Build/phpunit/UnitTests.xml create mode 100644 Build/phpunit/UnitTestsBootstrap.php create mode 100644 Classes/Service/Ajax/AbstractAjaxHandler.php create mode 100644 Classes/Service/Ajax/AjaxHandlerInterface.php create mode 100644 Classes/Service/Ajax/CrawlerHandler.php create mode 100644 Classes/Service/Ajax/InternalLinkingSuggestionsHandler.php create mode 100644 Classes/Service/Ajax/PreviewHandler.php create mode 100644 Classes/Service/Ajax/ProminentWordsHandler.php create mode 100644 Classes/Service/Ajax/SaveScoresHandler.php create mode 100644 Classes/Service/Crawler/CrawlerJavascriptConfigService.php rename Classes/Service/{ => Crawler}/CrawlerService.php (98%) create mode 100644 Classes/Service/Form/NodeTemplateService.php create mode 100644 Classes/Service/Javascript/JavascriptService.php rename Classes/{Utility/JsonConfigUtility.php => Service/Javascript/JsonConfigService.php} (82%) rename Classes/{Backend/Overview => Service/Overview/Dto}/DataProviderRequest.php (88%) rename Classes/{Backend/Overview/LanguageMenu => Service/Overview/Dto}/LanguageMenuItem.php (90%) create mode 100644 Classes/Service/Overview/Dto/OverviewData.php rename Classes/{Backend => Service}/Overview/LanguageMenu/LanguageMenuFactory.php (94%) create mode 100644 Classes/Service/Overview/OverviewService.php rename Classes/{ => Service/Overview}/Pagination/Pagination.php (97%) create mode 100644 Classes/Service/Overview/Pagination/PaginationService.php create mode 100644 Classes/Service/PageLayoutHeader/PageDataService.php create mode 100644 Classes/Service/PageLayoutHeader/PageLayoutHeaderRenderer.php create mode 100644 Classes/Service/PageLayoutHeader/VisibilityChecker.php create mode 100644 Classes/Service/Preview/ContentParser.php create mode 100644 Classes/Service/Preview/HttpOptionSetter.php create mode 100644 Classes/Service/Preview/PreviewService.php create mode 100644 Classes/Service/Preview/UrlContentFetcher.php delete mode 100644 Classes/Service/PreviewService.php create mode 100644 Classes/Service/SnippetPreview/SnippetPreviewConfigurationBuilder.php create mode 100644 Classes/Service/SnippetPreview/SnippetPreviewUrlGenerator.php create mode 100644 Classes/Service/YoastEnvironmentService.php rename Classes/{Utility/YoastRequestHash.php => Service/YoastRequestService.php} (74%) delete mode 100644 Classes/Utility/JavascriptUtility.php delete mode 100644 Classes/ViewHelpers/ModuleLayout/MetaInformationViewHelper.php delete mode 100644 Classes/ViewHelpers/RecordIconViewHelper.php create mode 100644 Tests/Functional/Fixtures/be_users.csv create mode 100644 Tests/Functional/Utility/YoastUtilityTest.php create mode 100644 Tests/Unit/Controller/CrawlerControllerTest.php create mode 100644 Tests/Unit/Controller/DashboardControllerTest.php delete mode 100644 Tests/Unit/Controller/ModuleControllerTest.php delete mode 100644 Tests/Unit/Utility/YoastUtilityTest.php diff --git a/.Build/.php-cs-fixer.php b/.Build/.php-cs-fixer.php deleted file mode 100644 index c52008ed..00000000 --- a/.Build/.php-cs-fixer.php +++ /dev/null @@ -1,67 +0,0 @@ -exclude('vendor') - ->exclude('node_modules') - ->exclude('public') - ->exclude('var') - ->in(__DIR__); - -$config = new PhpCsFixer\Config(); -$config - ->setRiskyAllowed(true) - ->setRules([ - '@PSR2' => true, - 'general_phpdoc_annotation_remove' => [ - 'annotations' => [ - 'author' - ] - ], - 'no_leading_import_slash' => true, - 'no_trailing_comma_in_singleline' => true, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_unused_imports' => true, - 'concat_space' => ['spacing' => 'one'], - 'no_whitespace_in_blank_line' => true, - 'ordered_imports' => true, - 'single_quote' => true, - 'no_empty_statement' => true, - 'no_extra_blank_lines' => true, - 'phpdoc_no_package' => true, - 'phpdoc_scalar' => true, - 'no_blank_lines_after_phpdoc' => true, - 'array_syntax' => ['syntax' => 'short'], - 'whitespace_after_comma_in_array' => true, - 'function_typehint_space' => true, - 'single_line_comment_style' => true, - 'no_alias_functions' => true, - 'lowercase_cast' => true, - 'no_leading_namespace_whitespace' => true, - 'native_function_casing' => true, - 'self_accessor' => true, - 'no_short_bool_cast' => true, - 'no_unneeded_control_parentheses' => true - ]) - ->setFinder($finder); - -return $config; diff --git a/.Build/phpstan.cms12.neon b/.Build/phpstan.cms12.neon deleted file mode 100644 index 9e7b1d7b..00000000 --- a/.Build/phpstan.cms12.neon +++ /dev/null @@ -1,12 +0,0 @@ -parameters: - level: 8 - paths: - - ../Classes - - ../Configuration - excludePaths: - - ../Classes/Updates - ignoreErrors: - - '#TYPO3\\CMS\\Backend\\ViewHelpers\\ModuleLayoutViewHelper#' - - '#TYPO3\\CMS\\Frontend\\Page\\PageInformation#' - - '#frontend.page.information#' - - '#protected method getRecordOverlay#' \ No newline at end of file diff --git a/.DS_Store b/.DS_Store index 73be34b7f34ba22051183d643e91513c5fc0a4f2..54a8c73858f8e7b7ae82a119e7d0b095934743fb 100644 GIT binary patch delta 50 zcmZoMXfc@J&&a$nU^gQp^JX3setParallelConfig(ParallelConfigFactory::detect()); +} else { + // Old TYPO3 config standards so manually add some rules + $config->addRules([ + 'single_line_empty_body' => true + ]); +} +$config->getFinder()->in('Classes')->in('Configuration')->in('Tests'); +return $config; diff --git a/.Build/.phplint.yml b/Build/.phplint.yml similarity index 100% rename from .Build/.phplint.yml rename to Build/.phplint.yml diff --git a/.Build/ExcludeFromPackaging.php b/Build/ExcludeFromPackaging.php similarity index 95% rename from .Build/ExcludeFromPackaging.php rename to Build/ExcludeFromPackaging.php index 4a4fb03d..f4277065 100644 --- a/.Build/ExcludeFromPackaging.php +++ b/Build/ExcludeFromPackaging.php @@ -18,7 +18,8 @@ 'tests', 'vendor', '.Build', - 'grunt' + 'Build', + 'grunt', ], 'files' => [ '.gitignore', @@ -57,6 +58,6 @@ 'travis.yml', 'webpack.config.js', 'webpack.mix.js', - 'yarn.lock' - ] + 'yarn.lock', + ], ]; diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh new file mode 100755 index 00000000..98e5aada --- /dev/null +++ b/Build/Scripts/runTests.sh @@ -0,0 +1,668 @@ +#!/usr/bin/env bash + +# +# EXT:yoast_seo test runner based on docker/podman. +# + +trap 'cleanUp;exit 2' SIGINT + +waitFor() { + local HOST=${1} + local PORT=${2} + local TESTCOMMAND=" + COUNT=0; + while ! nc -z ${HOST} ${PORT}; do + if [ \"\${COUNT}\" -gt 10 ]; then + echo \"Can not connect to ${HOST} port ${PORT}. Aborting.\"; + exit 1; + fi; + sleep 1; + COUNT=\$((COUNT + 1)); + done; + " + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name wait-for-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_ALPINE} /bin/sh -c "${TESTCOMMAND}" + if [[ $? -gt 0 ]]; then + kill -SIGINT -$$ + fi +} + +cleanUp() { + ATTACHED_CONTAINERS=$(${CONTAINER_BIN} ps --filter network=${NETWORK} --format='{{.Names}}') + for ATTACHED_CONTAINER in ${ATTACHED_CONTAINERS}; do + ${CONTAINER_BIN} rm -f ${ATTACHED_CONTAINER} >/dev/null + done + ${CONTAINER_BIN} network rm ${NETWORK} >/dev/null +} + +cleanCacheFiles() { + echo -n "Clean caches ... " + rm -rf \ + .Build/.cache \ + .php-cs-fixer.cache + echo "done" +} + +cleanRenderedDocumentationFiles() { + echo -n "Clean rendered documentation files ... " + rm -rf \ + Documentation-GENERATED-temp + echo "done" +} + +cleanComposer() { + rm -rf \ + .Build/vendor \ + .Build/bin \ + composer.lock +} + +stashComposerFiles() { + cp composer.json composer.json.orig + if [ -f "composer.json.testing" ]; then + cp composer.json composer.json.orig + fi +} + +restoreComposerFiles() { + cp composer.json composer.json.testing + mv composer.json.orig composer.json +} + +handleDbmsOptions() { + # -a, -d, -i depend on each other. Validate input combinations and set defaults. + case ${DBMS} in + mariadb) + [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="10.4" + if ! [[ ${DBMS_VERSION} =~ ^(10.4|10.5|10.6|10.7|10.8|10.9|10.10|10.11|11.0|11.1)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + mysql) + [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="8.0" + if ! [[ ${DBMS_VERSION} =~ ^(8.0|8.1|8.2|8.3)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + postgres) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="10" + if ! [[ ${DBMS_VERSION} =~ ^(10|11|12|13|14|15|16)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + sqlite) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + if [ -n "${DBMS_VERSION}" ]; then + echo "Invalid combination -d ${DBMS} -i ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + *) + echo "Invalid option -d ${DBMS}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + ;; + esac +} + +loadHelp() { + # Load help text into $HELP + read -r -d '' HELP < + Specifies which test suite to run + - cgl: cgl test and fix all php files + - clean: Clean temporary files + - cleanCache: Clean cache folders for files. + - cleanRenderedDocumentation: Clean existing rendered documentation output. + - composer: "composer" with all remaining arguments dispatched. + - composerInstall: "composer install", handy if host has no PHP + - composerInstallLowest: "composer install" with the lowest dependencies, handy if host has no PHP + - composerInstallHighest: "composer install" with the highest dependencies, handy if host has no PHP + - composerUpdate: "composer update", handy if host has no PHP + - composerNormalize: "composer normalize" + - composerValidate: "composer validate" + - functional: PHP functional tests + - lint: PHP linting + - phpstan: PHPStan static analysis + - phpstanBaseline: Generate PHPStan baseline + - unit: PHP unit tests + - renderDocumentation + - testRenderDocumentation + + -b + Container environment: + - docker + - podman + + If not specified, podman will be used if available. Otherwise, docker is used. + + -a + Only with -s functional|functionalDeprecated + Specifies to use another driver, following combinations are available: + - mysql + - mysqli (default) + - pdo_mysql + - mariadb + - mysqli (default) + - pdo_mysql + + -d + Only with -s functional|functionalDeprecated|acceptance|acceptanceComposer|acceptanceInstall + Specifies on which DBMS tests are performed + - sqlite: (default): use sqlite + - mariadb: use mariadb + - mysql: use MySQL + - postgres: use postgres + + -i version + Specify a specific database version + With "-d mariadb": + - 10.4 short-term, maintained until 2024-06-18 (default) + - 10.5 short-term, maintained until 2025-06-24 + - 10.6 long-term, maintained until 2026-06 + - 10.7 short-term, no longer maintained + - 10.8 short-term, maintained until 2023-05 + - 10.9 short-term, maintained until 2023-08 + - 10.10 short-term, maintained until 2023-11 + - 10.11 long-term, maintained until 2028-02 + - 11.0 development series + - 11.1 short-term development series + With "-d mysql": + - 8.0 maintained until 2026-04 (default) LTS + - 8.1 unmaintained since 2023-10 + - 8.2 unmaintained since 2024-01 + - 8.3 maintained until 2024-04 + With "-d postgres": + - 10 unmaintained since 2022-11-10 (default) + - 11 unmaintained since 2023-11-09 + - 12 maintained until 2024-11-14 + - 13 maintained until 2025-11-13 + - 14 maintained until 2026-11-12 + - 15 maintained until 2027-11-11 + - 16 maintained until 2028-11-09 + + -t <11|12|13> + Only with -s composerInstall|composerInstallLowest|composerInstallHighest + Specifies the TYPO3 CORE Version to be used + - 11.5: use TYPO3 v11 (default) + - 12.4: use TYPO3 v12 + - 13.3: use TYPO3 v13 + + -p <8.0|8.2|8.3|8.4> + Specifies the PHP minor version to be used + - 8.0: use PHP 8.0 + - 8.2: (default) use PHP 8.2 + - 8.3: use PHP 8.3 + - 8.4: use PHP 8.4 + + -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. + + -n + Only with -s cgl, composerNormalize + Activate dry-run in CGL check and composer normalize that does not actively change files and only prints broken ones. + + -u + Update existing typo3/core-testing-*:latest container images and remove dangling local volumes. + New images are published once in a while and only the latest ones are supported by core testing. + Use this if weird test errors occur. Also removes obsolete image versions of typo3/core-testing-*. + + -h + Show this help. + +Examples: + # Run unit tests using PHP 8.2 + ./Build/Scripts/runTests.sh -p 8.2 -s unit + + # Run functional tests using PHP 8.3 and MariaDB 10.6 using pdo_mysql + ./Build/Scripts/runTests.sh -p 8.3 -s functional -d mariadb -i 10.6 -a pdo_mysql + + # Run functional tests on postgres with xdebug, php 8.3 and execute a restricted set of tests + ./Build/Scripts/runTests.sh -x -p 8.3 -s functional -d postgres -- Tests/Functional/DummyTest.php +EOF +} + +# Test if docker exists, else exit out with error +if ! type "docker" >/dev/null 2>&1 && ! type "podman" >/dev/null 2>&1; then + echo "This script relies on docker or podman. Please install" >&2 + exit 1 +fi + +# Option defaults +# @todo Consider to switch from cgl to help as default +TEST_SUITE="cgl" +TYPO3_VERSION="11" +EXTRA_TEST_OPTIONS="" +DATABASE_DRIVER="" +DBMS="sqlite" +DBMS_VERSION="" +PHP_VERSION="8.2" +PHP_XDEBUG_ON=0 +PHP_XDEBUG_PORT=9003 +CGLCHECK_DRY_RUN=0 +CI_PARAMS="${CI_PARAMS:-}" +DOCS_PARAMS="${DOCS_PARAMS:=--pull always}" +CONTAINER_BIN="" +CONTAINER_HOST="host.docker.internal" + +# Option parsing updates above default vars +# 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 "a:b:d:i:s:p:e:t:xy:nhu" OPT; do + case ${OPT} in + a) + DATABASE_DRIVER=${OPTARG} + ;; + s) + TEST_SUITE=${OPTARG} + ;; + b) + if ! [[ ${OPTARG} =~ ^(docker|podman)$ ]]; then + INVALID_OPTIONS+=("${OPTARG}") + fi + CONTAINER_BIN=${OPTARG} + ;; + d) + DBMS=${OPTARG} + ;; + i) + DBMS_VERSION=${OPTARG} + ;; + p) + PHP_VERSION=${OPTARG} + if ! [[ ${PHP_VERSION} =~ ^(8.0|8.1|8.2|8.3|8.4)$ ]]; then + INVALID_OPTIONS+=("p ${OPTARG}") + fi + ;; + e) + EXTRA_TEST_OPTIONS=${OPTARG} + ;; + t) + TYPO3_VERSION=${OPTARG} + if ! [[ ${TYPO3_VERSION} =~ ^(11|12|13)$ ]]; then + INVALID_OPTIONS+=("-t ${OPTARG}") + fi + ;; + x) + PHP_XDEBUG_ON=1 + ;; + y) + PHP_XDEBUG_PORT=${OPTARG} + ;; + n) + CGLCHECK_DRY_RUN=1 + ;; + h) + loadHelp + echo "${HELP}" + exit 0 + ;; + u) + TEST_SUITE=update + ;; + \?) + 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 "call \".Build/Scripts/runTests.sh -h\" to display help and valid options" + exit 1 +fi + +handleDbmsOptions + +COMPOSER_ROOT_VERSION="13.0.x-dev" +HOST_UID=$(id -u) +USERSET="" +if [ $(uname) != "Darwin" ]; then + USERSET="--user $HOST_UID" +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, then go up two dirs. +THIS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$THIS_SCRIPT_DIR" || exit 1 +cd ../../ || exit 1 +ROOT_DIR="${PWD}" + +# Create .cache dir: composer need this. +mkdir -p .Build/.cache +mkdir -p .Build/web/typo3temp/var/tests + +IMAGE_PREFIX="docker.io/" +# Non-CI fetches TYPO3 images (php and nodejs) from ghcr.io +TYPO3_IMAGE_PREFIX="ghcr.io/typo3/" +CONTAINER_INTERACTIVE="-it --init" + +IS_CORE_CI=0 +# ENV var "CI" is set by gitlab-ci. We use it here to distinct 'local' and 'CI' environment. +if [ "${CI}" == "true" ]; then + IS_CORE_CI=1 + IMAGE_PREFIX="" + CONTAINER_INTERACTIVE="" +fi + +# determine default container binary to use: 1. podman 2. docker +if [[ -z "${CONTAINER_BIN}" ]]; then + if type "podman" >/dev/null 2>&1; then + CONTAINER_BIN="podman" + elif type "docker" >/dev/null 2>&1; then + CONTAINER_BIN="docker" + fi +fi + +IMAGE_PHP="${TYPO3_IMAGE_PREFIX}core-testing-$(echo "php${PHP_VERSION}" | sed -e 's/\.//'):latest" +IMAGE_ALPINE="${IMAGE_PREFIX}alpine:3.8" +IMAGE_MARIADB="docker.io/mariadb:${DBMS_VERSION}" +IMAGE_MYSQL="docker.io/mysql:${DBMS_VERSION}" +IMAGE_POSTGRES="docker.io/postgres:${DBMS_VERSION}-alpine" +IMAGE_DOCS="ghcr.io/typo3-documentation/render-guides:latest" + +# Set $1 to first mass argument, this is the optional test file or test directory to execute +shift $((OPTIND - 1)) + +SUFFIX=$(echo $RANDOM) +NETWORK="t3docsexamples-${SUFFIX}" +${CONTAINER_BIN} network create ${NETWORK} >/dev/null + +if [ ${CONTAINER_BIN} = "docker" ]; then + # docker needs the add-host for xdebug remote debugging. podman has host.container.internal built in + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} --rm --network ${NETWORK} --add-host "${CONTAINER_HOST}:host-gateway" ${USERSET} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" + CONTAINER_DOCS_PARAMS="${CONTAINER_INTERACTIVE} ${DOCS_PARAMS} --rm --network ${NETWORK} --add-host "${CONTAINER_HOST}:host-gateway" ${USERSET} -v ${ROOT_DIR}:/project" +else + # podman + CONTAINER_HOST="host.containers.internal" + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} ${CI_PARAMS} --rm --network ${NETWORK} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" + CONTAINER_DOCS_PARAMS="${CONTAINER_INTERACTIVE} ${DOCS_PARAMS} --rm --network ${NETWORK} -v ${ROOT_DIR}:/project" +fi + +if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE="-e XDEBUG_MODE=off" + XDEBUG_CONFIG=" " +else + XDEBUG_MODE="-e XDEBUG_MODE=debug -e XDEBUG_TRIGGER=foo" + XDEBUG_CONFIG="client_port=${PHP_XDEBUG_PORT} client_host=${CONTAINER_HOST}" +fi + +# Suite execution +case ${TEST_SUITE} in + cgl) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND="PHP_CS_FIXER_IGNORE_ENV=1 php -dxdebug.mode=off .Build/bin/php-cs-fixer fix -v --dry-run --diff --config=.php-cs-fixer.php --using-cache=no" + else + COMMAND="PHP_CS_FIXER_IGNORE_ENV=1 php -dxdebug.mode=off .Build/bin/php-cs-fixer fix -v --config=.php-cs-fixer.php --using-cache=no" + fi + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name cgl-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + clean) + cleanCacheFiles + cleanRenderedDocumentationFiles + ;; + cleanCache) + cleanCacheFiles + ;; + cleanRenderedDocumentation) + cleanRenderedDocumentationFiles + ;; + composer) + COMMAND=(composer "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + composerNormalize) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND=(composer normalize --no-check-lock --dry-run) + else + COMMAND=(composer normalize) + fi + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + composerUpdate) + rm -rf .Build/bin/ .Build/typo3 .Build/vendor .Build/Web ./composer.lock + cp ${ROOT_DIR}/composer.json ${ROOT_DIR}/composer.json.orig + if [ -f "${ROOT_DIR}/composer.json.testing" ]; then + cp ${ROOT_DIR}/composer.json ${ROOT_DIR}/composer.json.orig + fi + COMMAND=(composer require --no-ansi --no-interaction --no-progress) + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-install-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + cp ${ROOT_DIR}/composer.json ${ROOT_DIR}/composer.json.testing + mv ${ROOT_DIR}/composer.json.orig ${ROOT_DIR}/composer.json + ;; + composerInstallHighest) + cleanComposer + stashComposerFiles + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-install-highest-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/bash -c " + if [ ${TYPO3_VERSION} -eq 11 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^11.5 typo3/cms-dashboard:^11.5 || exit 1 + fi + if [ ${TYPO3_VERSION} -eq 12 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^12.4 typo3/cms-dashboard:^12.4 || exit 1 + fi + if [ ${TYPO3_VERSION} -eq 13 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^13.3 typo3/cms-dashboard:^13.3 || exit 1 + fi + composer update --no-progress --no-interaction || exit 1 + composer show || exit 1 + " + SUITE_EXIT_CODE=$? + restoreComposerFiles + ;; + composerInstallLowest) + cleanComposer + stashComposerFiles + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-install-lowest-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/bash -c " + if [ ${TYPO3_VERSION} -eq 11 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^11.5 typo3/cms-dashboard:^11.5 || exit 1 + fi + if [ ${TYPO3_VERSION} -eq 12 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^12.4 typo3/cms-dashboard:^12.4 || exit 1 + fi + if [ ${TYPO3_VERSION} -eq 13 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^13.3 typo3/cms-dashboard:^13.3 || exit 1 + fi + composer update --no-ansi --no-interaction --no-progress --with-dependencies --prefer-lowest || exit 1 + composer show || exit 1 + " + SUITE_EXIT_CODE=$? + restoreComposerFiles + ;; + composerValidate) + COMMAND=(composer validate "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + functional) + CONTAINER_PARAMS="" + COMMAND=(.Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml --exclude-group not-${DBMS} ${EXTRA_TEST_OPTIONS} "$@") + case ${DBMS} in + mariadb) + echo "Using driver: ${DATABASE_DRIVER}" + ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name mariadb-func-${SUFFIX} --network ${NETWORK} -d -e MYSQL_ROOT_PASSWORD=funcp --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MARIADB} >/dev/null + waitFor mariadb-func-${SUFFIX} 3306 + CONTAINERPARAMS="-e typo3DatabaseDriver=${DATABASE_DRIVER} -e typo3DatabaseName=func_test -e typo3DatabaseUsername=root -e typo3DatabaseHost=mariadb-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + mysql) + echo "Using driver: ${DATABASE_DRIVER}" + ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name mysql-func-${SUFFIX} --network ${NETWORK} -d -e MYSQL_ROOT_PASSWORD=funcp --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MYSQL} >/dev/null + waitFor mysql-func-${SUFFIX} 3306 + CONTAINERPARAMS="-e typo3DatabaseDriver=${DATABASE_DRIVER} -e typo3DatabaseName=func_test -e typo3DatabaseUsername=root -e typo3DatabaseHost=mysql-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + postgres) + ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name postgres-func-${SUFFIX} --network ${NETWORK} -d -e POSTGRES_PASSWORD=funcp -e POSTGRES_USER=funcu --tmpfs /var/lib/postgresql/data:rw,noexec,nosuid ${IMAGE_POSTGRES} >/dev/null + waitFor postgres-func-${SUFFIX} 5432 + CONTAINERPARAMS="-e typo3DatabaseDriver=pdo_pgsql -e typo3DatabaseName=bamboo -e typo3DatabaseUsername=funcu -e typo3DatabaseHost=postgres-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + sqlite) + # create sqlite tmpfs mount typo3temp/var/tests/functional-sqlite-dbs/ to avoid permission issues + mkdir -p "${ROOT_DIR}/.Build/web/typo3temp/var/tests/functional-sqlite-dbs/" + CONTAINERPARAMS="-e typo3DatabaseDriver=pdo_sqlite --tmpfs ${ROOT_DIR}/.Build/web/typo3temp/var/tests/functional-sqlite-dbs/:rw,noexec,nosuid" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + esac + ;; + lint) + COMMAND="find . -name \\*.php ! -path "./.Build/\\*" -print0 | xargs -0 -n1 -P4 php -dxdebug.mode=off -l >/dev/null" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + phpstan) + COMMAND="php -dxdebug.mode=off .Build/bin/phpstan --configuration=Build/phpstan/phpstan.cms${TYPO3_VERSION}.neon ${EXTRA_TEST_OPTIONS}" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name phpstan-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + phpstanBaseline) + COMMAND="php -dxdebug.mode=off .Build/bin/phpstan --configuration=Build/phpstan/phpstan.neon --generate-baseline=Build/phpstan/phpstan-baseline.neon -v" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name phpstan-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + renderDocumentation) + COMMAND=(--config=Documentation "$@") + mkdir -p Documentation-GENERATED-temp + ${CONTAINER_BIN} run ${CONTAINER_INTERACTIVE} ${CONTAINER_DOCS_PARAMS} --name render-documentation-${SUFFIX} ${IMAGE_DOCS} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + testRenderDocumentation) + COMMAND=(--config=Documentation --no-progress --fail-on-log "$@") + mkdir -p Documentation-GENERATED-temp + ${CONTAINER_BIN} run ${CONTAINER_INTERACTIVE} ${CONTAINER_DOCS_PARAMS} --name render-documentation-test-${SUFFIX} ${IMAGE_DOCS} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + unit) + COMMAND=(.Build/bin/phpunit -c Build/phpunit/UnitTests.xml ${EXTRA_TEST_OPTIONS} "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name unit-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + update) + # pull typo3/core-testing-* versions of those ones that exist locally + echo "> pull ${TYPO3_IMAGE_PREFIX}core-testing-* versions of those ones that exist locally" + ${CONTAINER_BIN} images "${TYPO3_IMAGE_PREFIX}core-testing-*" --format "{{.Repository}}:{{.Tag}}" | xargs -I {} ${CONTAINER_BIN} pull {} + echo "" + # remove "dangling" typo3/core-testing-* images (those tagged as ) + echo "> remove \"dangling\" ${TYPO3_IMAGE_PREFIX}/core-testing-* images (those tagged as )" + ${CONTAINER_BIN} images --filter "reference=${TYPO3_IMAGE_PREFIX}/core-testing-*" --filter "dangling=true" --format "{{.ID}}" | xargs -I {} ${CONTAINER_BIN} rmi -f {} + echo "" + ;; + *) + loadHelp + echo "Invalid -s option argument ${TEST_SUITE}" >&2 + echo >&2 + echo "${HELP}" >&2 + exit 1 + ;; +esac + +cleanUp + +# Print summary +echo "" >&2 +echo "###########################################################################" >&2 +echo "Result of ${TEST_SUITE}" >&2 +echo "Container runtime: ${CONTAINER_BIN}" >&2 +if [[ ${IS_CORE_CI} -eq 1 ]]; then + echo "Environment: CI" >&2 +else + echo "Environment: local" >&2 +fi +echo "PHP: ${PHP_VERSION}" >&2 +echo "TYPO3: ${TYPO3_VERSION}" >&2 +if [[ ${TEST_SUITE} =~ ^functional$ ]]; then + case "${DBMS}" in + mariadb|mysql) + echo "DBMS: ${DBMS} version ${DBMS_VERSION} driver ${DATABASE_DRIVER}" >&2 + ;; + postgres) + echo "DBMS: ${DBMS} version ${DBMS_VERSION} driver pdo_pgsql" >&2 + ;; + sqlite) + echo "DBMS: ${DBMS} driver pdo_sqlite" >&2 + ;; + esac +fi +if [[ ${SUITE_EXIT_CODE} -eq 0 ]]; then + echo "SUCCESS" >&2 +else + echo "FAILURE" >&2 +fi +echo "###########################################################################" >&2 +echo "" >&2 + +# Exit with code of test suite - This script return non-zero if the executed test failed. +exit $SUITE_EXIT_CODE \ No newline at end of file diff --git a/.Build/phpstan.cms11.neon b/Build/phpstan/phpstan.cms11.neon similarity index 81% rename from .Build/phpstan.cms11.neon rename to Build/phpstan/phpstan.cms11.neon index 96bdbd38..53132d6f 100644 --- a/.Build/phpstan.cms11.neon +++ b/Build/phpstan/phpstan.cms11.neon @@ -1,10 +1,10 @@ parameters: level: 8 paths: - - ../Classes - - ../Configuration + - ../../Classes + - ../../Configuration excludePaths: - - ../Classes/Updates + - ../../Classes/Updates ignoreErrors: - '#Parameter \$event of method#' - '#TYPO3\\CMS\\Frontend\\Page\\PageInformation#' @@ -19,3 +19,6 @@ parameters: - '#loadJavaScriptModule#' - '#getLanguageCode#' - '#addJsInlineCode#' + typo3: + requestGetAttributeMapping: + handlerRequest: string diff --git a/Build/phpstan/phpstan.cms12.neon b/Build/phpstan/phpstan.cms12.neon new file mode 100644 index 00000000..21268d53 --- /dev/null +++ b/Build/phpstan/phpstan.cms12.neon @@ -0,0 +1,14 @@ +parameters: + level: 8 + paths: + - ../../Classes + - ../../Configuration + excludePaths: + - ../../Classes/Updates + ignoreErrors: + - '#TYPO3\\CMS\\Frontend\\Page\\PageInformation#' + - '#frontend.page.information#' + - '#protected method getRecordOverlay#' + typo3: + requestGetAttributeMapping: + handlerRequest: string \ No newline at end of file diff --git a/.Build/phpstan.cms13.neon b/Build/phpstan/phpstan.cms13.neon similarity index 66% rename from .Build/phpstan.cms13.neon rename to Build/phpstan/phpstan.cms13.neon index 7b1e54ac..8b3fad89 100644 --- a/.Build/phpstan.cms13.neon +++ b/Build/phpstan/phpstan.cms13.neon @@ -1,15 +1,15 @@ parameters: level: 8 paths: - - ../Classes - - ../Configuration + - ../../Classes + - ../../Configuration excludePaths: - - ../Classes/Updates + - ../../Classes/Updates ignoreErrors: - - '#TYPO3\\CMS\\Backend\\ViewHelpers\\ModuleLayoutViewHelper#' - '#TYPO3\\CMS\\Backend\\Template\\ModuleTemplate#' - '#getRecordOverlay#' - '#loadRequireJsModule#' typo3: requestGetAttributeMapping: - frontend.page.information: TYPO3\CMS\Frontend\Page\PageInformation \ No newline at end of file + frontend.page.information: TYPO3\CMS\Frontend\Page\PageInformation + handlerRequest: string diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml new file mode 100644 index 00000000..b1697832 --- /dev/null +++ b/Build/phpunit/FunctionalTests.xml @@ -0,0 +1,35 @@ + + + + + + ../../Tests/Functional/ + + + + + + + + + \ No newline at end of file diff --git a/Build/phpunit/FunctionalTestsBootstrap.php b/Build/phpunit/FunctionalTestsBootstrap.php new file mode 100644 index 00000000..a95bc520 --- /dev/null +++ b/Build/phpunit/FunctionalTestsBootstrap.php @@ -0,0 +1,30 @@ +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 new file mode 100644 index 00000000..33a62ed9 --- /dev/null +++ b/Build/phpunit/UnitTests.xml @@ -0,0 +1,29 @@ + + + + + + ../../Tests/Unit/ + + + + + + + + \ No newline at end of file diff --git a/Build/phpunit/UnitTestsBootstrap.php b/Build/phpunit/UnitTestsBootstrap.php new file mode 100644 index 00000000..0c0b8e71 --- /dev/null +++ b/Build/phpunit/UnitTestsBootstrap.php @@ -0,0 +1,87 @@ +getWebRoot(), '/')); + } + if (!getenv('TYPO3_PATH_WEB')) { + putenv('TYPO3_PATH_WEB=' . rtrim($testbase->getWebRoot(), '/')); + } + + $testbase->defineSitePath(); + + // We can use the "typo3/cms-composer-installers" constant "TYPO3_COMPOSER_MODE" to determine composer mode. + // This should be always true except for TYPO3 mono repository. + $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/Classes/Backend/ModifyPageLayoutContentListener.php b/Classes/Backend/ModifyPageLayoutContentListener.php index ca0e3439..2161b689 100644 --- a/Classes/Backend/ModifyPageLayoutContentListener.php +++ b/Classes/Backend/ModifyPageLayoutContentListener.php @@ -5,13 +5,15 @@ namespace YoastSeoForTypo3\YoastSeo\Backend; use TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent; -use TYPO3\CMS\Core\Utility\GeneralUtility; class ModifyPageLayoutContentListener { + public function __construct( + protected PageLayoutHeader $pageLayoutHeader + ) {} + public function __invoke(ModifyPageLayoutContentEvent $event): void { - $pageLayoutHeader = GeneralUtility::makeInstance(PageLayoutHeader::class); - $event->addHeaderContent($pageLayoutHeader->render([], $event->getModuleTemplate())); + $event->addHeaderContent($this->pageLayoutHeader->render([], $event->getModuleTemplate())); } } diff --git a/Classes/Backend/PageLayoutHeader.php b/Classes/Backend/PageLayoutHeader.php index 29e32dbd..3a5d6f76 100644 --- a/Classes/Backend/PageLayoutHeader.php +++ b/Classes/Backend/PageLayoutHeader.php @@ -7,19 +7,23 @@ use TYPO3\CMS\Backend\Controller\PageLayoutController; use TYPO3\CMS\Backend\Template\ModuleTemplate; use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; +use YoastSeoForTypo3\YoastSeo\Service\PageLayoutHeader\PageDataService; +use YoastSeoForTypo3\YoastSeo\Service\PageLayoutHeader\PageLayoutHeaderRenderer; +use YoastSeoForTypo3\YoastSeo\Service\PageLayoutHeader\VisibilityChecker; +use YoastSeoForTypo3\YoastSeo\Service\SnippetPreview\SnippetPreviewConfigurationBuilder; use YoastSeoForTypo3\YoastSeo\Service\SnippetPreviewService; use YoastSeoForTypo3\YoastSeo\Service\UrlService; -use YoastSeoForTypo3\YoastSeo\Utility\YoastUtility; class PageLayoutHeader { public function __construct( protected UrlService $urlService, - protected SnippetPreviewService $snippetPreviewService - ) { - } + protected SnippetPreviewService $snippetPreviewService, + protected SnippetPreviewConfigurationBuilder $snippetPreviewConfigurationBuilder, + protected PageLayoutHeaderRenderer $pageLayoutHeaderRenderer, + protected VisibilityChecker $visibilityChecker, + protected PageDataService $pageDataService + ) {} /** * @param array|null $params @@ -28,83 +32,19 @@ public function render(array $params = null, PageLayoutController|ModuleTemplate { $languageId = $this->getLanguageId(); $pageId = (int)$_GET['id']; - $currentPage = $this->getCurrentPage($pageId, $languageId, $parentObj); + $currentPage = $this->pageDataService->getCurrentPage($pageId, $languageId, $parentObj); - if (!is_array($currentPage) || !$this->shouldShowPreview($pageId, $currentPage)) { + if (!is_array($currentPage) || !$this->visibilityChecker->shouldShowPreview($pageId, $currentPage)) { return ''; } $this->snippetPreviewService->buildSnippetPreview( $this->urlService->getPreviewUrl($pageId, $languageId), $currentPage, - [ - 'data' => [ - 'table' => 'pages', - 'uid' => $pageId, - 'pid' => $currentPage['pid'], - 'languageId' => $languageId - ], - 'fieldSelectors' => [], - ] - ); - - return $this->renderHtml(); - } - - protected function renderHtml(): string - { - $templateView = GeneralUtility::makeInstance(StandaloneView::class); - $templateView->setTemplatePathAndFilename( - GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Templates/PageLayout/Header.html') + $this->snippetPreviewConfigurationBuilder->buildConfigurationForPage($pageId, $currentPage, $languageId) ); - $templateView->assignMultiple([ - 'targetElementId' => uniqid('_YoastSEO_panel_') - ]); - return $templateView->render(); - } - - /** - * @return array|null - */ - protected function getCurrentPage(int $pageId, int $languageId, PageLayoutController|ModuleTemplate|null $parentObj): ?array - { - if ((!$parentObj instanceof PageLayoutController && !$parentObj instanceof ModuleTemplate) || $pageId <= 0) { - return null; - } - - if ($languageId === 0) { - return BackendUtility::getRecord( - 'pages', - $pageId - ); - } - - if ($languageId > 0) { - $overlayRecords = BackendUtility::getRecordLocalization( - 'pages', - $pageId, - $languageId - ); - - if (is_array($overlayRecords) && array_key_exists(0, $overlayRecords) && is_array($overlayRecords[0])) { - return $overlayRecords[0]; - } - } - - return null; - } - - /** - * @param array $pageRecord - */ - protected function shouldShowPreview(int $pageId, array $pageRecord): bool - { - if (!YoastUtility::snippetPreviewEnabled($pageId, $pageRecord)) { - return false; - } - $allowedDoktypes = YoastUtility::getAllowedDoktypes(); - return isset($pageRecord['doktype']) && in_array((int)$pageRecord['doktype'], $allowedDoktypes, true); + return $this->pageLayoutHeaderRenderer->render(); } protected function getLanguageId(): int diff --git a/Classes/Controller/AbstractBackendController.php b/Classes/Controller/AbstractBackendController.php index 9799f72b..58d02097 100644 --- a/Classes/Controller/AbstractBackendController.php +++ b/Classes/Controller/AbstractBackendController.php @@ -7,9 +7,7 @@ use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Backend\Template\ModuleTemplate; use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; -use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Information\Typo3Version; -use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; use YoastSeoForTypo3\YoastSeo\Traits\BackendUserTrait; @@ -17,7 +15,12 @@ abstract class AbstractBackendController extends ActionController { - use BackendUserTrait, LanguageServiceTrait; + use BackendUserTrait; + use LanguageServiceTrait; + + public function __construct( + protected ModuleTemplateFactory $moduleTemplateFactory + ) {} /** * @param array $data @@ -36,7 +39,6 @@ protected function returnResponse(string $template, array $data = [], ModuleTemp $moduleTemplate->setContent($this->view->render()); return $this->htmlResponse($moduleTemplate->renderContent()); } - $moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->getPageInformation()); $moduleTemplate->assignMultiple($data); return $moduleTemplate->renderResponse($template); @@ -44,23 +46,6 @@ protected function returnResponse(string $template, array $data = [], ModuleTemp protected function getModuleTemplate(): ModuleTemplate { - $moduleTemplateFactory = GeneralUtility::makeInstance(ModuleTemplateFactory::class); - return $moduleTemplateFactory->create($this->request); - } - - /** - * @return array - */ - protected function getPageInformation(): array - { - $id = (int)($this->request->getQueryParams()['id'] ?? 0); - if ($id === 0) { - return []; - } - $pageInformation = BackendUtility::readPageAccess( - $id, - $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW) - ); - return is_array($pageInformation) ? $pageInformation : []; + return $this->moduleTemplateFactory->create($this->request); } } diff --git a/Classes/Controller/AjaxController.php b/Classes/Controller/AjaxController.php index b89e4a8f..9cbd3c0f 100644 --- a/Classes/Controller/AjaxController.php +++ b/Classes/Controller/AjaxController.php @@ -4,183 +4,57 @@ namespace YoastSeoForTypo3\YoastSeo\Controller; -use Throwable; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Http\HtmlResponse; -use TYPO3\CMS\Core\Http\JsonResponse; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use YoastSeoForTypo3\YoastSeo\Service\CrawlerService; -use YoastSeoForTypo3\YoastSeo\Service\LinkingSuggestionsService; -use YoastSeoForTypo3\YoastSeo\Service\PreviewService; -use YoastSeoForTypo3\YoastSeo\Service\ProminentWordsService; -use YoastSeoForTypo3\YoastSeo\Service\UrlService; +use YoastSeoForTypo3\YoastSeo\Service\Ajax\CrawlerHandler; +use YoastSeoForTypo3\YoastSeo\Service\Ajax\InternalLinkingSuggestionsHandler; +use YoastSeoForTypo3\YoastSeo\Service\Ajax\PreviewHandler; +use YoastSeoForTypo3\YoastSeo\Service\Ajax\ProminentWordsHandler; +use YoastSeoForTypo3\YoastSeo\Service\Ajax\SaveScoresHandler; class AjaxController { public function __construct( - protected PreviewService $previewService, - protected UrlService $urlService, - protected ProminentWordsService $prominentWordsService, - protected LinkingSuggestionsService $linkingSuggestionsService, - protected CrawlerService $crawlerService - ) { - } + protected PreviewHandler $previewHandler, + protected SaveScoresHandler $saveScoresHandler, + protected ProminentWordsHandler $prominentWordsHandler, + protected InternalLinkingSuggestionsHandler $internalLinkingSuggestionsHandler, + protected CrawlerHandler $crawlerHandler, + ) {} public function previewAction( ServerRequestInterface $request ): ResponseInterface { - $queryParams = $request->getQueryParams(); - - if (!isset($queryParams['pageId'], $queryParams['languageId'], $queryParams['additionalGetVars'])) { - $json = $this->getJsonData($request); - if (isset($json['pageId'], $json['languageId'], $json['additionalGetVars'])) { - $queryParams = $json; - } else { - return new JsonResponse([]); - } - } - - $content = $this->previewService->getPreviewData( - $this->urlService->getUriToCheck( - (int)$queryParams['pageId'], - (int)$queryParams['languageId'], - (string)$queryParams['additionalGetVars'] - ), - (int)$queryParams['pageId'] - ); - - return new HtmlResponse($content); + return $this->previewHandler->handle($request); } public function saveScoresAction( ServerRequestInterface $request ): ResponseInterface { - $data = $this->getJsonData($request); - if (!empty($data['table']) && !empty($data['uid'])) { - $this->saveScores($data); - } - return new JsonResponse($data); - } - - /** - * @param array $data - */ - protected function saveScores(array $data): void - { - $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($data['table']); - try { - $row = $connection->select(['*'], $data['table'], ['uid' => (int)$data['uid']], [], - [], - 1)->fetchAssociative(); - } catch (Throwable) { - return; - } - - if ($row !== false && isset($row['tx_yoastseo_score_readability'], $row['tx_yoastseo_score_seo'])) { - $connection->update($data['table'], [ - 'tx_yoastseo_score_readability' => (string)$data['readabilityScore'], - 'tx_yoastseo_score_seo' => (string)$data['seoScore'], - ], ['uid' => (int)$data['uid']]); - } + return $this->saveScoresHandler->handle($request); } public function promimentWordsAction( ServerRequestInterface $request ): ResponseInterface { - $data = $this->getJsonData($request); - - if (isset($data['words'], $data['uid'])) { - $this->prominentWordsService->saveProminentWords( - (int)$data['uid'], - isset($data['pid']) ? (int)$data['pid'] : null, - $data['table'] ?? 'pages', - (int)($data['languageId'] ?? 0), - (array)$data['words'] - ); - } - - return new JsonResponse(['OK']); + return $this->prominentWordsHandler->handle($request); } public function internalLinkingSuggestionsAction( ServerRequestInterface $request ): ResponseInterface { - $data = $this->getJsonData($request); - - $words = $data['words'] ?? []; - $excludedPageId = (int)($data['excludedPage'] ?? 0); - $languageId = (int)($data['languageId'] ?? 0); - $content = (string)($data['content'] ?? ''); - - $links = $this->linkingSuggestionsService->getLinkingSuggestions( - $words, - $excludedPageId, - $languageId, - $content - ); - - return new JsonResponse([ - 'OK', - 'links' => $links, - 'excludedPage' => $excludedPageId, - 'languageId' => $languageId, - ]); + return $this->internalLinkingSuggestionsHandler->handle($request); } public function crawlerDeterminePages( ServerRequestInterface $request ): ResponseInterface { - $crawlerData = $this->getCrawlerRequestData($request); - $amount = $this->crawlerService->getAmountOfPages($crawlerData['site'], $crawlerData['language']); - if ($amount > 0) { - return new JsonResponse([ - 'amount' => $amount, - ]); - } - return new JsonResponse([ - 'error' => 'No pages found to analyse', - ]); + return $this->crawlerHandler->handle($request->withAttribute('handlerRequest', 'determine')); } public function crawlerIndexPages( ServerRequestInterface $request ): ResponseInterface { - $crawlerData = $this->getCrawlerRequestData($request); - $indexInformation = $this->crawlerService->getIndexInformation( - $crawlerData['site'], - $crawlerData['language'], - $crawlerData['offset'] - ); - if (count($indexInformation['pages']) === 0) { - return new JsonResponse(['status' => 'finished', 'total' => $indexInformation['total']]); - } - return new JsonResponse($indexInformation); - } - - /** - * @return array - */ - protected function getCrawlerRequestData(ServerRequestInterface $request): array - { - $crawlerData = $this->getJsonData($request); - if (!isset($crawlerData['site'], $crawlerData['language'])) { - die(json_encode(['error' => 'No site and language provided by request'])); - } - return [ - 'site' => (int)$crawlerData['site'], - 'language' => (int)$crawlerData['language'], - 'offset' => (int)($crawlerData['offset'] ?? 0), - ]; - } - - /** - * @return array - */ - protected function getJsonData(ServerRequestInterface $request): array - { - $body = $request->getBody()->getContents(); - return json_decode($body, true); + return $this->crawlerHandler->handle($request->withAttribute('handlerRequest', 'index')); } } diff --git a/Classes/Controller/CrawlerController.php b/Classes/Controller/CrawlerController.php index 47afcff2..8454096a 100644 --- a/Classes/Controller/CrawlerController.php +++ b/Classes/Controller/CrawlerController.php @@ -5,48 +5,32 @@ namespace YoastSeoForTypo3\YoastSeo\Controller; use Psr\Http\Message\ResponseInterface; -use TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; use TYPO3\CMS\Core\Site\SiteFinder; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use YoastSeoForTypo3\YoastSeo\Utility\JavascriptUtility; -use YoastSeoForTypo3\YoastSeo\Utility\JsonConfigUtility; -use YoastSeoForTypo3\YoastSeo\Utility\PathUtility; -use YoastSeoForTypo3\YoastSeo\Service\CrawlerService; +use YoastSeoForTypo3\YoastSeo\Service\Crawler\CrawlerJavascriptConfigService; +use YoastSeoForTypo3\YoastSeo\Service\Crawler\CrawlerService; class CrawlerController extends AbstractBackendController { public function __construct( + protected ModuleTemplateFactory $moduleTemplateFactory, protected CrawlerService $crawlerService, + protected CrawlerJavascriptConfigService $crawlerJavascriptConfigService, protected SiteFinder $siteFinder, - ) {} + ) { + parent::__construct($this->moduleTemplateFactory); + } public function indexAction(): ResponseInterface { - $this->addYoastJavascriptConfig(); + $this->crawlerJavascriptConfigService->addJavascriptConfig(); return $this->returnResponse('Crawler/Index', ['sites' => $this->siteFinder->getAllSites()]); } - public function resetProgressAction(int $site, int $language):? ResponseInterface + public function resetProgressAction(int $site, int $language): ?ResponseInterface { $this->crawlerService->resetProgressInformation($site, $language); return $this->redirect('index'); } - - protected function addYoastJavascriptConfig(): void - { - JavascriptUtility::loadJavascript(); - - $jsonConfigUtility = GeneralUtility::makeInstance(JsonConfigUtility::class); - $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); - $jsonConfigUtility->addConfig([ - 'urls' => [ - 'workerUrl' => PathUtility::getPublicPathToResources() . '/JavaScript/dist/worker.js', - 'preview' => (string)$uriBuilder->buildUriFromRoute('ajax_yoast_preview'), - 'determinePages' => (string)$uriBuilder->buildUriFromRoute('ajax_yoast_crawler_determine_pages'), - 'indexPages' => (string)$uriBuilder->buildUriFromRoute('ajax_yoast_crawler_index_pages'), - 'prominentWords' => (string)$uriBuilder->buildUriFromRoute('ajax_yoast_prominent_words'), - ], - ]); - } } diff --git a/Classes/Controller/OverviewController.php b/Classes/Controller/OverviewController.php index 62ef6239..03a66946 100644 --- a/Classes/Controller/OverviewController.php +++ b/Classes/Controller/OverviewController.php @@ -5,132 +5,35 @@ namespace YoastSeoForTypo3\YoastSeo\Controller; use Psr\Http\Message\ResponseInterface; -use TYPO3\CMS\Core\Pagination\ArrayPaginator; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use YoastSeoForTypo3\YoastSeo\Backend\Overview\DataProviderRequest; -use YoastSeoForTypo3\YoastSeo\Backend\Overview\LanguageMenu\LanguageMenuFactory; -use YoastSeoForTypo3\YoastSeo\DataProviders\OverviewDataProviderInterface; -use YoastSeoForTypo3\YoastSeo\Pagination\Pagination; +use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; +use YoastSeoForTypo3\YoastSeo\Service\Overview\LanguageMenu\LanguageMenuFactory; +use YoastSeoForTypo3\YoastSeo\Service\Overview\OverviewService; class OverviewController extends AbstractBackendController { - /** @var array */ - protected array $filters; - - /** - * @param iterable $filters - */ public function __construct( + protected ModuleTemplateFactory $moduleTemplateFactory, protected LanguageMenuFactory $languageMenuFactory, - iterable $filters + protected OverviewService $overviewService, ) { - foreach ($filters as $key => $dataProvider) { - $this->addFilter($key, $dataProvider); - } + parent::__construct($this->moduleTemplateFactory); } public function listAction(int $currentPage = 1): ResponseInterface { - $overviewData = $this->getOverviewData($currentPage) + ['action' => 'list']; + $overviewData = $this->overviewService->getOverviewData($this->request, $currentPage, (int)$this->settings['itemsPerPage']); $moduleTemplate = $this->getModuleTemplate(); - if (!isset($overviewData['pageInformation'])) { - return $this->returnResponse('Overview/List', $overviewData, $moduleTemplate); - } - $moduleTemplate->getDocHeaderComponent()->setMetaInformation($overviewData['pageInformation']); + $moduleTemplate->getDocHeaderComponent()->setMetaInformation($overviewData->getPageInformation()); $languageMenu = $this->languageMenuFactory->create( $this->request, $moduleTemplate, - $overviewData['pageInformation']['uid'] ?? 0 + (int)($overviewData->getPageInformation()['uid'] ?? 0) ); if ($languageMenu !== null) { $moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($languageMenu); } - return $this->returnResponse('Overview/List', $overviewData, $moduleTemplate); - } - - /** - * @return array - */ - protected function getOverviewData(int $currentPage): array - { - $pageInformation = $this->getPageInformation(); - if (!isset($pageInformation['uid'])) { - return ['noPageSelected' => true]; - } - - $filters = $this->getAvailableFilters(); - if ($filters === null) { - return []; - } - - $activeFilter = $this->getActiveFilter(); - $items = $activeFilter->process(); - - $arrayPaginator = GeneralUtility::makeInstance( - ArrayPaginator::class, - $items, - $currentPage, - (int)$this->settings['itemsPerPage'] - ); - $pagination = GeneralUtility::makeInstance(Pagination::class, $arrayPaginator); - - return [ - 'pageInformation' => $pageInformation, - 'items' => $items, - 'paginator' => $arrayPaginator, - 'pagination' => $pagination, - 'filters' => $filters, - 'activeFilter' => $activeFilter, - 'params' => $this->getDataProviderRequest(), - ]; - } - - /** - * @return array|null - */ - public function getAvailableFilters(): ?array - { - if ($this->filters === []) { - return null; - } - - $dataProviderRequest = $this->getDataProviderRequest(); - foreach ($this->filters as $dataProvider) { - $dataProvider->initialize($dataProviderRequest); - } - - return $this->filters; - } - - protected function getActiveFilter(): OverviewDataProviderInterface - { - if ($this->filters === []) { - throw new \RuntimeException('No filters available'); - } - - if ($this->request->hasArgument('filter')) { - $activeFilter = $this->request->getArgument('filter'); - if (is_string($activeFilter) && isset($this->filters[$activeFilter])) { - return $this->filters[$activeFilter]; - } - } - - return current($this->filters); - } - - protected function getDataProviderRequest(): DataProviderRequest - { - return new DataProviderRequest( - (int)($this->request->getQueryParams()['id'] ?? 0), - (int)($this->request->getQueryParams()['tx_yoastseo_yoast_yoastseooverview']['language'] ?? $this->request->getQueryParams()['language'] ?? 0), - 'pages' - ); - } - - protected function addFilter(string $key, OverviewDataProviderInterface $dataProvider): void - { - $this->filters[$key] = $dataProvider; + return $this->returnResponse('Overview/List', $overviewData->toArray(), $moduleTemplate); } } diff --git a/Classes/DataProviders/AbstractOverviewDataProvider.php b/Classes/DataProviders/AbstractOverviewDataProvider.php index c9cea0f2..3060e0ab 100644 --- a/Classes/DataProviders/AbstractOverviewDataProvider.php +++ b/Classes/DataProviders/AbstractOverviewDataProvider.php @@ -7,7 +7,7 @@ use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Platform\PlatformInformation; use TYPO3\CMS\Core\Utility\GeneralUtility; -use YoastSeoForTypo3\YoastSeo\Backend\Overview\DataProviderRequest; +use YoastSeoForTypo3\YoastSeo\Service\Overview\Dto\DataProviderRequest; use YoastSeoForTypo3\YoastSeo\Utility\PageAccessUtility; abstract class AbstractOverviewDataProvider implements OverviewDataProviderInterface diff --git a/Classes/DataProviders/OrphanedContentDataProvider.php b/Classes/DataProviders/OrphanedContentDataProvider.php index d054b07a..c959fc03 100644 --- a/Classes/DataProviders/OrphanedContentDataProvider.php +++ b/Classes/DataProviders/OrphanedContentDataProvider.php @@ -48,7 +48,7 @@ public function getResults(array $pageIds = []): ?Result $constraints = [ $qb->expr()->in('doktype', YoastUtility::getAllowedDoktypes()), - $qb->expr()->eq('sys_language_uid', $this->dataProviderRequest->getLanguage()) + $qb->expr()->eq('sys_language_uid', $this->dataProviderRequest->getLanguage()), ]; if (count($this->referencedPages) > 0) { $constraints[] = $qb->expr()->notIn('uid', $this->referencedPages); @@ -79,9 +79,9 @@ protected function getReferencedPages(): array 'field', $qb->createNamedParameter([ 'l10n_parent', - 'db_mountpoints' + 'db_mountpoints', ], Connection::PARAM_STR_ARRAY) - ) + ), ]; $references = $qb->select('ref_uid') diff --git a/Classes/DataProviders/OverviewDataProviderInterface.php b/Classes/DataProviders/OverviewDataProviderInterface.php index 930d170a..f9214a0e 100644 --- a/Classes/DataProviders/OverviewDataProviderInterface.php +++ b/Classes/DataProviders/OverviewDataProviderInterface.php @@ -5,7 +5,7 @@ namespace YoastSeoForTypo3\YoastSeo\DataProviders; use Doctrine\DBAL\Result; -use YoastSeoForTypo3\YoastSeo\Backend\Overview\DataProviderRequest; +use YoastSeoForTypo3\YoastSeo\Service\Overview\Dto\DataProviderRequest; interface OverviewDataProviderInterface { diff --git a/Classes/DataProviders/PagesWithoutDescriptionOverviewDataProvider.php b/Classes/DataProviders/PagesWithoutDescriptionOverviewDataProvider.php index 3dcabb64..580ad8b2 100644 --- a/Classes/DataProviders/PagesWithoutDescriptionOverviewDataProvider.php +++ b/Classes/DataProviders/PagesWithoutDescriptionOverviewDataProvider.php @@ -44,7 +44,7 @@ public function getResults(array $pageIds = []): ?Result ), $queryBuilder->expr()->in('doktype', YoastUtility::getAllowedDoktypes()), $queryBuilder->expr()->eq('tx_yoastseo_hide_snippet_preview', 0), - $queryBuilder->expr()->eq('sys_language_uid', $this->dataProviderRequest->getLanguage()) + $queryBuilder->expr()->eq('sys_language_uid', $this->dataProviderRequest->getLanguage()), ]; if (count($pageIds) > 0) { diff --git a/Classes/EventListener/AbstractListener.php b/Classes/EventListener/AbstractListener.php index 5e1c715f..d0818a6c 100644 --- a/Classes/EventListener/AbstractListener.php +++ b/Classes/EventListener/AbstractListener.php @@ -5,10 +5,20 @@ namespace YoastSeoForTypo3\YoastSeo\EventListener; use YoastSeoForTypo3\YoastSeo\Record\Builder\AbstractBuilder; +use YoastSeoForTypo3\YoastSeo\Record\Record; +use YoastSeoForTypo3\YoastSeo\Record\RecordRegistry; abstract class AbstractListener { public function __construct( protected AbstractBuilder $builder ) {} + + /** + * @return Record[] + */ + protected function getRecordsFromRegistry(): array + { + return RecordRegistry::getInstance()->getRecords(); + } } diff --git a/Classes/EventListener/RecordCanonicalListener.php b/Classes/EventListener/RecordCanonicalListener.php index 0d1162a4..6b3afebd 100644 --- a/Classes/EventListener/RecordCanonicalListener.php +++ b/Classes/EventListener/RecordCanonicalListener.php @@ -4,22 +4,15 @@ namespace YoastSeoForTypo3\YoastSeo\EventListener; -use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Seo\Event\ModifyUrlForCanonicalTagEvent; use YoastSeoForTypo3\YoastSeo\Record\Record; use YoastSeoForTypo3\YoastSeo\Record\RecordService; class RecordCanonicalListener { - protected RecordService $recordService; - - public function __construct(RecordService $recordService = null) - { - if ($recordService === null) { - $recordService = GeneralUtility::makeInstance(RecordService::class); - } - $this->recordService = $recordService; - } + public function __construct( + protected RecordService $recordService + ) {} public function setCanonical(ModifyUrlForCanonicalTagEvent $event): void { diff --git a/Classes/EventListener/TableDefinitionsListener.php b/Classes/EventListener/TableDefinitionsListener.php index 80562f76..8a843740 100644 --- a/Classes/EventListener/TableDefinitionsListener.php +++ b/Classes/EventListener/TableDefinitionsListener.php @@ -5,13 +5,12 @@ namespace YoastSeoForTypo3\YoastSeo\EventListener; use TYPO3\CMS\Core\Database\Event\AlterTableDefinitionStatementsEvent; -use YoastSeoForTypo3\YoastSeo\Record\RecordRegistry; class TableDefinitionsListener extends AbstractListener { public function addDatabaseSchema(AlterTableDefinitionStatementsEvent $event): void { - foreach (RecordRegistry::getInstance()->getRecords() as $record) { + foreach ($this->getRecordsFromRegistry() as $record) { $this->builder ->setRecord($record) ->build(); diff --git a/Classes/EventListener/TcaBuiltListener.php b/Classes/EventListener/TcaBuiltListener.php index 4574a356..d746d3e1 100644 --- a/Classes/EventListener/TcaBuiltListener.php +++ b/Classes/EventListener/TcaBuiltListener.php @@ -5,14 +5,13 @@ namespace YoastSeoForTypo3\YoastSeo\EventListener; use TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent; -use YoastSeoForTypo3\YoastSeo\Record\RecordRegistry; class TcaBuiltListener extends AbstractListener { public function addRecordTca(AfterTcaCompilationEvent $event): void { $GLOBALS['TCA'] = $event->getTca(); - foreach (RecordRegistry::getInstance()->getRecords() as $record) { + foreach ($this->getRecordsFromRegistry() as $record) { $this->builder ->setRecord($record) ->build(); diff --git a/Classes/Form/Element/Cornerstone.php b/Classes/Form/Element/Cornerstone.php index b3c0c3b1..1de2c139 100644 --- a/Classes/Form/Element/Cornerstone.php +++ b/Classes/Form/Element/Cornerstone.php @@ -6,25 +6,28 @@ use TYPO3\CMS\Backend\Form\AbstractNode; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; +use YoastSeoForTypo3\YoastSeo\Service\Form\NodeTemplateService; class Cornerstone extends AbstractNode { + // TODO: Use constructor DI when TYPO3 v11 can be dropped + protected NodeTemplateService $templateService; + /** * @return array */ public function render(): array { - $resultArray = $this->initializeResultArray(); + $this->init(); - $templateView = GeneralUtility::makeInstance(StandaloneView::class); - $templateView->setTemplatePathAndFilename( - GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Templates/TCA/Cornerstone.html') - ); - $templateView->assign('data', $this->data); - - $resultArray['html'] = $templateView->render(); + $resultArray = $this->initializeResultArray(); + $resultArray['html'] = $this->templateService->renderView('Cornerstone', ['data' => $this->data]); return $resultArray; } + + protected function init(): void + { + $this->templateService = GeneralUtility::makeInstance(NodeTemplateService::class); + } } diff --git a/Classes/Form/Element/FocusKeywordAnalysis.php b/Classes/Form/Element/FocusKeywordAnalysis.php index b1354f74..7597e6fa 100644 --- a/Classes/Form/Element/FocusKeywordAnalysis.php +++ b/Classes/Form/Element/FocusKeywordAnalysis.php @@ -6,57 +6,47 @@ use TYPO3\CMS\Backend\Form\AbstractNode; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; +use YoastSeoForTypo3\YoastSeo\Service\Form\NodeTemplateService; use YoastSeoForTypo3\YoastSeo\Utility\YoastUtility; class FocusKeywordAnalysis extends AbstractNode { + // TODO: Use constructor DI when TYPO3 v11 can be dropped + protected NodeTemplateService $templateService; + /** * @return array */ public function render(): array { + $this->init(); $resultArray = $this->initializeResultArray(); - $templateView = $this->getTemplateView(); - if ($focusKeywordField = $this->getFocusKeywordField()) { - $templateView->assign('focusKeywordField', $this->getFieldSelector($focusKeywordField)); + if ($this->data['tableName'] === 'pages' + && !in_array((int)($this->data['databaseRow']['doktype'][0] ?? 0), YoastUtility::getAllowedDoktypes())) { + $resultArray['html'] = $this->templateService->renderView('FocusKeywordAnalysis', ['wrongDoktype' => true]); + return $resultArray; } - $allowedDoktypes = YoastUtility::getAllowedDoktypes(); - if ($this->data['tableName'] === 'pages' - && !in_array((int)($this->data['databaseRow']['doktype'][0] ?? 0), $allowedDoktypes)) { - $templateView->assign('wrongDoktype', true); + if ($focusKeywordField = $this->getFocusKeywordField()) { + $focusKeywordField = $this->getFieldSelector($focusKeywordField); } + $subtype = ''; if ($this->data['tableName'] === 'tx_yoastseo_related_focuskeyword') { $subtype = 'rk' . $this->data['vanillaUid']; } - $templateView->assign('subtype', $subtype); - $resultArray['html'] = $templateView->render(); + $resultArray['html'] = $this->templateService->renderView('FocusKeywordAnalysis', [ + 'focusKeywordField' => $focusKeywordField, + 'subtype' => $subtype, + ]); return $resultArray; } - protected function getTemplateView(): StandaloneView - { - $templateView = GeneralUtility::makeInstance(StandaloneView::class); - $templateView->setPartialRootPaths( - [GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Partials/TCA')] - ); - $templateView->setTemplatePathAndFilename( - GeneralUtility::getFileAbsFileName( - 'EXT:yoast_seo/Resources/Private/Templates/TCA/FocusKeywordAnalysis.html' - ) - ); - return $templateView; - } - protected function getFocusKeywordField(): ?string { - if (isset($this->data['parameterArray']['fieldConf']['config']['settings']['focusKeywordField']) - && !empty($this->data['parameterArray']['fieldConf']['config']['settings']['focusKeywordField']) - ) { + if (!empty($this->data['parameterArray']['fieldConf']['config']['settings']['focusKeywordField'] ?? '')) { return $this->data['parameterArray']['fieldConf']['config']['settings']['focusKeywordField']; } return null; @@ -68,4 +58,9 @@ protected function getFieldSelector(string $field): string return 'data[' . $this->data['tableName'] . '][' . $uid . '][' . $field . ']'; } + + protected function init(): void + { + $this->templateService = GeneralUtility::makeInstance(NodeTemplateService::class); + } } diff --git a/Classes/Form/Element/Insights.php b/Classes/Form/Element/Insights.php index 1671ab2b..c83ef2de 100644 --- a/Classes/Form/Element/Insights.php +++ b/Classes/Form/Element/Insights.php @@ -6,29 +6,26 @@ use TYPO3\CMS\Backend\Form\AbstractNode; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; +use YoastSeoForTypo3\YoastSeo\Service\Form\NodeTemplateService; class Insights extends AbstractNode { + // TODO: Use constructor DI when TYPO3 v11 can be dropped + protected NodeTemplateService $templateService; + /** * @return array */ public function render(): array { + $this->init(); $resultArray = $this->initializeResultArray(); - - $templateView = GeneralUtility::makeInstance(StandaloneView::class); - $templateView->setPartialRootPaths( - [GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Partials/TCA')] - ); - $templateView->setTemplatePathAndFilename( - GeneralUtility::getFileAbsFileName( - 'EXT:yoast_seo/Resources/Private/Templates/TCA/Insights.html' - ) - ); - - $templateView->assign('data', $this->data); - $resultArray['html'] = $templateView->render(); + $resultArray['html'] = $this->templateService->renderView('Insights', ['data' => $this->data]); return $resultArray; } + + protected function init(): void + { + $this->templateService = GeneralUtility::makeInstance(NodeTemplateService::class); + } } diff --git a/Classes/Form/Element/InternalLinkingSuggestion.php b/Classes/Form/Element/InternalLinkingSuggestion.php index 68aa4a34..050dfa03 100644 --- a/Classes/Form/Element/InternalLinkingSuggestion.php +++ b/Classes/Form/Element/InternalLinkingSuggestion.php @@ -6,19 +6,22 @@ use TYPO3\CMS\Backend\Form\AbstractNode; use TYPO3\CMS\Backend\Routing\UriBuilder; -use TYPO3\CMS\Core\Exception\SiteNotFoundException; -use TYPO3\CMS\Core\Page\PageRenderer; -use TYPO3\CMS\Core\Site\Entity\SiteLanguage; -use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; -use YoastSeoForTypo3\YoastSeo\Utility\JavascriptUtility; -use YoastSeoForTypo3\YoastSeo\Utility\JsonConfigUtility; +use YoastSeoForTypo3\YoastSeo\Service\Form\NodeTemplateService; +use YoastSeoForTypo3\YoastSeo\Service\Javascript\JavascriptService; +use YoastSeoForTypo3\YoastSeo\Service\Javascript\JsonConfigService; +use YoastSeoForTypo3\YoastSeo\Service\LocaleService; use YoastSeoForTypo3\YoastSeo\Utility\PathUtility; class InternalLinkingSuggestion extends AbstractNode { - protected StandaloneView $templateView; + // TODO: Use constructor DI when TYPO3 v11 can be dropped + protected LocaleService $localeService; + protected NodeTemplateService $templateService; + protected JsonConfigService $jsonConfigService; + protected JavascriptService $javascriptService; + protected UriBuilder $uriBuilder; + protected int $languageId; protected int $currentPage; @@ -29,22 +32,15 @@ public function render(): array { $this->init(); - $locale = $this->getLocale($this->currentPage); - if ($locale === null) { - $this->templateView->assign('languageError', true); - $resultArray['html'] = $this->templateView->render(); + $resultArray = $this->initializeResultArray(); + + if (($locale = $this->localeService->getLocale($this->currentPage, $this->languageId)) === null) { + $resultArray['html'] = $this->templateService->renderView('InternalLinkingSuggestion', ['languageError' => true]); return $resultArray; } - - $publicResourcesPath = PathUtility::getPublicPathToResources(); - - $resultArray = $this->initializeResultArray(); $resultArray['stylesheetFiles'][] = 'EXT:yoast_seo/Resources/Public/CSS/yoast.min.css'; - $jsonConfigUtility = GeneralUtility::makeInstance(JsonConfigUtility::class); - - $workerUrl = $publicResourcesPath . '/JavaScript/dist/worker.js'; - $config = [ + $this->jsonConfigService->addConfig([ 'isCornerstoneContent' => false, 'focusKeyphrase' => [ 'keyword' => '', @@ -58,64 +54,28 @@ public function render(): array 'locale' => $locale, ], 'urls' => [ - 'workerUrl' => $workerUrl, - 'linkingSuggestions' => (string)GeneralUtility::makeInstance(UriBuilder::class) + 'workerUrl' => PathUtility::getPublicPathToResources() . '/JavaScript/dist/worker.js', + 'linkingSuggestions' => (string)$this->uriBuilder ->buildUriFromRoute('ajax_yoast_internal_linking_suggestions'), ], - ]; - $jsonConfigUtility->addConfig($config); + ]); - $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); - JavascriptUtility::loadJavascript($pageRenderer); + $this->javascriptService->loadPluginJavascript(); - $resultArray['html'] = $this->templateView->render(); + $resultArray['html'] = $this->templateService->renderView('InternalLinkingSuggestion'); return $resultArray; } - protected function getLocale(int $pageId): ?string - { - $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); - try { - $site = $siteFinder->getSiteByPageId($pageId); - if ($this->languageId === -1) { - $this->languageId = $site->getDefaultLanguage()->getLanguageId(); - return $this->getLanguageCode($site->getDefaultLanguage()); - } - return $this->getLanguageCode($site->getLanguageById($this->languageId)); - } catch (SiteNotFoundException|\InvalidArgumentException $e) { - return null; - } - } - - protected function getLanguageCode(SiteLanguage $siteLanguage): string - { - // Support for v11 - if (method_exists($siteLanguage, 'getTwoLetterIsoCode')) { - return $siteLanguage->getTwoLetterIsoCode(); - } - return $siteLanguage->getLocale()->getLanguageCode(); - } - protected function init(): void { - $this->currentPage = $this->data['parentPageRow']['uid']; - - if (isset($this->data['databaseRow']['sys_language_uid'])) { - if (is_array($this->data['databaseRow']['sys_language_uid']) && count( - $this->data['databaseRow']['sys_language_uid'] - ) > 0) { - $this->languageId = (int)current($this->data['databaseRow']['sys_language_uid']); - } else { - $this->languageId = (int)$this->data['databaseRow']['sys_language_uid']; - } - } + $this->localeService = GeneralUtility::makeInstance(LocaleService::class); + $this->templateService = GeneralUtility::makeInstance(NodeTemplateService::class); + $this->jsonConfigService = GeneralUtility::makeInstance(JsonConfigService::class); + $this->javascriptService = GeneralUtility::makeInstance(JavascriptService::class); + $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); - $this->templateView = GeneralUtility::makeInstance(StandaloneView::class); - $this->templateView->setTemplatePathAndFilename( - GeneralUtility::getFileAbsFileName( - 'EXT:yoast_seo/Resources/Private/Templates/TCA/InternalLinkingSuggestion.html' - ) - ); + $this->currentPage = $this->data['parentPageRow']['uid']; + $this->languageId = $this->localeService->getLanguageIdFromData($this->data); } } diff --git a/Classes/Form/Element/ReadabilityAnalysis.php b/Classes/Form/Element/ReadabilityAnalysis.php index bba25420..1b84103b 100644 --- a/Classes/Form/Element/ReadabilityAnalysis.php +++ b/Classes/Form/Element/ReadabilityAnalysis.php @@ -6,38 +6,34 @@ use TYPO3\CMS\Backend\Form\AbstractNode; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; +use YoastSeoForTypo3\YoastSeo\Service\Form\NodeTemplateService; use YoastSeoForTypo3\YoastSeo\Utility\YoastUtility; class ReadabilityAnalysis extends AbstractNode { + // TODO: Use constructor DI when TYPO3 v11 can be dropped + protected NodeTemplateService $templateService; + /** * @return array */ public function render(): array { + $this->init(); $resultArray = $this->initializeResultArray(); - $templateView = $this->getTemplateView(); - $allowedDoktypes = YoastUtility::getAllowedDoktypes(); if ($this->data['tableName'] === 'pages' - && !\in_array((int)($this->data['databaseRow']['doktype'][0] ?? 0), $allowedDoktypes)) { - $templateView->assign('wrongDoktype', true); + && !in_array((int)($this->data['databaseRow']['doktype'][0] ?? 0), YoastUtility::getAllowedDoktypes())) { + $resultArray['html'] = $this->templateService->renderView('ReadabilityAnalysis', ['wrongDoktype' => true]); + return $resultArray; } - $templateView->assign('subtype', ''); - $resultArray['html'] = $templateView->render(); + + $resultArray['html'] = $this->templateService->renderView('ReadabilityAnalysis'); return $resultArray; } - protected function getTemplateView(): StandaloneView + protected function init(): void { - $templateView = GeneralUtility::makeInstance(StandaloneView::class); - $templateView->setPartialRootPaths( - [GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Partials/TCA')] - ); - $templateView->setTemplatePathAndFilename( - GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Templates/TCA/ReadabilityAnalysis.html') - ); - return $templateView; + $this->templateService = GeneralUtility::makeInstance(NodeTemplateService::class); } } diff --git a/Classes/Form/Element/SnippetPreview.php b/Classes/Form/Element/SnippetPreview.php index 688d5a5d..2ec7f703 100644 --- a/Classes/Form/Element/SnippetPreview.php +++ b/Classes/Form/Element/SnippetPreview.php @@ -5,30 +5,25 @@ namespace YoastSeoForTypo3\YoastSeo\Form\Element; use TYPO3\CMS\Backend\Form\AbstractNode; -use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Domain\Repository\PageRepository; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; -use TYPO3\CMS\Frontend\Page\CacheHashCalculator; +use YoastSeoForTypo3\YoastSeo\Service\Form\NodeTemplateService; +use YoastSeoForTypo3\YoastSeo\Service\LocaleService; +use YoastSeoForTypo3\YoastSeo\Service\SnippetPreview\SnippetPreviewConfigurationBuilder; +use YoastSeoForTypo3\YoastSeo\Service\SnippetPreview\SnippetPreviewUrlGenerator; use YoastSeoForTypo3\YoastSeo\Service\SnippetPreviewService; -use YoastSeoForTypo3\YoastSeo\Service\UrlService; use YoastSeoForTypo3\YoastSeo\Utility\YoastUtility; class SnippetPreview extends AbstractNode { - protected string $titleField = 'title'; - protected string $pageTitleField = 'title'; - protected string $descriptionField = 'description'; - protected string $focusKeywordField = 'tx_yoastseo_focuskeyword'; - protected string $focusKeywordSynonymsField = 'tx_yoastseo_focuskeyword_synonyms'; - protected string $cornerstoneField = 'tx_yoastseo_cornerstone'; - protected string $relatedKeyphrases = 'tx_yoastseo_focuskeyword_related'; - protected string $table = 'pages'; + // TODO: Use constructor DI when TYPO3 v11 can be dropped + protected NodeTemplateService $templateService; + protected SnippetPreviewConfigurationBuilder $configurationBuilder; + protected SnippetPreviewUrlGenerator $urlGenerator; + protected SnippetPreviewService $snippetPreviewService; + protected string $previewUrl = ''; protected int $languageId = 0; - protected UrlService $urlService; - /** * @return array */ @@ -39,247 +34,42 @@ public function render(): array $resultArray = $this->initializeResultArray(); $resultArray['stylesheetFiles'][] = 'EXT:yoast_seo/Resources/Public/CSS/yoast.min.css'; - $templateView = $this->getTemplateView(); - if ($this->data['tableName'] === 'pages' && !in_array((int)($this->data['databaseRow']['doktype'][0] ?? 0), YoastUtility::getAllowedDoktypes(), true)) { - $templateView->assign('wrongDoktype', true); - $resultArray['html'] = $templateView->render(); + $resultArray['html'] = $this->templateService->renderView('SnippetPreview', ['wrongDoktype' => true]); return $resultArray; } - $firstFocusKeyword = YoastUtility::getFocusKeywordOfRecord( - (int)$this->data['databaseRow']['uid'], - $this->data['tableName'] - ); - - $snippetPreviewConfiguration = [ - 'TCA' => 1, - 'data' => [ - 'table' => $this->data['tableName'], - 'uid' => (int)($this->data['defaultLanguagePageRow']['uid'] ?? $this->data['databaseRow']['uid']), - 'pid' => (int)$this->data['databaseRow']['pid'], - 'languageId' => $this->languageId - ], - 'fieldSelectors' => [ - 'title' => $this->getFieldSelector($this->titleField), - 'pageTitle' => $this->getFieldSelector($this->pageTitleField), - 'description' => $this->getFieldSelector($this->descriptionField), - 'focusKeyword' => $this->getFieldSelector($this->focusKeywordField), - 'focusKeywordSynonyms' => $this->getFieldSelector($this->focusKeywordSynonymsField), - 'cornerstone' => $this->getFieldSelector($this->cornerstoneField), - 'relatedKeyword' => $this->getFieldSelector($this->relatedKeyphrases, true), - ], - 'relatedKeyphrases' => YoastUtility::getRelatedKeyphrases( - $this->data['tableName'], - (int)$this->data['databaseRow']['uid'] - ) - ]; + $snippetPreviewConfiguration = $this->configurationBuilder->buildConfigurationForTCA($this->data, $this->languageId); - $snippetPreviewService = GeneralUtility::makeInstance(SnippetPreviewService::class); - $snippetPreviewService->buildSnippetPreview( - $this->previewUrl, - $this->data['databaseRow'], - $snippetPreviewConfiguration - ); + $this->snippetPreviewService->buildSnippetPreview($this->previewUrl, $this->data['databaseRow'], $snippetPreviewConfiguration); - $templateView->assignMultiple([ + $resultArray['html'] = $this->templateService->renderView('SnippetPreview', [ 'previewUrl' => $this->previewUrl, 'previewTargetId' => $this->data['fieldName'], - 'titleFieldSelector' => $this->getFieldSelector($this->titleField), - 'descriptionFieldSelector' => $this->getFieldSelector($this->descriptionField), + 'titleFieldSelector' => $snippetPreviewConfiguration['fieldSelectors']['title'], + 'descriptionFieldSelector' => $snippetPreviewConfiguration['fieldSelectors']['description'], 'databaseRow' => $this->data['databaseRow'], - 'focusKeyword' => $firstFocusKeyword, + 'focusKeyword' => YoastUtility::getFocusKeywordOfRecord( + (int)$this->data['databaseRow']['uid'], + $this->data['tableName'] + ), 'vanillaUid' => $this->data['vanillaUid'], 'tableName' => $this->data['tableName'], 'languageId' => $this->languageId, ]); - $resultArray['html'] = $templateView->render(); return $resultArray; } protected function initialize(): void { - $this->urlService = GeneralUtility::makeInstance(UrlService::class); - - if (!empty($this->data['parameterArray']['fieldConf']['config']['settings']['titleField'] ?? '')) { - $this->titleField = $this->data['parameterArray']['fieldConf']['config']['settings']['titleField']; - } - - if (!empty($this->data['parameterArray']['fieldConf']['config']['settings']['pageTitleField'] ?? '')) { - $this->pageTitleField = $this->data['parameterArray']['fieldConf']['config']['settings']['pageTitleField']; - } - - if (!empty($this->data['parameterArray']['fieldConf']['config']['settings']['descriptionField'] ?? '')) { - $this->descriptionField = - $this->data['parameterArray']['fieldConf']['config']['settings']['descriptionField']; - } - - if (isset($this->data['databaseRow']['sys_language_uid'])) { - if (is_array($this->data['databaseRow']['sys_language_uid']) && count( - $this->data['databaseRow']['sys_language_uid'] - ) > 0) { - $this->languageId = (int)current($this->data['databaseRow']['sys_language_uid']); - } else { - $this->languageId = (int)$this->data['databaseRow']['sys_language_uid']; - } - } - - $this->table = $this->data['tableName']; - - $this->previewUrl = $this->getPreviewUrl(); - } - - protected function getTemplateView(): StandaloneView - { - $templateView = GeneralUtility::makeInstance(StandaloneView::class); - $templateView->setPartialRootPaths( - [GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Partials/TCA')] - ); - $templateView->setTemplatePathAndFilename( - GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Templates/TCA/SnippetPreview.html') - ); - return $templateView; - } - - protected function getFieldSelector(string $field, bool $id = false): string - { - if ($id === true) { - $element = 'data-' . $this->data['vanillaUid'] . '-' . $this->data['tableName'] . '-' . $this->data['vanillaUid'] . '-' . $field; - } else { - $element = 'data' . str_replace('tx_yoastseo_snippetpreview', $field, $this->data['elementBaseName']); - } - - return $element; - } - - protected function getPreviewUrl(): string - { - $currentPageId = $this->data['effectivePid']; - $recordId = $this->data['vanillaUid']; - - $recordArray = BackendUtility::getRecord($this->table, $recordId); + $this->templateService = GeneralUtility::makeInstance(NodeTemplateService::class); + $this->configurationBuilder = GeneralUtility::makeInstance(SnippetPreviewConfigurationBuilder::class); + $this->urlGenerator = GeneralUtility::makeInstance(SnippetPreviewUrlGenerator::class); + $this->snippetPreviewService = GeneralUtility::makeInstance(SnippetPreviewService::class); - $pageTsConfig = BackendUtility::getPagesTSconfig($currentPageId); - $previewConfiguration = $pageTsConfig['TCEMAIN.']['preview.'][$this->table . '.'] ?? []; - - $previewPageId = $this->getPreviewPageId($currentPageId, $previewConfiguration); - - $linkParameters = []; - $languageId = 0; - // language handling - $languageField = $GLOBALS['TCA'][$this->table]['ctrl']['languageField'] ?? ''; - - if ($languageField && !empty($recordArray[$languageField])) { - $l18nPointer = $GLOBALS['TCA'][$this->table]['ctrl']['transOrigPointerField'] ?? ''; - if ($l18nPointer && !empty($recordArray[$l18nPointer])) { - if (isset($previewConfiguration['useDefaultLanguageRecord']) - && !$previewConfiguration['useDefaultLanguageRecord']) { - // use parent record - $recordId = $recordArray[$l18nPointer]; - } - - if ($this->table === 'pages') { - $previewPageId = $recordArray[$l18nPointer]; - } - } - $languageId = $recordArray[$languageField] > -1 ? $recordArray[$languageField] : 0; - } - - // map record data to GET parameters - if (isset($previewConfiguration['fieldToParameterMap.'])) { - foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) { - $value = $recordArray[$field] ?? ''; - if ($field === 'uid') { - $value = $recordId; - } - $linkParameters[$parameterName] = $value; - } - } - - // add/override parameters by configuration - if (isset($previewConfiguration['additionalGetParameters.'])) { - $additionalGetParameters = []; - $this->parseAdditionalGetParameters( - $additionalGetParameters, - $previewConfiguration['additionalGetParameters.'] - ); - $linkParameters = array_replace($linkParameters, $additionalGetParameters); - } - - if (!empty($previewConfiguration['useCacheHash'])) { - $cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class); - $fullLinkParameters = GeneralUtility::implodeArrayForUrl( - '', - array_merge($linkParameters, ['id' => $previewPageId]) - ); - $cacheHashParameters = $cacheHashCalculator->getRelevantParameters($fullLinkParameters); - $linkParameters['cHash'] = $cacheHashCalculator->calculateCacheHash($cacheHashParameters); - } - - $additionalParamsForUrl = GeneralUtility::implodeArrayForUrl('', $linkParameters, '', false, true); - - return $this->urlService->getPreviewUrl($previewPageId, $languageId, $additionalParamsForUrl); - } - - /** - * @param array $previewConfiguration - */ - protected function getPreviewPageId(int $currentPageId, array $previewConfiguration): int - { - // find the right preview page id - $previewPageId = (int)($previewConfiguration['previewPageId'] ?? 0); - - // if no preview page was configured - if (!$previewPageId) { - $rootPageData = null; - $rootLine = BackendUtility::BEgetRootLine($currentPageId); - $currentPage = reset($rootLine); - // Allow all doktypes below 200 - // This makes custom doktype work as well with opening a frontend page. - if ((int)$currentPage['doktype'] <= PageRepository::DOKTYPE_SPACER) { - // try the current page - $previewPageId = $currentPageId; - } else { - // or search for the root page - foreach ($rootLine as $page) { - if ($page['is_siteroot']) { - $rootPageData = $page; - break; - } - } - $previewPageId = isset($rootPageData) - ? (int)$rootPageData['uid'] - : $currentPageId; - } - } - return $previewPageId; - } - - /** - * Migrates a set of (possibly nested) GET parameters in TypoScript syntax to a - * plain array - * - * This basically removes the trailing dots of sub-array keys in TypoScript. - * The result can be used to create a query string with - * GeneralUtility::implodeArrayForUrl(). - * - * @param array $parameters Should be an empty array by default - * @param array $typoScript The TypoScript configuration - */ - protected function parseAdditionalGetParameters( - array &$parameters, - array $typoScript - ): void { - foreach ($typoScript as $key => $value) { - if (is_array($value)) { - $key = rtrim($key, '.'); - $parameters[$key] = []; - $this->parseAdditionalGetParameters($parameters[$key], $value); - } else { - $parameters[$key] = $value; - } - } + $this->previewUrl = $this->urlGenerator->getPreviewUrl($this->data); + $this->languageId = GeneralUtility::makeInstance(LocaleService::class)->getLanguageIdFromData($this->data); } } diff --git a/Classes/Frontend/AdditionalPreviewData.php b/Classes/Frontend/AdditionalPreviewData.php index 894a16f0..76e09148 100644 --- a/Classes/Frontend/AdditionalPreviewData.php +++ b/Classes/Frontend/AdditionalPreviewData.php @@ -9,15 +9,16 @@ use TYPO3\CMS\Core\Site\Entity\SiteLanguage; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; -use YoastSeoForTypo3\YoastSeo\Utility\YoastRequestHash; +use YoastSeoForTypo3\YoastSeo\Service\YoastRequestService; class AdditionalPreviewData implements SingletonInterface { /** @var array */ protected array $config; - public function __construct() - { + public function __construct( + protected YoastRequestService $yoastRequestService + ) { $this->config = $GLOBALS['TSFE']->tmpl->setup['config.'] ?? []; } @@ -27,7 +28,7 @@ public function __construct() public function render(array &$params, object $pObj): void { $serverParams = $GLOBALS['TYPO3_REQUEST'] ? $GLOBALS['TYPO3_REQUEST']->getServerParams() : $_SERVER; - if (!YoastRequestHash::isValid($serverParams)) { + if (!$this->yoastRequestService->isValidRequest($serverParams)) { return; } diff --git a/Classes/Frontend/AfterCacheableContentIsGeneratedListener.php b/Classes/Frontend/AfterCacheableContentIsGeneratedListener.php index a95f4a85..f5ea9bfa 100644 --- a/Classes/Frontend/AfterCacheableContentIsGeneratedListener.php +++ b/Classes/Frontend/AfterCacheableContentIsGeneratedListener.php @@ -5,14 +5,18 @@ namespace YoastSeoForTypo3\YoastSeo\Frontend; use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent; -use YoastSeoForTypo3\YoastSeo\Utility\YoastRequestHash; +use YoastSeoForTypo3\YoastSeo\Service\YoastRequestService; class AfterCacheableContentIsGeneratedListener { + public function __construct( + protected YoastRequestService $yoastRequestService + ) {} + public function __invoke(AfterCacheableContentIsGeneratedEvent $event): void { $serverParams = $GLOBALS['TYPO3_REQUEST'] ? $GLOBALS['TYPO3_REQUEST']->getServerParams() : $_SERVER; - if (YoastRequestHash::isValid($serverParams)) { + if ($this->yoastRequestService->isValidRequest($serverParams)) { $event->disableCaching(); } } diff --git a/Classes/Frontend/UsePageCache.php b/Classes/Frontend/UsePageCache.php index a678f23e..e7c6bd08 100644 --- a/Classes/Frontend/UsePageCache.php +++ b/Classes/Frontend/UsePageCache.php @@ -5,14 +5,18 @@ namespace YoastSeoForTypo3\YoastSeo\Frontend; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; -use YoastSeoForTypo3\YoastSeo\Utility\YoastRequestHash; +use YoastSeoForTypo3\YoastSeo\Service\YoastRequestService; class UsePageCache { + public function __construct( + protected YoastRequestService $yoastRequestService + ) {} + public function usePageCache(TypoScriptFrontendController $pObj, bool $usePageCache): bool { $serverParams = $GLOBALS['TYPO3_REQUEST'] ? $GLOBALS['TYPO3_REQUEST']->getServerParams() : $_SERVER; - if (YoastRequestHash::isValid($serverParams)) { + if ($this->yoastRequestService->isValidRequest($serverParams)) { return false; } return $usePageCache; diff --git a/Classes/Hooks/BackendYoastConfig.php b/Classes/Hooks/BackendYoastConfig.php index ef4cdc6a..8d93ca6b 100644 --- a/Classes/Hooks/BackendYoastConfig.php +++ b/Classes/Hooks/BackendYoastConfig.php @@ -8,7 +8,7 @@ use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Page\PageRenderer; use TYPO3\CMS\Core\Utility\GeneralUtility; -use YoastSeoForTypo3\YoastSeo\Utility\JsonConfigUtility; +use YoastSeoForTypo3\YoastSeo\Service\Javascript\JsonConfigService; class BackendYoastConfig { @@ -23,7 +23,7 @@ public function renderConfig(array &$params, PageRenderer $pObject): void return; } - $jsonConfigUtility = GeneralUtility::makeInstance(JsonConfigUtility::class); + $jsonConfigUtility = GeneralUtility::makeInstance(JsonConfigService::class); $pObject->addJsInlineCode('yoast-json-config', $jsonConfigUtility->render(), true, false, true); } } diff --git a/Classes/MetaTag/Generator/AbstractGenerator.php b/Classes/MetaTag/Generator/AbstractGenerator.php index f9bc4e28..158642a7 100644 --- a/Classes/MetaTag/Generator/AbstractGenerator.php +++ b/Classes/MetaTag/Generator/AbstractGenerator.php @@ -6,23 +6,17 @@ use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection; use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry; +use TYPO3\CMS\Core\Resource\FileInterface; use TYPO3\CMS\Core\Resource\FileReference; use TYPO3\CMS\Core\Resource\ProcessedFile; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Service\ImageService; -use YoastSeoForTypo3\YoastSeo\Record\Record; abstract class AbstractGenerator implements GeneratorInterface { - protected MetaTagManagerRegistry $managerRegistry; - - public function __construct(MetaTagManagerRegistry $managerRegistry = null) - { - if ($managerRegistry === null) { - $managerRegistry = GeneralUtility::makeInstance(MetaTagManagerRegistry::class); - } - $this->managerRegistry = $managerRegistry; - } + public function __construct( + protected MetaTagManagerRegistry $managerRegistry + ) {} /** * @see \TYPO3\CMS\Seo\MetaTag\MetaTagGenerator @@ -35,33 +29,45 @@ protected function generateSocialImages(array $fileReferences): array $socialImages = []; - foreach ($fileReferences as $file) { - $arguments = $file->getProperties(); - $cropVariantCollection = CropVariantCollection::create((string)$arguments['crop']); - $cropVariant = ($arguments['cropVariant'] ?? false) ?: 'social'; - $cropArea = $cropVariantCollection->getCropArea($cropVariant); - $crop = $cropArea->makeAbsoluteBasedOnFile($file); - - $processingConfiguration = [ - 'crop' => $crop, - 'maxWidth' => 2000, - ]; - - $processedImage = $file->getOriginalFile()->process( - ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, - $processingConfiguration - ); - - $imageUri = $imageService->getImageUri($processedImage, true); - + foreach ($fileReferences as $fileReference) { + $arguments = $fileReference->getProperties(); + $image = $this->processSocialImage($fileReference); $socialImages[] = [ - 'url' => $imageUri, - 'width' => floor($processedImage->getProperty('width')), - 'height' => floor($processedImage->getProperty('height')), + 'url' => $imageService->getImageUri($image, true), + 'width' => floor((float)$image->getProperty('width')), + 'height' => floor((float)$image->getProperty('height')), 'alternative' => $arguments['alternative'], ]; } return $socialImages; } + + protected function processSocialImage(FileReference $fileReference): FileInterface + { + $arguments = $fileReference->getProperties(); + $cropVariantCollection = CropVariantCollection::create((string)($arguments['crop'] ?? '')); + $cropVariantName = ($arguments['cropVariant'] ?? false) ?: 'social'; + $cropArea = $cropVariantCollection->getCropArea($cropVariantName); + $crop = $cropArea->makeAbsoluteBasedOnFile($fileReference); + + $processingConfiguration = [ + 'crop' => $crop, + 'maxWidth' => 2000, + ]; + + // The image needs to be processed if: + // - the image width is greater than the defined maximum width, or + // - there is a cropping other than the full image (starts at 0,0 and has a width and height of 100%) defined + $needsProcessing = $fileReference->getProperty('width') > $processingConfiguration['maxWidth'] + || !$cropArea->isEmpty(); + if (!$needsProcessing) { + return $fileReference->getOriginalFile(); + } + + return $fileReference->getOriginalFile()->process( + ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, + $processingConfiguration + ); + } } diff --git a/Classes/Middleware/PageRequestMiddleware.php b/Classes/Middleware/PageRequestMiddleware.php index 5d9d9a0e..0df2a685 100644 --- a/Classes/Middleware/PageRequestMiddleware.php +++ b/Classes/Middleware/PageRequestMiddleware.php @@ -11,13 +11,17 @@ use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\VisibilityAspect; use TYPO3\CMS\Core\Utility\GeneralUtility; -use YoastSeoForTypo3\YoastSeo\Utility\YoastRequestHash; +use YoastSeoForTypo3\YoastSeo\Service\YoastRequestService; class PageRequestMiddleware implements MiddlewareInterface { + public function __construct( + protected YoastRequestService $yoastRequestService + ) {} + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (YoastRequestHash::isValid($request->getServerParams())) { + if ($this->yoastRequestService->isValidRequest($request->getServerParams())) { $context = GeneralUtility::makeInstance(Context::class); $context->setAspect('visibility', new VisibilityAspect(true)); } diff --git a/Classes/PageTitle/RecordPageTitleProvider.php b/Classes/PageTitle/RecordPageTitleProvider.php index 14d9e878..e74d8f71 100644 --- a/Classes/PageTitle/RecordPageTitleProvider.php +++ b/Classes/PageTitle/RecordPageTitleProvider.php @@ -5,21 +5,14 @@ namespace YoastSeoForTypo3\YoastSeo\PageTitle; use TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider; -use TYPO3\CMS\Core\Utility\GeneralUtility; use YoastSeoForTypo3\YoastSeo\Record\Record; use YoastSeoForTypo3\YoastSeo\Record\RecordService; class RecordPageTitleProvider extends AbstractPageTitleProvider { - protected RecordService $recordService; - - public function __construct(RecordService $recordService = null) - { - if ($recordService === null) { - $recordService = GeneralUtility::makeInstance(RecordService::class); - } - $this->recordService = $recordService; - } + public function __construct( + protected RecordService $recordService + ) {} public function getTitle(): string { diff --git a/Classes/Record/Builder/TcaBuilder.php b/Classes/Record/Builder/TcaBuilder.php index 4724408d..a2298fec 100644 --- a/Classes/Record/Builder/TcaBuilder.php +++ b/Classes/Record/Builder/TcaBuilder.php @@ -71,7 +71,7 @@ protected function addDefaultSeoFields(): void 'twitter_description' => $GLOBALS['TCA']['pages']['columns']['twitter_description'], 'twitter_image' => $GLOBALS['TCA']['pages']['columns']['twitter_image'], 'twitter_card' => $GLOBALS['TCA']['pages']['columns']['twitter_card'], - ] + ], ]; $GLOBALS['TCA'][$this->record->getTableName()] = array_replace_recursive( $GLOBALS['TCA'][$this->record->getTableName()], @@ -111,7 +111,7 @@ protected function addSitemapFields(): void 'columns' => [ 'sitemap_changefreq' => $GLOBALS['TCA']['pages']['columns']['sitemap_changefreq'], 'sitemap_priority' => $GLOBALS['TCA']['pages']['columns']['sitemap_priority'], - ] + ], ]; $GLOBALS['TCA'][$this->record->getTableName()] = array_replace_recursive( $GLOBALS['TCA'][$this->record->getTableName()], @@ -128,7 +128,7 @@ protected function addSitemapFields(): void protected function addDescriptionField(): void { ExtensionManagementUtility::addTCAcolumns($this->record->getTableName(), [ - $this->record->getDescriptionField() => $GLOBALS['TCA']['pages']['columns']['description'] + $this->record->getDescriptionField() => $GLOBALS['TCA']['pages']['columns']['description'], ]); } diff --git a/Classes/Service/Ajax/AbstractAjaxHandler.php b/Classes/Service/Ajax/AbstractAjaxHandler.php new file mode 100644 index 00000000..233d86cf --- /dev/null +++ b/Classes/Service/Ajax/AbstractAjaxHandler.php @@ -0,0 +1,19 @@ + + */ + protected function getJsonData(ServerRequestInterface $request): array + { + $body = $request->getBody()->getContents(); + return json_decode($body, true); + } +} diff --git a/Classes/Service/Ajax/AjaxHandlerInterface.php b/Classes/Service/Ajax/AjaxHandlerInterface.php new file mode 100644 index 00000000..903d70d6 --- /dev/null +++ b/Classes/Service/Ajax/AjaxHandlerInterface.php @@ -0,0 +1,13 @@ +getAttribute('handlerRequest') === 'determine') { + return $this->determinePages($request); + } + if ($request->getAttribute('handlerRequest') === 'index') { + return $this->indexPages($request); + } + throw new BadRequestException('Invalid request'); + } + + protected function determinePages(ServerRequestInterface $request): ResponseInterface + { + $crawlerData = $this->getCrawlerRequestData($request); + $amount = $this->crawlerService->getAmountOfPages($crawlerData['site'], $crawlerData['language']); + if ($amount > 0) { + return new JsonResponse([ + 'amount' => $amount, + ]); + } + return new JsonResponse([ + 'error' => 'No pages found to analyse', + ]); + } + + protected function indexPages(ServerRequestInterface $request): ResponseInterface + { + $crawlerData = $this->getCrawlerRequestData($request); + $indexInformation = $this->crawlerService->getIndexInformation( + $crawlerData['site'], + $crawlerData['language'], + $crawlerData['offset'] + ); + if (count($indexInformation['pages']) === 0) { + return new JsonResponse(['status' => 'finished', 'total' => $indexInformation['total']]); + } + return new JsonResponse($indexInformation); + } + + /** + * @return array + */ + protected function getCrawlerRequestData(ServerRequestInterface $request): array + { + $crawlerData = $this->getJsonData($request); + if (!isset($crawlerData['site'], $crawlerData['language'])) { + die(json_encode(['error' => 'No site and language provided by request'])); + } + return [ + 'site' => (int)$crawlerData['site'], + 'language' => (int)$crawlerData['language'], + 'offset' => (int)($crawlerData['offset'] ?? 0), + ]; + } +} diff --git a/Classes/Service/Ajax/InternalLinkingSuggestionsHandler.php b/Classes/Service/Ajax/InternalLinkingSuggestionsHandler.php new file mode 100644 index 00000000..2653a4b7 --- /dev/null +++ b/Classes/Service/Ajax/InternalLinkingSuggestionsHandler.php @@ -0,0 +1,41 @@ +getJsonData($request); + + $words = $data['words'] ?? []; + $excludedPageId = (int)($data['excludedPage'] ?? 0); + $languageId = (int)($data['languageId'] ?? 0); + $content = (string)($data['content'] ?? ''); + + $links = $this->linkingSuggestionsService->getLinkingSuggestions( + $words, + $excludedPageId, + $languageId, + $content + ); + + return new JsonResponse([ + 'OK', + 'links' => $links, + 'excludedPage' => $excludedPageId, + 'languageId' => $languageId, + ]); + } +} diff --git a/Classes/Service/Ajax/PreviewHandler.php b/Classes/Service/Ajax/PreviewHandler.php new file mode 100644 index 00000000..812231e8 --- /dev/null +++ b/Classes/Service/Ajax/PreviewHandler.php @@ -0,0 +1,45 @@ +getQueryParams(); + + if (!isset($queryParams['pageId'], $queryParams['languageId'], $queryParams['additionalGetVars'])) { + $json = $this->getJsonData($request); + if (isset($json['pageId'], $json['languageId'], $json['additionalGetVars'])) { + $queryParams = $json; + } else { + return new JsonResponse([]); + } + } + + $content = $this->previewService->getPreviewData( + $this->urlService->getUriToCheck( + (int)$queryParams['pageId'], + (int)$queryParams['languageId'], + (string)$queryParams['additionalGetVars'] + ), + (int)$queryParams['pageId'] + ); + + return new HtmlResponse($content); + } +} diff --git a/Classes/Service/Ajax/ProminentWordsHandler.php b/Classes/Service/Ajax/ProminentWordsHandler.php new file mode 100644 index 00000000..1b651f3b --- /dev/null +++ b/Classes/Service/Ajax/ProminentWordsHandler.php @@ -0,0 +1,34 @@ +getJsonData($request); + + if (isset($data['words'], $data['uid'])) { + $this->prominentWordsService->saveProminentWords( + (int)$data['uid'], + isset($data['pid']) ? (int)$data['pid'] : null, + $data['table'] ?? 'pages', + (int)($data['languageId'] ?? 0), + (array)$data['words'] + ); + } + + return new JsonResponse(['OK']); + } +} diff --git a/Classes/Service/Ajax/SaveScoresHandler.php b/Classes/Service/Ajax/SaveScoresHandler.php new file mode 100644 index 00000000..4032ad57 --- /dev/null +++ b/Classes/Service/Ajax/SaveScoresHandler.php @@ -0,0 +1,53 @@ +getJsonData($request); + if (!empty($data['table']) && !empty($data['uid'])) { + $this->saveScores($data); + } + return new JsonResponse($data); + } + + /** + * @param array $data + */ + protected function saveScores(array $data): void + { + $connection = $this->connectionPool->getConnectionForTable($data['table']); + try { + $row = $connection->select( + ['*'], + $data['table'], + ['uid' => (int)$data['uid']], + [], + [], + 1 + )->fetchAssociative(); + } catch (\Throwable) { + return; + } + + if ($row !== false && isset($row['tx_yoastseo_score_readability'], $row['tx_yoastseo_score_seo'])) { + $connection->update($data['table'], [ + 'tx_yoastseo_score_readability' => (string)$data['readabilityScore'], + 'tx_yoastseo_score_seo' => (string)$data['seoScore'], + ], ['uid' => (int)$data['uid']]); + } + } +} diff --git a/Classes/Service/Crawler/CrawlerJavascriptConfigService.php b/Classes/Service/Crawler/CrawlerJavascriptConfigService.php new file mode 100644 index 00000000..99537bf0 --- /dev/null +++ b/Classes/Service/Crawler/CrawlerJavascriptConfigService.php @@ -0,0 +1,33 @@ +javascriptService->loadPluginJavascript(); + $this->jsonConfigService->addConfig([ + 'urls' => [ + 'workerUrl' => PathUtility::getPublicPathToResources() . '/JavaScript/dist/worker.js', + 'preview' => (string)$this->uriBuilder->buildUriFromRoute('ajax_yoast_preview'), + 'determinePages' => (string)$this->uriBuilder->buildUriFromRoute('ajax_yoast_crawler_determine_pages'), + 'indexPages' => (string)$this->uriBuilder->buildUriFromRoute('ajax_yoast_crawler_index_pages'), + 'prominentWords' => (string)$this->uriBuilder->buildUriFromRoute('ajax_yoast_prominent_words'), + ], + ]); + } +} diff --git a/Classes/Service/CrawlerService.php b/Classes/Service/Crawler/CrawlerService.php similarity index 98% rename from Classes/Service/CrawlerService.php rename to Classes/Service/Crawler/CrawlerService.php index eb758cb0..b39018fb 100644 --- a/Classes/Service/CrawlerService.php +++ b/Classes/Service/Crawler/CrawlerService.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace YoastSeoForTypo3\YoastSeo\Service; +namespace YoastSeoForTypo3\YoastSeo\Service\Crawler; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use TYPO3\CMS\Core\Database\ConnectionPool; diff --git a/Classes/Service/Form/NodeTemplateService.php b/Classes/Service/Form/NodeTemplateService.php new file mode 100644 index 00000000..80cb94dd --- /dev/null +++ b/Classes/Service/Form/NodeTemplateService.php @@ -0,0 +1,32 @@ + $data + */ + public function renderView(string $template, array $data = []): string + { + $templateView = $this->getStandaloneView(); + $templateView->setPartialRootPaths( + [GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Partials/TCA')] + ); + $templateView->setTemplatePathAndFilename( + GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Templates/TCA/' . $template . '.html') + ); + $templateView->assignMultiple($data); + return $templateView->render(); + } + + protected function getStandaloneView(): StandaloneView + { + return GeneralUtility::makeInstance(StandaloneView::class); + } +} diff --git a/Classes/Service/Javascript/JavascriptService.php b/Classes/Service/Javascript/JavascriptService.php new file mode 100644 index 00000000..a726f6ef --- /dev/null +++ b/Classes/Service/Javascript/JavascriptService.php @@ -0,0 +1,56 @@ +yoastEnvironmentService->isDevelopmentMode()) { + $this->pageRenderer->addHeaderData( + '' + ); + return; + } + + if ($this->isEs6()) { + $this->pageRenderer->loadJavaScriptModule( + '@yoast/yoast-seo-for-typo3/dist/plugin.js', + ); + } else { + $this->pageRenderer->loadRequireJsModule( + 'TYPO3/CMS/YoastSeo/dist/plugin', + ); + } + } + + public function loadModalJavascript(): void + { + if ($this->isEs6()) { + $this->pageRenderer->loadJavaScriptModule( + '@yoast/yoast-seo-for-typo3/yoastModalEs6.js', + ); + } else { + $this->pageRenderer->loadRequireJsModule( + 'TYPO3/CMS/YoastSeo/yoastModal', + ); + } + } + + public function isEs6(): bool + { + return GeneralUtility::makeInstance(Typo3Version::class)->getMajorVersion() >= 13; + } +} diff --git a/Classes/Utility/JsonConfigUtility.php b/Classes/Service/Javascript/JsonConfigService.php similarity index 82% rename from Classes/Utility/JsonConfigUtility.php rename to Classes/Service/Javascript/JsonConfigService.php index 861338f7..b31cdf85 100644 --- a/Classes/Utility/JsonConfigUtility.php +++ b/Classes/Service/Javascript/JsonConfigService.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace YoastSeoForTypo3\YoastSeo\Utility; +namespace YoastSeoForTypo3\YoastSeo\Service\Javascript; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\ArrayUtility; -class JsonConfigUtility implements SingletonInterface +class JsonConfigService implements SingletonInterface { /** @var array */ protected array $config = []; diff --git a/Classes/Service/LinkingSuggestionsService.php b/Classes/Service/LinkingSuggestionsService.php index 53371ac6..3bc5fd95 100644 --- a/Classes/Service/LinkingSuggestionsService.php +++ b/Classes/Service/LinkingSuggestionsService.php @@ -28,8 +28,7 @@ class LinkingSuggestionsService public function __construct( protected ConnectionPool $connectionPool, protected PageRepository $pageRepository - ) { - } + ) {} /** * @param array $words diff --git a/Classes/Service/LocaleService.php b/Classes/Service/LocaleService.php index aea5934d..3bf1cdcc 100644 --- a/Classes/Service/LocaleService.php +++ b/Classes/Service/LocaleService.php @@ -5,7 +5,10 @@ namespace YoastSeoForTypo3\YoastSeo\Service; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Localization\Locales; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; +use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Utility\GeneralUtility; use YoastSeoForTypo3\YoastSeo\Traits\LanguageServiceTrait; @@ -16,9 +19,9 @@ class LocaleService protected const APP_TRANSLATION_FILE_PATTERN = 'EXT:yoast_seo/Resources/Private/Language/wordpress-seo-%s.json'; public function __construct( - protected Locales $locales - ) { - } + protected Locales $locales, + protected SiteFinder $siteFinder + ) {} /** * @return array> @@ -57,7 +60,7 @@ public function getLabels(): array 'seo' => $this->getLanguageService()->sL($llPrefix . 'Seo'), 'bad' => $this->getLanguageService()->sL($llPrefix . 'Bad'), 'ok' => $this->getLanguageService()->sL($llPrefix . 'Ok'), - 'good' => $this->getLanguageService()->sL($llPrefix . 'Good') + 'good' => $this->getLanguageService()->sL($llPrefix . 'Good'), ]; } @@ -76,7 +79,7 @@ protected function getInterfaceLocale(): ?string $translationConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['yoast_seo']['translations'] ?? [ 'availableLocales' => [], - 'languageKeyToLocaleMapping' => [] + 'languageKeyToLocaleMapping' => [], ]; if ($GLOBALS['BE_USER'] instanceof BackendUserAuthentication @@ -120,4 +123,42 @@ protected function getInterfaceLocale(): ?string return $locale; } + + public function getLocale(int $pageId, int &$languageId): ?string + { + try { + $site = $this->siteFinder->getSiteByPageId($pageId); + if ($languageId === -1) { + $languageId = $site->getDefaultLanguage()->getLanguageId(); + return $this->getLanguageCode($site->getDefaultLanguage()); + } + return $this->getLanguageCode($site->getLanguageById($languageId)); + } catch (SiteNotFoundException|\InvalidArgumentException) { + return null; + } + } + + protected function getLanguageCode(SiteLanguage $siteLanguage): string + { + // Support for v11 + if (method_exists($siteLanguage, 'getTwoLetterIsoCode')) { + return $siteLanguage->getTwoLetterIsoCode(); + } + return $siteLanguage->getLocale()->getLanguageCode(); + } + + /** + * @param array $data + */ + public function getLanguageIdFromData(array $data): int + { + if (!isset($data['databaseRow']['sys_language_uid'])) { + return 0; + } + + if (is_array($data['databaseRow']['sys_language_uid']) && count($data['databaseRow']['sys_language_uid']) > 0) { + return (int)current($data['databaseRow']['sys_language_uid']); + } + return (int)$data['databaseRow']['sys_language_uid']; + } } diff --git a/Classes/Backend/Overview/DataProviderRequest.php b/Classes/Service/Overview/Dto/DataProviderRequest.php similarity index 88% rename from Classes/Backend/Overview/DataProviderRequest.php rename to Classes/Service/Overview/Dto/DataProviderRequest.php index 51dd4890..eb664cdc 100644 --- a/Classes/Backend/Overview/DataProviderRequest.php +++ b/Classes/Service/Overview/Dto/DataProviderRequest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace YoastSeoForTypo3\YoastSeo\Backend\Overview; +namespace YoastSeoForTypo3\YoastSeo\Service\Overview\Dto; class DataProviderRequest { diff --git a/Classes/Backend/Overview/LanguageMenu/LanguageMenuItem.php b/Classes/Service/Overview/Dto/LanguageMenuItem.php similarity index 90% rename from Classes/Backend/Overview/LanguageMenu/LanguageMenuItem.php rename to Classes/Service/Overview/Dto/LanguageMenuItem.php index 353eec9f..58262fb6 100644 --- a/Classes/Backend/Overview/LanguageMenu/LanguageMenuItem.php +++ b/Classes/Service/Overview/Dto/LanguageMenuItem.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace YoastSeoForTypo3\YoastSeo\Backend\Overview\LanguageMenu; +namespace YoastSeoForTypo3\YoastSeo\Service\Overview\Dto; class LanguageMenuItem { @@ -10,8 +10,7 @@ public function __construct( protected string $title = '', protected string $href = '', protected bool $active = false, - ) { - } + ) {} public function getTitle(): string { diff --git a/Classes/Service/Overview/Dto/OverviewData.php b/Classes/Service/Overview/Dto/OverviewData.php new file mode 100644 index 00000000..c0271d03 --- /dev/null +++ b/Classes/Service/Overview/Dto/OverviewData.php @@ -0,0 +1,136 @@ + */ + protected array $pageInformation = [], + /** @var array> */ + protected array $items = [], + protected ArrayPaginator|null $paginator = null, + protected Pagination|null $pagination = null, + /** @var array */ + protected array $filters = [], + protected OverviewDataProviderInterface|null $activeFilter = null, + protected DataProviderRequest|null $params = null, + ) {} + + /** + * @return array + */ + public function getPageInformation(): array + { + return $this->pageInformation; + } + + /** + * @param array $pageInformation + */ + public function setPageInformation(array $pageInformation): OverviewData + { + $this->pageInformation = $pageInformation; + return $this; + } + + /** + * @return array> + */ + public function getItems(): array + { + return $this->items; + } + + /** + * @param array> $items + */ + public function setItems(array $items): OverviewData + { + $this->items = $items; + return $this; + } + + public function getPaginator(): ?ArrayPaginator + { + return $this->paginator; + } + + public function setPaginator(?ArrayPaginator $paginator): OverviewData + { + $this->paginator = $paginator; + return $this; + } + + public function getPagination(): ?Pagination + { + return $this->pagination; + } + + public function setPagination(?Pagination $pagination): OverviewData + { + $this->pagination = $pagination; + return $this; + } + + /** + * @return array + */ + public function getFilters(): array + { + return $this->filters; + } + + /** + * @param array|null $filters + */ + public function setFilters(array|null $filters): OverviewData + { + $this->filters = $filters ?? []; + return $this; + } + + public function getActiveFilter(): ?OverviewDataProviderInterface + { + return $this->activeFilter; + } + + public function setActiveFilter(?OverviewDataProviderInterface $activeFilter): OverviewData + { + $this->activeFilter = $activeFilter; + return $this; + } + + public function getParams(): ?DataProviderRequest + { + return $this->params; + } + + public function setParams(?DataProviderRequest $params): OverviewData + { + $this->params = $params; + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'pageInformation' => $this->pageInformation, + 'items' => $this->items, + 'paginator' => $this->paginator, + 'pagination' => $this->pagination, + 'filters' => $this->filters, + 'activeFilter' => $this->activeFilter, + 'params' => $this->params, + ]; + } +} diff --git a/Classes/Backend/Overview/LanguageMenu/LanguageMenuFactory.php b/Classes/Service/Overview/LanguageMenu/LanguageMenuFactory.php similarity index 94% rename from Classes/Backend/Overview/LanguageMenu/LanguageMenuFactory.php rename to Classes/Service/Overview/LanguageMenu/LanguageMenuFactory.php index cd6937be..77e34c03 100644 --- a/Classes/Backend/Overview/LanguageMenu/LanguageMenuFactory.php +++ b/Classes/Service/Overview/LanguageMenu/LanguageMenuFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace YoastSeoForTypo3\YoastSeo\Backend\Overview\LanguageMenu; +namespace YoastSeoForTypo3\YoastSeo\Service\Overview\LanguageMenu; use TYPO3\CMS\Backend\Template\Components\Menu\Menu; use TYPO3\CMS\Backend\Template\ModuleTemplate; @@ -11,12 +11,14 @@ use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Extbase\Mvc\RequestInterface; use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder; +use YoastSeoForTypo3\YoastSeo\Service\Overview\Dto\LanguageMenuItem; use YoastSeoForTypo3\YoastSeo\Traits\BackendUserTrait; use YoastSeoForTypo3\YoastSeo\Traits\LanguageServiceTrait; class LanguageMenuFactory { - use BackendUserTrait, LanguageServiceTrait; + use BackendUserTrait; + use LanguageServiceTrait; protected RequestInterface $request; protected ModuleTemplate $moduleTemplate; @@ -25,8 +27,7 @@ class LanguageMenuFactory public function __construct( protected SiteFinder $siteFinder, protected UriBuilder $uriBuilder - ) { - } + ) {} public function create( RequestInterface $request, diff --git a/Classes/Service/Overview/OverviewService.php b/Classes/Service/Overview/OverviewService.php new file mode 100644 index 00000000..40500d6a --- /dev/null +++ b/Classes/Service/Overview/OverviewService.php @@ -0,0 +1,121 @@ + */ + protected array $filters; + + /** + * @param iterable $filters + */ + public function __construct( + protected PaginationService $overviewPaginationService, + iterable $filters + ) { + foreach ($filters as $key => $dataProvider) { + $this->addFilter($key, $dataProvider); + } + } + + public function getOverviewData(RequestInterface $request, int $currentPage, int $itemsPerPage): OverviewData + { + $overviewData = new OverviewData($this->getPageInformation($request)); + if (empty($overviewData->getPageInformation())) { + return $overviewData; + } + + $overviewData->setFilters($this->getAvailableFilters($request)); + if (empty($overviewData->getFilters())) { + return $overviewData; + } + + $activeFilter = $this->getActiveFilter($request); + $items = $activeFilter->process(); + + $arrayPaginator = $this->overviewPaginationService->getArrayPaginator($items, $currentPage, $itemsPerPage); + + return $overviewData->setItems($items) + ->setPaginator($arrayPaginator) + ->setPagination($this->overviewPaginationService->getPagination($arrayPaginator)) + ->setActiveFilter($activeFilter) + ->setParams($this->getDataProviderRequest($request)); + } + + /** + * @return array|null + */ + public function getAvailableFilters(RequestInterface $request): ?array + { + if ($this->filters === []) { + return null; + } + + $dataProviderRequest = $this->getDataProviderRequest($request); + foreach ($this->filters as $dataProvider) { + $dataProvider->initialize($dataProviderRequest); + } + + return $this->filters; + } + + protected function getActiveFilter(RequestInterface $request): OverviewDataProviderInterface + { + if ($this->filters === []) { + throw new \RuntimeException('No filters available'); + } + + if ($request->hasArgument('filter')) { + $activeFilter = $request->getArgument('filter'); + if (is_string($activeFilter) && isset($this->filters[$activeFilter])) { + return $this->filters[$activeFilter]; + } + } + + return current($this->filters); + } + + protected function getDataProviderRequest(RequestInterface $request): DataProviderRequest + { + return new DataProviderRequest( + (int)($request->getQueryParams()['id'] ?? 0), + (int)($request->getQueryParams()['tx_yoastseo_yoast_yoastseooverview']['language'] ?? $request->getQueryParams()['language'] ?? 0), + 'pages' + ); + } + + /** + * @return array + */ + protected function getPageInformation(RequestInterface $request): array + { + $id = (int)($request->getQueryParams()['id'] ?? 0); + if ($id === 0) { + return []; + } + $pageInformation = BackendUtility::readPageAccess( + $id, + $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW) + ); + return is_array($pageInformation) ? $pageInformation : []; + } + + protected function addFilter(string $key, OverviewDataProviderInterface $dataProvider): void + { + $this->filters[$key] = $dataProvider; + } +} diff --git a/Classes/Pagination/Pagination.php b/Classes/Service/Overview/Pagination/Pagination.php similarity index 97% rename from Classes/Pagination/Pagination.php rename to Classes/Service/Overview/Pagination/Pagination.php index b00ca0ee..e92fbaf7 100644 --- a/Classes/Pagination/Pagination.php +++ b/Classes/Service/Overview/Pagination/Pagination.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace YoastSeoForTypo3\YoastSeo\Pagination; +namespace YoastSeoForTypo3\YoastSeo\Service\Overview\Pagination; use TYPO3\CMS\Core\Pagination\ArrayPaginator; @@ -38,7 +38,7 @@ public function getPreviousPageNumber(): ?int return $previousPage >= $this->getFirstPageNumber() ? $previousPage : null - ; + ; } public function getNextPageNumber(): ?int @@ -48,7 +48,7 @@ public function getNextPageNumber(): ?int return $nextPage <= $this->paginator->getNumberOfPages() ? $nextPage : null - ; + ; } public function getFirstPageNumber(): int diff --git a/Classes/Service/Overview/Pagination/PaginationService.php b/Classes/Service/Overview/Pagination/PaginationService.php new file mode 100644 index 00000000..c3caddb5 --- /dev/null +++ b/Classes/Service/Overview/Pagination/PaginationService.php @@ -0,0 +1,32 @@ +> $items + */ + public function getArrayPaginator( + array $items, + int $currentPage, + int $itemsPerPage, + ): ArrayPaginator { + return GeneralUtility::makeInstance( + ArrayPaginator::class, + $items, + $currentPage, + $itemsPerPage + ); + } + + public function getPagination(ArrayPaginator $arrayPaginator): Pagination + { + return GeneralUtility::makeInstance(Pagination::class, $arrayPaginator); + } +} diff --git a/Classes/Service/PageLayoutHeader/PageDataService.php b/Classes/Service/PageLayoutHeader/PageDataService.php new file mode 100644 index 00000000..f0c88e09 --- /dev/null +++ b/Classes/Service/PageLayoutHeader/PageDataService.php @@ -0,0 +1,59 @@ +|null + */ + public function getCurrentPage(int $pageId, int $languageId, PageLayoutController|ModuleTemplate|null $parentObj): ?array + { + if ((!$parentObj instanceof PageLayoutController && !$parentObj instanceof ModuleTemplate) || $pageId <= 0) { + return null; + } + + if ($languageId === 0) { + return $this->getPageRecord($pageId); + } + + if ($languageId > 0) { + $overlayRecords = $this->getOverlayRecords($pageId, $languageId); + + if (is_array($overlayRecords[0] ?? false)) { + return $overlayRecords[0]; + } + } + + return null; + } + + /** + * @return array + */ + protected function getPageRecord(int $pageId): array + { + return BackendUtility::getRecord( + 'pages', + $pageId + ) ?? []; + } + + /** + * @return array|bool + */ + protected function getOverlayRecords(int $pageId, int $languageId): array|bool + { + return BackendUtility::getRecordLocalization( + 'pages', + $pageId, + $languageId + ); + } +} diff --git a/Classes/Service/PageLayoutHeader/PageLayoutHeaderRenderer.php b/Classes/Service/PageLayoutHeader/PageLayoutHeaderRenderer.php new file mode 100644 index 00000000..e66bb494 --- /dev/null +++ b/Classes/Service/PageLayoutHeader/PageLayoutHeaderRenderer.php @@ -0,0 +1,28 @@ +getStandaloneView(); + $templateView->setTemplatePathAndFilename( + GeneralUtility::getFileAbsFileName('EXT:yoast_seo/Resources/Private/Templates/PageLayout/Header.html') + ); + $templateView->assignMultiple([ + 'targetElementId' => uniqid('_YoastSEO_panel_'), + ]); + return $templateView->render(); + } + + protected function getStandaloneView(): StandaloneView + { + return GeneralUtility::makeInstance(StandaloneView::class); + } +} diff --git a/Classes/Service/PageLayoutHeader/VisibilityChecker.php b/Classes/Service/PageLayoutHeader/VisibilityChecker.php new file mode 100644 index 00000000..d5d109a4 --- /dev/null +++ b/Classes/Service/PageLayoutHeader/VisibilityChecker.php @@ -0,0 +1,60 @@ + $pageRecord + */ + public function shouldShowPreview(int $pageId, array $pageRecord): bool + { + if (!$this->isSnippetPreviewEnabled($pageId, $pageRecord)) { + return false; + } + + $allowedDoktypes = YoastUtility::getAllowedDoktypes(); + return isset($pageRecord['doktype']) && in_array((int)$pageRecord['doktype'], $allowedDoktypes, true); + } + + /** + * @param array $pageRecord + */ + protected function isSnippetPreviewEnabled(int $pageId, array $pageRecord): bool + { + $backendUser = $this->getBackendUser(); + + if (!$GLOBALS['BE_USER']->check('non_exclude_fields', 'pages:tx_yoastseo_snippetpreview')) { + return false; + } + + if ((bool)($backendUser->uc['hideYoastInPageModule'] ?? false)) { + return false; + } + + $pageTsConfig = $this->getPageTsConfig($pageId); + if (isset($pageTsConfig['mod.']['web_SeoPlugin.']['disableSnippetPreview']) + && (int)$pageTsConfig['mod.']['web_SeoPlugin.']['disableSnippetPreview'] === 1 + ) { + return false; + } + + return !((bool)($pageRecord['tx_yoastseo_hide_snippet_preview'] ?? false)); + } + + /** + * @return array + */ + protected function getPageTsConfig(int $pageId): array + { + return BackendUtility::getPagesTSconfig($pageId); + } +} diff --git a/Classes/Service/Preview/ContentParser.php b/Classes/Service/Preview/ContentParser.php new file mode 100644 index 00000000..72093063 --- /dev/null +++ b/Classes/Service/Preview/ContentParser.php @@ -0,0 +1,159 @@ + + */ + public function parse(string $content, string $uriToCheck, int $pageId): array + { + $urlParts = parse_url((string)preg_replace('/\/$/', '', $uriToCheck)); + $baseUrl = $this->getBaseUrl($urlParts); + $url = $baseUrl . ($urlParts['path'] ?? ''); + + $titleConfiguration = $this->getTitleConfiguration($content); + + return [ + 'id' => $pageId, + 'url' => $url, + 'baseUrl' => $baseUrl, + 'slug' => '/', + 'title' => $this->getTitle($content), + 'description' => $this->getDescription($content), + 'locale' => $this->getLocale($content), + 'body' => $this->getBody($content), + 'faviconSrc' => $this->getFaviconSrc($baseUrl, $content), + 'pageTitlePrepend' => $titleConfiguration['titlePrepend'], + 'pageTitleAppend' => $titleConfiguration['titleAppend'], + ]; + } + + protected function getBaseUrl(mixed $urlParts): string + { + if (!is_array($urlParts)) { + return '://'; + } + if ($urlParts['port'] ?? false) { + return (isset($urlParts['scheme']) ? $urlParts['scheme'] . ':' : '') . '//' . ($urlParts['host'] ?? '') . ':' . $urlParts['port']; + } + return (isset($urlParts['scheme']) ? $urlParts['scheme'] . ':' : '') . '//' . ($urlParts['host'] ?? ''); + } + + protected function getTitle(string $content): string + { + $title = ''; + $titleFound = preg_match("/]*>(.*?)<\/title>/is", $content, $matchesTitle); + + if ($titleFound) { + $title = $matchesTitle[1]; + } + + return strip_tags(html_entity_decode($title)); + } + + protected function getDescription(string $content): string + { + $metaDescription = ''; + $descriptionFound = preg_match( + "/]*name=[\" | \']description[\"|\'][^>]*content=[\"]([^\"]*)[\"][^>]*>/i", + $content, + $matchesDescription + ); + + if ($descriptionFound) { + $metaDescription = $matchesDescription[1]; + } + + return strip_tags(html_entity_decode($metaDescription)); + } + + protected function getLocale(string $content): string + { + $locale = 'en'; + $localeFound = preg_match('/]*lang="([a-z\-A-Z]*)"/is', $content, $matchesLocale); + + if ($localeFound) { + [$locale] = explode('-', trim($matchesLocale[1])); + } + + return $locale; + } + + protected function getBody(string $content): string + { + $body = ''; + + $bodyFound = preg_match("/]*>(.*)<\/body>/is", $content, $matchesBody); + + if ($bodyFound) { + $body = $matchesBody[1]; + + preg_match_all( + '/.*?/mis', + $body, + $indexableContents + ); + + if (is_array($indexableContents[0]) && !empty($indexableContents[0])) { + $body = implode('', $indexableContents[0]); + } + } + + return $this->prepareBody($body); + } + + protected function prepareBody(string $body): string + { + $body = $this->stripTagsContent($body, '' - ); - } - } -} diff --git a/Classes/Utility/YoastUtility.php b/Classes/Utility/YoastUtility.php index 97a7edae..5e81e4c1 100644 --- a/Classes/Utility/YoastUtility.php +++ b/Classes/Utility/YoastUtility.php @@ -5,11 +5,8 @@ namespace YoastSeoForTypo3\YoastSeo\Utility; use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Configuration\ConfigurationManager; -use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; class YoastUtility { @@ -46,34 +43,6 @@ public static function getAllowedDoktypesList(?array $configuration = null): str return implode(',', self::getAllowedDoktypes($configuration)); } - /** - * @param array $pageRecord - * @param array $pageTs - */ - public static function snippetPreviewEnabled(int $pageId, array $pageRecord, ?array $pageTs = null): bool - { - if (!$GLOBALS['BE_USER'] instanceof BackendUserAuthentication || - !$GLOBALS['BE_USER']->check('non_exclude_fields', 'pages:tx_yoastseo_snippetpreview')) { - return false; - } - - if ((bool)($GLOBALS['BE_USER']->uc['hideYoastInPageModule'] ?? false)) { - return false; - } - - if ($pageTs === null) { - $pageTs = BackendUtility::getPagesTSconfig($pageId); - } - - if (isset($pageTs['mod.']['web_SeoPlugin.']['disableSnippetPreview']) - && (int)$pageTs['mod.']['web_SeoPlugin.']['disableSnippetPreview'] === 1 - ) { - return false; - } - - return !$pageRecord['tx_yoastseo_hide_snippet_preview']; - } - public static function getFocusKeywordOfRecord(int $uid, string $table = 'pages'): ?string { $focusKeyword = ''; @@ -109,43 +78,13 @@ public static function getRelatedKeyphrases(string $parentTable, int $parentId): foreach ($relatedKeyphrases as $relatedKeyphrase) { $config['rk' . (int)$relatedKeyphrase['uid']] = [ 'keyword' => (string)$relatedKeyphrase['keyword'], - 'synonyms' => (string)$relatedKeyphrase['synonyms'] + 'synonyms' => (string)$relatedKeyphrase['synonyms'], ]; } return $config; } - /** - * Returns true if Yoast extension is in production mode. You need a webpack dev server running to load - * JS files if not in production mode - * - * You can set development by using TypoScript "module.tx_yoastseo.settings.developmentMode = 1" - * - * @param array|null $configuration - * @return bool - */ - public static function inProductionMode(?array $configuration = null): bool - { - if ($configuration === null) { - $configuration = self::getTypoScriptConfiguration(); - } - - return !((int)($_ENV['YOAST_DEVELOPMENT_MODE'] ?? 0) === 1 || (int)($configuration['developmentMode'] ?? 0) === 1); - } - - /** - * @return array - */ - protected static function getTypoScriptConfiguration(): array - { - $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class); - return $configurationManager->getConfiguration( - ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, - 'yoastseo' - ); - } - /** * Fix absolute url when site configuration has '/' as base * diff --git a/Classes/ViewHelpers/CrawlerProgressViewHelper.php b/Classes/ViewHelpers/CrawlerProgressViewHelper.php index 22aae60d..c8c90b3c 100644 --- a/Classes/ViewHelpers/CrawlerProgressViewHelper.php +++ b/Classes/ViewHelpers/CrawlerProgressViewHelper.php @@ -5,7 +5,7 @@ namespace YoastSeoForTypo3\YoastSeo\ViewHelpers; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; -use YoastSeoForTypo3\YoastSeo\Service\CrawlerService; +use YoastSeoForTypo3\YoastSeo\Service\Crawler\CrawlerService; class CrawlerProgressViewHelper extends AbstractViewHelper { diff --git a/Classes/ViewHelpers/ModuleLayout/MetaInformationViewHelper.php b/Classes/ViewHelpers/ModuleLayout/MetaInformationViewHelper.php deleted file mode 100644 index 6bb67bd0..00000000 --- a/Classes/ViewHelpers/ModuleLayout/MetaInformationViewHelper.php +++ /dev/null @@ -1,36 +0,0 @@ -registerArgument('pageInformation', 'array', 'Page information', true); - } - - /** - * @param array $arguments - */ - public static function renderStatic( - array $arguments, - \Closure $renderChildrenClosure, - RenderingContextInterface $renderingContext - ): void { - $viewHelperVariableContainer = $renderingContext->getViewHelperVariableContainer(); - - /** @var \TYPO3\CMS\Backend\Template\ModuleTemplate $moduleTemplate */ - $moduleTemplate = $viewHelperVariableContainer->get(ModuleLayoutViewHelper::class, ModuleTemplate::class); - $moduleTemplate->getDocHeaderComponent()->setMetaInformation($arguments['pageInformation']); - } -} diff --git a/Classes/ViewHelpers/RecordIconViewHelper.php b/Classes/ViewHelpers/RecordIconViewHelper.php deleted file mode 100644 index 80d00db7..00000000 --- a/Classes/ViewHelpers/RecordIconViewHelper.php +++ /dev/null @@ -1,37 +0,0 @@ -registerArgument('table', 'string', '', true); - $this->registerArgument('row', 'array', '', true, []); - $this->registerArgument('size', 'string', '', false, Icon::SIZE_DEFAULT); - } - - /** - * @param array $arguments - */ - public static function renderStatic( - array $arguments, - \Closure $renderChildrenClosure, - RenderingContextInterface $renderingContext - ): string { - $iconFactory = GeneralUtility::makeInstance(IconFactory::class); - $icon = $iconFactory->getIconForRecord($arguments['table'], $arguments['row'], $arguments['size']); - - return $icon->render(); - } -} diff --git a/Classes/ViewHelpers/RecordLinksViewHelper.php b/Classes/ViewHelpers/RecordLinksViewHelper.php index 7ee676b5..cf8f9725 100644 --- a/Classes/ViewHelpers/RecordLinksViewHelper.php +++ b/Classes/ViewHelpers/RecordLinksViewHelper.php @@ -38,10 +38,10 @@ public static function renderStatic( $urlParameters = [ 'edit' => [ $arguments['table'] => [ - $arguments['uid'] => $arguments['command'] - ] + $arguments['uid'] => $arguments['command'], + ], ], - 'returnUrl' => (string)$returnUri + 'returnUrl' => (string)$returnUri, ]; $module = 'record_edit'; break; diff --git a/Classes/Widgets/Provider/OrphanedContentDataProvider.php b/Classes/Widgets/Provider/OrphanedContentDataProvider.php index a0e95ae2..af44e5bf 100644 --- a/Classes/Widgets/Provider/OrphanedContentDataProvider.php +++ b/Classes/Widgets/Provider/OrphanedContentDataProvider.php @@ -35,9 +35,9 @@ public function getPages(): array 'field', $qb->createNamedParameter([ 'l10n_parent', - 'db_mountpoints' + 'db_mountpoints', ], Connection::PARAM_STR_ARRAY) - ) + ), ]; $refs = $qb->select('ref_uid') diff --git a/Classes/Widgets/Provider/PagesWithoutDescriptionDataProvider.php b/Classes/Widgets/Provider/PagesWithoutDescriptionDataProvider.php index 24dfc07e..ba8280f2 100644 --- a/Classes/Widgets/Provider/PagesWithoutDescriptionDataProvider.php +++ b/Classes/Widgets/Provider/PagesWithoutDescriptionDataProvider.php @@ -4,7 +4,6 @@ namespace YoastSeoForTypo3\YoastSeo\Widgets\Provider; -use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\Utility\GeneralUtility; diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php index f6959e2e..75c0d54c 100644 --- a/Configuration/Backend/AjaxRoutes.php +++ b/Configuration/Backend/AjaxRoutes.php @@ -5,19 +5,19 @@ return [ 'yoast_preview' => [ 'path' => 'yoast/preview', - 'target' => AjaxController::class . '::previewAction' + 'target' => AjaxController::class . '::previewAction', ], 'yoast_save_scores' => [ 'path' => 'yoast/savescores', - 'target' => AjaxController::class . '::saveScoresAction' + 'target' => AjaxController::class . '::saveScoresAction', ], 'yoast_prominent_words' => [ 'path' => 'yoast/prominentwords', - 'target' => AjaxController::class . '::promimentWordsAction' + 'target' => AjaxController::class . '::promimentWordsAction', ], 'yoast_internal_linking_suggestions' => [ 'path' => 'yoast/internallinkingsuggestions', - 'target' => AjaxController::class . '::internalLinkingSuggestionsAction' + 'target' => AjaxController::class . '::internalLinkingSuggestionsAction', ], 'yoast_crawler_determine_pages' => [ 'path' => 'yoast/crawlerdeterminepages', @@ -25,6 +25,6 @@ ], 'yoast_crawler_index_pages' => [ 'path' => 'yoast/crawlerindexpages', - 'target' => AjaxController::class . '::crawlerIndexPages' - ] + 'target' => AjaxController::class . '::crawlerIndexPages', + ], ]; diff --git a/Configuration/Backend/Modules.php b/Configuration/Backend/Modules.php index b7563793..c746deef 100644 --- a/Configuration/Backend/Modules.php +++ b/Configuration/Backend/Modules.php @@ -18,7 +18,7 @@ 'labels' => 'LLL:EXT:yoast_seo/Resources/Private/Language/BackendModuleDashboard.xlf', 'extensionName' => 'YoastSeo', 'controllerActions' => [ - DashboardController::class => ['index'] + DashboardController::class => ['index'], ], ], 'yoast_YoastSeoOverview' => [ @@ -30,7 +30,7 @@ 'navigationComponent' => '@typo3/backend/page-tree/page-tree-element', 'extensionName' => 'YoastSeo', 'controllerActions' => [ - OverviewController::class => ['list'] + OverviewController::class => ['list'], ], ], 'yoast_YoastSeoCrawler' => [ @@ -41,7 +41,7 @@ 'labels' => 'LLL:EXT:yoast_seo/Resources/Private/Language/BackendModuleCrawler.xlf', 'extensionName' => 'YoastSeo', 'controllerActions' => [ - CrawlerController::class => ['index', 'resetProgress'] + CrawlerController::class => ['index', 'resetProgress'], ], - ] + ], ]; diff --git a/Configuration/Icons.php b/Configuration/Icons.php index d4ac09fe..b181f9c4 100644 --- a/Configuration/Icons.php +++ b/Configuration/Icons.php @@ -7,22 +7,22 @@ return [ 'extension-yoast' => [ 'provider' => SvgIconProvider::class, - 'source' => 'EXT:yoast_seo/Resources/Public/Icons/Extension.svg' + 'source' => 'EXT:yoast_seo/Resources/Public/Icons/Extension.svg', ], 'module-yoast' => [ 'provider' => SvgIconProvider::class, - 'source' => 'EXT:yoast_seo/Resources/Public/Images/Yoast-module-container.svg' + 'source' => 'EXT:yoast_seo/Resources/Public/Images/Yoast-module-container.svg', ], 'module-yoast-dashboard' => [ 'provider' => SvgIconProvider::class, - 'source' => 'EXT:yoast_seo/Resources/Public/Images/Yoast-module-dashboard.svg' + 'source' => 'EXT:yoast_seo/Resources/Public/Images/Yoast-module-dashboard.svg', ], 'module-yoast-overview' => [ 'provider' => SvgIconProvider::class, - 'source' => 'EXT:yoast_seo/Resources/Public/Images/Yoast-module-overview.svg' + 'source' => 'EXT:yoast_seo/Resources/Public/Images/Yoast-module-overview.svg', ], 'module-yoast-crawler' => [ 'provider' => SvgIconProvider::class, - 'source' => 'EXT:yoast_seo/Resources/Public/Images/Yoast-module-crawler.svg' - ] + 'source' => 'EXT:yoast_seo/Resources/Public/Images/Yoast-module-crawler.svg', + ], ]; diff --git a/Configuration/RequestMiddlewares.php b/Configuration/RequestMiddlewares.php index b8e49362..9bef0861 100644 --- a/Configuration/RequestMiddlewares.php +++ b/Configuration/RequestMiddlewares.php @@ -7,11 +7,11 @@ 'yoast-seo-page-request' => [ 'target' => PageRequestMiddleware::class, 'before' => [ - 'typo3/cms-frontend/tsfe' + 'typo3/cms-frontend/tsfe', ], 'after' => [ - 'typo3/cms-frontend/eid' - ] - ] - ] + 'typo3/cms-frontend/eid', + ], + ], + ], ]; diff --git a/Configuration/Services.php b/Configuration/Services.php index 07044ebd..b426c934 100644 --- a/Configuration/Services.php +++ b/Configuration/Services.php @@ -34,7 +34,7 @@ 'description' => 'LLL:EXT:yoast_seo/Resources/Private/Language/BackendModule.xlf:dashboard.widget.orphanedContent.description', 'iconIdentifier' => 'extension-yoast', 'height' => 'large', - 'width' => 'medium' + 'width' => 'medium', ] ); @@ -59,7 +59,7 @@ 'description' => 'LLL:EXT:yoast_seo/Resources/Private/Language/BackendModule.xlf:dashboard.widget.pagesWithoutMetaDescription.description', 'iconIdentifier' => 'extension-yoast', 'height' => 'large', - 'width' => 'medium' + 'width' => 'medium', ] ); }; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 3471e743..fc7db74a 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -19,18 +19,21 @@ services: YoastSeoForTypo3\YoastSeo\Controller\AjaxController: public: true - YoastSeoForTypo3\YoastSeo\Controller\OverviewController: + YoastSeoForTypo3\YoastSeo\Service\Overview\OverviewService: arguments: $filters: - cornerstore: '@YoastSeoForTypo3\YoastSeo\DataProviders\CornerstoneOverviewDataProvider' + cornerstone: '@YoastSeoForTypo3\YoastSeo\DataProviders\CornerstoneOverviewDataProvider' withoutDescription: '@YoastSeoForTypo3\YoastSeo\DataProviders\PagesWithoutDescriptionOverviewDataProvider' orphaned: '@YoastSeoForTypo3\YoastSeo\DataProviders\OrphanedContentDataProvider' - YoastSeoForTypo3\YoastSeo\Service\CrawlerService: + YoastSeoForTypo3\YoastSeo\Service\Crawler\CrawlerService: public: true arguments: $cache: '@cache.pages' + YoastSeoForTypo3\YoastSeo\Service\SnippetPreview\SnippetPreviewUrlGenerator: + public: true + YoastSeoForTypo3\YoastSeo\StructuredData\StructuredDataProviderManager: public: true arguments: @@ -72,6 +75,9 @@ services: YoastSeoForTypo3\YoastSeo\MetaTag\AdvancedRobotsGenerator: public: true + YoastSeoForTypo3\YoastSeo\Frontend\UsePageCache: + public: true + YoastSeoForTypo3\YoastSeo\Backend\ModifyPageLayoutContentListener: tags: - name: event.listener diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php index ee5f7fe4..aec08652 100644 --- a/Configuration/TCA/Overrides/tt_content.php +++ b/Configuration/TCA/Overrides/tt_content.php @@ -12,9 +12,9 @@ 'exclude' => true, 'config' => [ 'type' => 'none', - 'renderType' => 'internalLinkingSuggestion' - ] - ] + 'renderType' => 'internalLinkingSuggestion', + ], + ], ] ); foreach ($GLOBALS['TCA']['tt_content']['types'] as $type => $config) { diff --git a/Configuration/TCA/tx_yoastseo_prominent_word.php b/Configuration/TCA/tx_yoastseo_prominent_word.php index f8de69f8..32ee5572 100644 --- a/Configuration/TCA/tx_yoastseo_prominent_word.php +++ b/Configuration/TCA/tx_yoastseo_prominent_word.php @@ -8,7 +8,7 @@ 'label' => 'stem', 'languageField' => 'sys_language_uid', 'iconfile' => 'EXT:yoast_seo/Resources/Public/Icons/Extension.svg', - 'hideTable' => true + 'hideTable' => true, ], 'columns' => [ 'stem' => [ @@ -17,20 +17,20 @@ 'type' => 'input', 'size' => 30, 'eval' => 'required', - ] + ], ], 'table' => [ 'label' => $llPrefix . 'tx_yoastseo_prominent_word.fields.table', 'config' => [ 'type' => 'input', 'size' => 30, - ] + ], ], 'weight' => [ 'label' => $llPrefix . 'tx_yoastseo_prominent_word.fields.weight', 'config' => [ 'type' => 'input', - ] + ], ], ], 'palettes' => [ @@ -38,15 +38,15 @@ 'showitem' => ' --linebreak--, stem, --linebreak--, table, - --linebreak--, weight' - ] + --linebreak--, weight', + ], ], 'types' => [ '0' => [ 'showitem' => ' --div--;General, --palette--;;yoast-prominentword, --div--;Visibility, sys_language_uid - ' - ] + ', + ], ], ]; diff --git a/Configuration/TCA/tx_yoastseo_related_focuskeyword.php b/Configuration/TCA/tx_yoastseo_related_focuskeyword.php index 00f0b264..a47dec43 100644 --- a/Configuration/TCA/tx_yoastseo_related_focuskeyword.php +++ b/Configuration/TCA/tx_yoastseo_related_focuskeyword.php @@ -37,7 +37,7 @@ 'size' => 30, 'eval' => 'required', 'required' => true, - ] + ], ], 'synonyms' => [ 'exclude' => 1, @@ -46,7 +46,7 @@ 'config' => [ 'type' => 'input', 'size' => 30, - ] + ], ], 'analysis' => [ 'exclude' => 1, @@ -55,7 +55,7 @@ 'config' => [ 'type' => 'none', 'renderType' => 'focusKeywordAnalysis', - ] + ], ], 'uid_foreign' => [ 'config' => [ @@ -73,16 +73,16 @@ 'showitem' => ' --linebreak--, keyword, --linebreak--, synonyms, - --linebreak--, analysis' - ] + --linebreak--, analysis', + ], ], 'types' => [ '0' => [ 'showitem' => ' --div--;General, --palette--;;yoast-focuskeyword, --div--;Visibility, sys_language_uid, l10n_parent,l10n_diffsource, uid_foreign, tablenames, hidden - ' - ] + ', + ], ], ]; @@ -95,7 +95,7 @@ 'config' => [ 'foreign_table' => 'tx_yoastseo_related_focuskeyword', 'foreign_table_where' => 'AND tx_yoastseo_related_focuskeyword.pid=###CURRENT_PID### AND tx_yoastseo_related_focuskeyword.sys_language_uid IN (-1,0)', - ] + ], ]), 'l10n_source' => $GLOBALS['TCA']['tt_content']['columns']['l10n_source'], 'l10n_diffsource' => $GLOBALS['TCA']['tt_content']['columns']['l18n_diffsource'], diff --git a/Resources/Private/Partials/Overview/View.html b/Resources/Private/Partials/Overview/View.html index 4bd6fbf0..5d1fdbba 100644 --- a/Resources/Private/Partials/Overview/View.html +++ b/Resources/Private/Partials/Overview/View.html @@ -5,7 +5,7 @@ - +

@@ -84,7 +84,7 @@

- + {item.title} [{item.uid}] {item.seo_title}{item.title} diff --git a/Tests/Functional/Fixtures/be_users.csv b/Tests/Functional/Fixtures/be_users.csv new file mode 100644 index 00000000..a8a129e2 --- /dev/null +++ b/Tests/Functional/Fixtures/be_users.csv @@ -0,0 +1,4 @@ +"be_users" +,"uid","pid","tstamp","username","password","admin","disable","starttime","endtime","options","crdate","workspace_perms","deleted","TSconfig","lastlogin","workspace_id" +# The password is "password" +,1,0,1366642540,"admin","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,0,0,0,0,1366642540,1,0,,1371033743,0 diff --git a/Tests/Functional/Utility/YoastUtilityTest.php b/Tests/Functional/Utility/YoastUtilityTest.php new file mode 100644 index 00000000..7c214377 --- /dev/null +++ b/Tests/Functional/Utility/YoastUtilityTest.php @@ -0,0 +1,171 @@ + [ + 'yoast_seo' => [ + 'allowedDoktypes' => self::DOKTYPES_FROM_CONFIGURATION, + ], + ], + ]; + + public function setUp(): void + { + parent::setUp(); + + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $this->setUpBackendUser(1); + Bootstrap::initializeLanguageObject(); + } + + #[Test] + public function getAllowedDoktypesReturnsAllowedDoktypes(): void + { + $allowedDoktypes = YoastUtility::getAllowedDoktypes(); + + self::assertSame(self::DOKTYPES_FROM_CONFIGURATION, $allowedDoktypes); + } + + #[DataProvider('areTheRightDoktypesExtractedFromConfigurationDataProvider')] + #[Test] + public function areTheRightDoktypesExtractedFromConfiguration(array $inputArray, array $expected): void + { + $actual = YoastUtility::getAllowedDoktypes($inputArray); + + self::assertEquals($expected, $actual); + } + + #[DataProvider('isSnippetPreviewEnabledCorrectlyBasedOnPageTsConfigurationDataProvider')] + #[Test] + public function isSnippetPreviewEnabledCorrectlyBasedOnPageTsConfiguration(int $pageId, array $config, bool $expected): void + { + $actual = YoastUtility::snippetPreviewEnabled($pageId, ['tx_yoastseo_hide_snippet_preview' => false], $config); + + self::assertEquals($expected, $actual); + } + + #[DataProvider('isSnippetPreviewEnabledCorrectlyBasedOnPageRecordDataProvider')] + #[Test] + public function isSnippetPreviewEnabledCorrectlyBasedOnPageRecord(int $pageId, array $pageRecord, bool $expected): void + { + $actual = YoastUtility::snippetPreviewEnabled($pageId, $pageRecord, []); + + self::assertEquals($expected, $actual); + } + + public static function areTheRightDoktypesExtractedFromConfigurationDataProvider(): array + { + return [ + [ + [], + self::DOKTYPES_FROM_CONFIGURATION, + ], + [ + [ + 'allowedDoktypes' => [ + 'page' => 1, + 'backend_user_section' => 6, + ], + ], + array_merge(self::DOKTYPES_FROM_CONFIGURATION, [6]), + ], + [ + [ + 'allowedDoktypes' => [ + 'backend_user_section' => 6, + ], + ], + array_merge(self::DOKTYPES_FROM_CONFIGURATION, [6]), + ], + [ + [ + 'allowedDoktypes' => [ + 'duplicateDoktype' => 1, + 'backend_user_section' => 6, + ], + ], + array_merge(self::DOKTYPES_FROM_CONFIGURATION, [6]), + ], + ]; + } + + public static function isSnippetPreviewEnabledCorrectlyBasedOnPageTsConfigurationDataProvider(): array + { + return [ + [ + 1, + [], + true, + ], + [ + 1, + [ + 'mod.' => [ + 'web_SeoPlugin.' => [ + 'disableSnippetPreview' => 0, + ], + ], + ], + true, + ], + [ + 1, + [ + 'mod.' => [ + 'web_SeoPlugin.' => [ + 'disableSnippetPreview' => 1, + ], + ], + ], + false, + ], + ]; + } + + public static function isSnippetPreviewEnabledCorrectlyBasedOnPageRecordDataProvider(): array + { + return [ + [ + 1, + [], + true, + ], + [ + 1, + ['tx_yoastseo_hide_snippet_preview' => '0'], + true, + ], + [ + 1, + ['tx_yoastseo_hide_snippet_preview' => false], + true, + ], + [ + 1, + ['tx_yoastseo_hide_snippet_preview' => '1'], + false, + ], + [ + 1, + ['tx_yoastseo_hide_snippet_preview' => true], + false, + ], + ]; + } +} diff --git a/Tests/Unit/Controller/CrawlerControllerTest.php b/Tests/Unit/Controller/CrawlerControllerTest.php new file mode 100644 index 00000000..9ce5ee74 --- /dev/null +++ b/Tests/Unit/Controller/CrawlerControllerTest.php @@ -0,0 +1,79 @@ +subject = $this->getAccessibleMock( + CrawlerController::class, + ['returnResponse'], + [], + '', + false + ); + + $crawlerService = $this->createMock(CrawlerService::class); + $this->subject->_set('crawlerService', $crawlerService); + + $crawlerJavascriptConfigService = $this->createMock(CrawlerJavascriptConfigService::class); + $this->subject->_set('crawlerJavascriptConfigService', $crawlerJavascriptConfigService); + + $siteFinder = $this->createMock(SiteFinder::class); + $this->subject->_set('siteFinder', $siteFinder); + + $request = $this->createMock(Request::class); + $this->subject->_set('request', $request); + + $responseStub = $this->createStub(HtmlResponse::class); + $this->subject->method('returnResponse')->willReturn($responseStub); + } + + /** + * @test + */ + public function isActionController(): void + { + self::assertInstanceOf(ActionController::class, $this->subject); + } + + /** + * @test + */ + public function indexActionReturnsHtmlResponse(): void + { + $result = $this->subject->indexAction(); + + self::assertInstanceOf(HtmlResponse::class, $result); + } + + /** + * @test + */ + public function resetProgressActionReturnsRedirectResponse(): void + { + $result = $this->subject->resetProgressAction(1, 1); + + self::assertInstanceOf(RedirectResponse::class, $result); + } +} diff --git a/Tests/Unit/Controller/DashboardControllerTest.php b/Tests/Unit/Controller/DashboardControllerTest.php new file mode 100644 index 00000000..94dea6d4 --- /dev/null +++ b/Tests/Unit/Controller/DashboardControllerTest.php @@ -0,0 +1,52 @@ +subject = $this->getAccessibleMock( + DashboardController::class, + ['returnResponse'], + [], + '', + false + ); + + $responseStub = $this->createStub(HtmlResponse::class); + $this->subject->method('returnResponse')->willReturn($responseStub); + } + + /** + * @test + */ + public function isActionController(): void + { + self::assertInstanceOf(ActionController::class, $this->subject); + } + + /** + * @test + */ + public function indexActionReturnsHtmlResponse(): void + { + $result = $this->subject->indexAction(); + + self::assertInstanceOf(HtmlResponse::class, $result); + } +} diff --git a/Tests/Unit/Controller/ModuleControllerTest.php b/Tests/Unit/Controller/ModuleControllerTest.php deleted file mode 100644 index 62da2be3..00000000 --- a/Tests/Unit/Controller/ModuleControllerTest.php +++ /dev/null @@ -1,41 +0,0 @@ -assertEquals($value, $moduleController->getLanguageService()); - } - - public function testBackendUserReturnsValueThatIsSet() - { - $moduleController = new ModuleController(); - - $value = '1517350444'; - $GLOBALS['BE_USER'] = $value; - - $this->assertEquals($value, $moduleController->getBackendUser()); - } -} diff --git a/Tests/Unit/Utility/YoastUtilityTest.php b/Tests/Unit/Utility/YoastUtilityTest.php deleted file mode 100644 index 14a86567..00000000 --- a/Tests/Unit/Utility/YoastUtilityTest.php +++ /dev/null @@ -1,190 +0,0 @@ -assertEquals($expected, $actual); - } - - /** - * @dataProvider isSnippetPreviewEnabledCorrectlyBasedOnPageTsConfigurationDataProvider - * @test - */ - public function isSnippetPreviewEnabledCorrectlyBasedOnPageTsConfiguration($pageId, $config, $expected) - { - $actual = YoastUtility::snippetPreviewEnabled($pageId, ['tx_yoastseo_hide_snippet_preview' => false], $config); - - $this->assertEquals($expected, $actual); - } - - /** - * @dataProvider isSnippetPreviewEnabledCorrectlyBasedOnPageRecordDataProvider - * @test - */ - public function isSnippetPreviewEnabledCorrectlyBasedOnPageRecord($pageId, $pageRecord, $expected) - { - $actual = YoastUtility::snippetPreviewEnabled($pageId, $pageRecord, []); - - $this->assertEquals($expected, $actual); - } - - /** - * ############################### - * - * DATAPROVIDERS - * - * ############################### - */ - - /** - * Dataprovider for areTheRightDoktypesExtractedFromConfiguration test method - * - * @return array - */ - public function areTheRightDoktypesExtractedFromConfigurationDataProvider() - { - return [ - [ - [], - [1] - ], - [ - [ - 'allowedDoktypes' => [ - 'page' => 1, - 'backend_user_section' => 6 - ] - ], - [1, 6] - ], - [ - [ - 'allowedDoktypes' => [ - 'backend_user_section' => 6 - ] - ], - [6] - ], - [ - [ - 'allowedDoktypes' => [ - 'duplicateDoktype' => 1, - 'backend_user_section' => 6 - ] - ], - [1, 6] - ], - ]; - } - - /** - * @return array - */ - public function isSnippetPreviewEnabledCorrectlyBasedOnPageTsConfigurationDataProvider() - { - return [ - [ - 1, - [], - true - ], - [ - 1, - [ - 'mod.' => [ - 'web_SeoPlugin.' => [ - 'disableSnippetPreview' => 0 - ] - ] - ], - true - ], - [ - 1, - [ - 'mod.' => [ - 'web_SeoPlugin.' => [ - 'disableSnippetPreview' => 1 - ] - ] - ], - false - ] - ]; - } - - /** - * @return array - */ - public function isSnippetPreviewEnabledCorrectlyBasedOnPageRecordDataProvider() - { - return [ - [ - 1, - [], - true - ], - [ - 1, - ['tx_yoastseo_hide_snippet_preview' => '0'], - true - ], - [ - 1, - ['tx_yoastseo_hide_snippet_preview' => false], - true - ], - [ - 1, - ['tx_yoastseo_hide_snippet_preview' => '1'], - false - ], - [ - 1, - ['tx_yoastseo_hide_snippet_preview' => true], - false - ], - ]; - } -} diff --git a/composer.json b/composer.json index ec2f6e4c..d8447af4 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "name": "yoast-seo-for-typo3/yoast_seo", "description": "Yoast SEO for TYPO3", - "type": "typo3-cms-extension", "license": "GPL-3.0-or-later", + "type": "typo3-cms-extension", "keywords": [ "TYPO3 CMS", "Yoast", @@ -23,65 +23,66 @@ ], "homepage": "https://yoast.com", "support": { - "source": "https://github.com/Yoast/Yoast-SEO-for-TYPO3", "issues": "https://github.com/Yoast/Yoast-SEO-for-TYPO3/issues", + "source": "https://github.com/Yoast/Yoast-SEO-for-TYPO3", "docs": "https://docs.typo3.org/p/yoast-seo-for-typo3/yoast_seo/main/en-us/" }, "require": { - "typo3/cms-core": "^11.5 || ^12.4 || ^13.0", - "typo3/cms-backend": "^11.5 || ^12.4 || ^13.0", - "typo3/cms-extbase": "^11.5 || ^12.4 || ^13.0", - "typo3/cms-fluid": "^11.5 || ^12.4 || ^13.0", - "typo3/cms-frontend": "^11.5 || ^12.4 || ^13.0", - "typo3/cms-install": "^11.5 || ^12.4 || ^13.0", - "typo3/cms-seo": "^11.5 || ^12.4 || ^13.0", - "ext-curl": "*", + "php": "^8.0", "ext-json": "*", - "php": "^8.0" + "typo3/cms-backend": "^11.5.25 || ^12.4.15 || ^13.0", + "typo3/cms-core": "^11.5.25 || ^12.4.15 || ^13.0", + "typo3/cms-extbase": "^11.5.25 || ^12.4.15 || ^13.0", + "typo3/cms-fluid": "^11.5.25 || ^12.4.15 || ^13.0", + "typo3/cms-frontend": "^11.5.25 || ^12.4.15 || ^13.0", + "typo3/cms-install": "^11.5.25 || ^12.4.15 || ^13.0", + "typo3/cms-seo": "^11.5.25 || ^12.4.15 || ^13.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.0", - "overtrue/phplint": ">=5.5 <10.0", - "phpstan/phpstan": "^1.9", + "ergebnis/composer-normalize": "^2.43", + "friendsofphp/php-cs-fixer": "^3.60.0", + "php-parallel-lint/php-parallel-lint": "^1.4", "phpstan/extension-installer": "^1.0", - "typo3/tailor": "^1.1", - "saschaegerer/phpstan-typo3": "^1.8" - }, - "suggest": { - "typo3/cms-dashboard": "Display Yoast SEO widgets within the Dashboard of TYPO3" + "phpstan/phpstan": "^1.9", + "saschaegerer/phpstan-typo3": "^1.10", + "typo3/coding-standards": "^0.7.1 || ^0.8.0", + "typo3/testing-framework": "^7.1.0 || ^8.2.0" }, "replace": { "typo3-ter/yoast-seo": "self.version" }, + "suggest": { + "typo3/cms-dashboard": "Display Yoast SEO widgets within the Dashboard of TYPO3" + }, "autoload": { "psr-4": { "YoastSeoForTypo3\\YoastSeo\\": "Classes/" } }, - "extra": { - "typo3/cms": { - "extension-key": "yoast_seo" + "autoload-dev": { + "psr-4": { + "YoastSeoForTypo3\\YoastSeo\\Tests\\": "Tests/" } }, "config": { "allow-plugins": { + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true, + "sbuerk/typo3-cmscomposerinstallers-testingframework-bridge": true, "typo3/class-alias-loader": true, - "typo3/cms-composer-installers": true, - "phpstan/extension-installer": true - } + "typo3/cms-composer-installers": true + }, + "bin-dir": ".Build/bin", + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "vendor-dir": ".Build/vendor" }, - "scripts": { - "test:php:lint": [ - "phplint --configuration=.Build/.phplint.yml" - ], - "cgl": [ - "php-cs-fixer fix --config=.Build/.php-cs-fixer.php -v --dry-run --using-cache no --diff" - ], - "cgl-fix": [ - "php-cs-fixer fix --config=.Build/.php-cs-fixer.php -v --using-cache no" - ], - "test:php:phpstan": [ - "phpstan analyse" - ] + "extra": { + "typo3/cms": { + "extension-key": "yoast_seo", + "web-dir": ".Build/public" + } } } diff --git a/ext_emconf.php b/ext_emconf.php index 4d44661f..2cf01a22 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -1,4 +1,5 @@ 'Yoast SEO for TYPO3', 'description' => 'Optimise your website for search engines with Yoast SEO for TYPO3. With this extension you get all the technical SEO stuff you need and will help editors to write high quality content.', @@ -14,9 +15,9 @@ 'depends' => [ 'typo3' => '11.5.0-13.4.99', 'seo' => '11.5.0-13.4.99', - ] + ], ], 'autoload' => [ - 'psr-4' => ['YoastSeoForTypo3\\YoastSeo\\' => 'Classes'] + 'psr-4' => ['YoastSeoForTypo3\\YoastSeo\\' => 'Classes'], ], ]; diff --git a/ext_localconf.php b/ext_localconf.php index 84f1e1c1..41f606ed 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -39,7 +39,7 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][$nodeKey] = [ 'nodeName' => $nodeInfo[0], 'priority' => 40, - 'class' => $nodeInfo[1] + 'class' => $nodeInfo[1], ]; } diff --git a/ext_tables.php b/ext_tables.php index e6473ae8..b6b7388c 100644 --- a/ext_tables.php +++ b/ext_tables.php @@ -15,10 +15,14 @@ $typo3Version = GeneralUtility::makeInstance(Typo3Version::class); if ($typo3Version->getMajorVersion() < 12) { ExtensionManagementUtility::addModule( - 'yoast', '', 'after:web', null, [ + 'yoast', + '', + 'after:web', + null, + [ 'iconIdentifier' => 'module-yoast', 'labels' => 'LLL:EXT:yoast_seo/Resources/Private/Language/BackendModule.xlf', - 'name' => 'yoast' + 'name' => 'yoast', ] ); ExtensionManagementUtility::addCoreNavigationComponent( @@ -36,7 +40,7 @@ 'access' => 'user,group', 'iconIdentifier' => 'module-yoast-dashboard', 'labels' => 'LLL:EXT:yoast_seo/Resources/Private/Language/BackendModuleDashboard.xlf', - 'inheritNavigationComponentFromMainModule' => false + 'inheritNavigationComponentFromMainModule' => false, ] ); @@ -63,7 +67,7 @@ 'access' => 'user,group', 'iconIdentifier' => 'module-yoast-crawler', 'labels' => 'LLL:EXT:yoast_seo/Resources/Private/Language/BackendModuleCrawler.xlf', - 'inheritNavigationComponentFromMainModule' => false + 'inheritNavigationComponentFromMainModule' => false, ] ); @@ -78,7 +82,7 @@ // Extend user settings $GLOBALS['TYPO3_USER_SETTINGS']['columns']['hideYoastInPageModule'] = [ 'label' => 'LLL:EXT:yoast_seo/Resources/Private/Language/BackendModule.xlf:usersettings.hideYoastInPageModule', - 'type' => 'check' + 'type' => 'check', ]; ExtensionManagementUtility::addFieldsToUserSettings( '--div--;LLL:EXT:yoast_seo/Resources/Private/Language/BackendModule.xlf:usersettings.title,hideYoastInPageModule'