From a4037e5e580734707203afec59852a59b398affb Mon Sep 17 00:00:00 2001 From: Riny van Tiggelen Date: Tue, 27 May 2025 20:25:48 +0200 Subject: [PATCH] [FEATURE] First version of the extension, javascript to make a frontend request and returning a json with page information --- .github/workflows/tests.yml | 74 ++ .gitignore | 12 + .php-cs-fixer.php | 10 + Build/Scripts/renderDocumentation.sh | 9 + Build/Scripts/runTests.sh | 666 ++++++++++++++++++ Build/phpstan/phpstan.cms12.neon | 5 + Build/phpstan/phpstan.cms13.neon | 5 + Classes/Builder/RequestContextBuilder.php | 92 +++ Classes/Builder/RequestParametersBuilder.php | 49 ++ Classes/Controller/RequestController.php | 22 + Classes/Dto/RequestContext.php | 31 + Classes/Dto/RequestError.php | 28 + Classes/Dto/RequestParameters.php | 58 ++ Classes/Dto/RequestResult.php | 36 + Classes/Event/ModifyRequestEvent.php | 31 + Classes/Event/ModifyUrlEvent.php | 37 + Classes/Handler/RequestHandler.php | 56 ++ Classes/PageParser/PageParser.php | 24 + Classes/PageParser/Parser/BodyParser.php | 26 + Classes/PageParser/Parser/FaviconParser.php | 37 + Classes/PageParser/Parser/LocaleParser.php | 27 + Classes/PageParser/Parser/MetadataParser.php | 30 + Classes/PageParser/Parser/TitleParser.php | 24 + Classes/PageParser/Parser/UrlParser.php | 26 + Classes/PageParser/ParserCollector.php | 29 + Classes/PageParser/ParserInterface.php | 14 + Classes/PageParser/Service/UrlService.php | 46 ++ Classes/Request/FrontendRequest.php | 42 ++ Configuration/Backend/AjaxRoutes.php | 10 + Configuration/JavaScriptModules.php | 8 + Configuration/Services.yaml | 19 + Documentation/Includes.rst.txt | 34 + Documentation/Index.rst | 59 ++ Documentation/Installation/Index.rst | 19 + Documentation/Introduction/About/Index.rst | 16 + .../Introduction/Contribution/Index.rst | 18 + Documentation/Introduction/Index.rst | 16 + Documentation/Settings.cfg | 62 ++ Documentation/Sitemap.rst | 9 + Documentation/Usage/Events/Index.rst | 81 +++ Documentation/Usage/Index.rst | 17 + Documentation/Usage/Javascript/Index.rst | 72 ++ Documentation/Usage/PageParser/Index.rst | 69 ++ Documentation/genindex.rst | 7 + README.md | 20 +- .../Public/JavaScript/frontend-request.js | 39 + Tests/.gitkeep | 0 composer.json | 77 ++ ext_emconf.php | 20 + 49 files changed, 2216 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .php-cs-fixer.php create mode 100755 Build/Scripts/renderDocumentation.sh create mode 100755 Build/Scripts/runTests.sh create mode 100644 Build/phpstan/phpstan.cms12.neon create mode 100644 Build/phpstan/phpstan.cms13.neon create mode 100644 Classes/Builder/RequestContextBuilder.php create mode 100644 Classes/Builder/RequestParametersBuilder.php create mode 100644 Classes/Controller/RequestController.php create mode 100644 Classes/Dto/RequestContext.php create mode 100644 Classes/Dto/RequestError.php create mode 100644 Classes/Dto/RequestParameters.php create mode 100644 Classes/Dto/RequestResult.php create mode 100644 Classes/Event/ModifyRequestEvent.php create mode 100644 Classes/Event/ModifyUrlEvent.php create mode 100644 Classes/Handler/RequestHandler.php create mode 100644 Classes/PageParser/PageParser.php create mode 100644 Classes/PageParser/Parser/BodyParser.php create mode 100644 Classes/PageParser/Parser/FaviconParser.php create mode 100644 Classes/PageParser/Parser/LocaleParser.php create mode 100644 Classes/PageParser/Parser/MetadataParser.php create mode 100644 Classes/PageParser/Parser/TitleParser.php create mode 100644 Classes/PageParser/Parser/UrlParser.php create mode 100644 Classes/PageParser/ParserCollector.php create mode 100644 Classes/PageParser/ParserInterface.php create mode 100644 Classes/PageParser/Service/UrlService.php create mode 100644 Classes/Request/FrontendRequest.php create mode 100644 Configuration/Backend/AjaxRoutes.php create mode 100644 Configuration/JavaScriptModules.php create mode 100644 Configuration/Services.yaml create mode 100644 Documentation/Includes.rst.txt create mode 100644 Documentation/Index.rst create mode 100644 Documentation/Installation/Index.rst create mode 100644 Documentation/Introduction/About/Index.rst create mode 100644 Documentation/Introduction/Contribution/Index.rst create mode 100644 Documentation/Introduction/Index.rst create mode 100644 Documentation/Settings.cfg create mode 100644 Documentation/Sitemap.rst create mode 100644 Documentation/Usage/Events/Index.rst create mode 100644 Documentation/Usage/Index.rst create mode 100644 Documentation/Usage/Javascript/Index.rst create mode 100644 Documentation/Usage/PageParser/Index.rst create mode 100644 Documentation/genindex.rst create mode 100644 Resources/Public/JavaScript/frontend-request.js create mode 100644 Tests/.gitkeep create mode 100644 composer.json create mode 100644 ext_emconf.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..1772d5f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,74 @@ +name: Tests + +on: [ push, pull_request ] + +jobs: + tests: + name: Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [ '8.2', '8.3' ] + typo3: [ '12', '13' ] + composerInstall: [ 'composerInstallLowest', 'composerInstallHighest' ] + include: + - typo3: '12' + php: '8.1' + composerInstall: 'composerInstallLowest' + - typo3: '12' + php: '8.1' + composerInstall: 'composerInstallHighest' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Composer validate + run: Build/Scripts/runTests.sh -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s composerValidate + + - name: Install dependencies + run: Build/Scripts/runTests.sh -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s ${{ matrix.composerInstall }} + + - name: Composer normalize + run: Build/Scripts/runTests.sh -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s composerNormalize -n + + - name: Lint PHP + run: Build/Scripts/runTests.sh -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s lint + + - name: CGL + run: Build/Scripts/runTests.sh -n -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s cgl -n + + - name: PHPStan + run: Build/Scripts/runTests.sh -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s phpstan -e "--error-format=github" + + #- name: Functional tests + #run: Build/Scripts/runTests.sh -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s functional + + #- name: Unit tests + #run: Build/Scripts/runTests.sh -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s unit + + #- name: Functional tests coverage + #if: matrix.php == '8.2' && matrix.typo3 == '13' && matrix.composerInstall == 'composerInstallHighest' + #run: Build/Scripts/runTests.sh -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s unit -x -e "--coverage-php=.Build/coverage/functional.cov" + + #- name: Unit tests coverage + #if: matrix.php == '8.2' && matrix.typo3 == '13' && matrix.composerInstall == 'composerInstallHighest' + #run: Build/Scripts/runTests.sh -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s unit -x -e "--coverage-php=.Build/coverage/unit.cov" + + #- name: Composer require phpunit/phpcov for merging the code coverage + #if: matrix.php == '8.2' && matrix.typo3 == '13' && matrix.composerInstall == 'composerInstallHighest' + #run: Build/Scripts/runTests.sh -t ${{ matrix.typo3 }} -p ${{ matrix.php }} -s composer require --dev phpunit/phpcov + + #- name: Merge coverage + #if: matrix.php == '8.2' && matrix.typo3 == '13' && matrix.composerInstall == 'composerInstallHighest' + #run: ./.Build/bin/phpcov merge --clover=build/logs/clover.xml .Build/coverage + + #- name: Upload coverage + #if: matrix.php == '8.2' && matrix.typo3 == '13' && matrix.composerInstall == 'composerInstallHighest' + #env: + #COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + #run: | + #composer global require php-coveralls/php-coveralls + #export PATH="$(composer config -g home)/vendor/bin:$PATH" + #php-coveralls --coverage_clover=build/logs/clover.xml -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea90d81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.idea +vendor +composer.lock +.php-cs-fixer.cache +.Build +Documentation-GENERATED-temp +/public +.phplint.cache +.cache +composer.json.testing +var +*.map \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..900ca7b --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,10 @@ +setParallelConfig(ParallelConfigFactory::detect()); + +$config->getFinder()->in('Classes')->in('Configuration')->in('Tests'); +return $config; diff --git a/Build/Scripts/renderDocumentation.sh b/Build/Scripts/renderDocumentation.sh new file mode 100755 index 0000000..9cf0e85 --- /dev/null +++ b/Build/Scripts/renderDocumentation.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# This file is meant to be run from the root of the repository. + +docker run --rm --user=$(id -u):$(id -g) \ + -v "$(pwd):/PROJECT" \ + -v "$(pwd)/Documentation-GENERATED-temp:/RESULT" \ + ghcr.io/t3docs/render-documentation:latest \ + makehtml \ No newline at end of file diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh new file mode 100755 index 0000000..25d68c2 --- /dev/null +++ b/Build/Scripts/runTests.sh @@ -0,0 +1,666 @@ +#!/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 +TEST_SUITE="help" +TYPO3_VERSION="11" +EXTRA_TEST_OPTIONS="" +DATABASE_DRIVER="" +DBMS="sqlite" +DBMS_VERSION="" +PHP_VERSION="8.2" +PHP_XDEBUG_COVERAGE=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_COVERAGE=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_COVERAGE} -eq 0 ]; then + XDEBUG_MODE="-e XDEBUG_MODE=off" + XDEBUG_CONFIG=" " +else + XDEBUG_MODE="-e XDEBUG_MODE=coverage" +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/phpstan.cms12.neon b/Build/phpstan/phpstan.cms12.neon new file mode 100644 index 0000000..b545cef --- /dev/null +++ b/Build/phpstan/phpstan.cms12.neon @@ -0,0 +1,5 @@ +parameters: + level: 8 + paths: + - ../../Classes + - ../../Configuration \ No newline at end of file diff --git a/Build/phpstan/phpstan.cms13.neon b/Build/phpstan/phpstan.cms13.neon new file mode 100644 index 0000000..b545cef --- /dev/null +++ b/Build/phpstan/phpstan.cms13.neon @@ -0,0 +1,5 @@ +parameters: + level: 8 + paths: + - ../../Classes + - ../../Configuration \ No newline at end of file diff --git a/Classes/Builder/RequestContextBuilder.php b/Classes/Builder/RequestContextBuilder.php new file mode 100644 index 0000000..8eb7582 --- /dev/null +++ b/Classes/Builder/RequestContextBuilder.php @@ -0,0 +1,92 @@ +resolveMountPoint($parameters); + $site = $this->getSite($parameters); + if ($site === null) { + return new RequestContext(); + } + $url = $this->generateUri($site, $parameters); + + /** @var ModifyUrlEvent $event */ + $event = $this->eventDispatcher->dispatch( + new ModifyUrlEvent($url, $site, $parameters) + ); + return new RequestContext( + $event->getUrl(), + $site, + $parameters + ); + } + + protected function resolveMountPoint(RequestParameters $parameters): void + { + if ($parameters->getPageId() === null) { + return; + } + $mountPoint = $this->pageRepository->getMountPointInfo($parameters->getPageId()); + if (isset($mountPoint['overlay'], $mountPoint['mount_pid'], $mountPoint['MPvar'])) { + $parameters->setPageId($mountPoint['mount_pid']); + $parameters->addAdditionalParameters('&MP=' . $mountPoint['MPvar']); + } + } + + protected function getSite(RequestParameters $parameters): ?Site + { + if ($parameters->getPageId() === null) { + return null; + } + $rootLine = BackendUtility::BEgetRootLine($parameters->getPageId()); + try { + return $this->siteFinder->getSiteByPageId($parameters->getPageId(), $rootLine); + } catch (SiteNotFoundException) { + return null; + } + } + + protected function generateUri(Site $site, RequestParameters $parameters): string + { + if ($parameters->getPageId() === null) { + return ''; + } + $additionalQueryParams = []; + $additionalGetVars = rawurldecode($parameters->getAdditionalParameters() ?? ''); + parse_str($additionalGetVars, $additionalQueryParams); + $additionalQueryParams['_language'] = $site->getLanguageById($parameters->getLanguageId() ?? 0); + return $this->fixAbsoluteUrl( + (string)$site->getRouter()->generateUri($parameters->getPageId(), $additionalQueryParams) + ); + } + + protected function fixAbsoluteUrl(string $url): string + { + if (str_starts_with($url, '/')) { + $url = GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . $url; + } + return $url; + } +} diff --git a/Classes/Builder/RequestParametersBuilder.php b/Classes/Builder/RequestParametersBuilder.php new file mode 100644 index 0000000..211d0b5 --- /dev/null +++ b/Classes/Builder/RequestParametersBuilder.php @@ -0,0 +1,49 @@ +getQueryParams(); + if (isset($queryParams['pageId'], $queryParams['languageId'])) { + return new RequestParameters( + (int)$queryParams['pageId'], + (int)$queryParams['languageId'], + (string)($queryParams['additionalGetVars'] ?? '') + ); + } + + $json = $this->getJsonPostData($request); + if (isset($json['pageId'], $json['languageId'])) { + return new RequestParameters( + (int)$json['pageId'], + (int)$json['languageId'], + (string)($json['additionalGetVars'] ?? '') + ); + } + return new RequestParameters(); + } + + /** + * @return array + */ + protected function getJsonPostData(ServerRequestInterface $request): array + { + $body = $request->getBody()->getContents(); + if (empty($body)) { + return []; + } + try { + return json_decode($body, true, 3, JSON_THROW_ON_ERROR); + } catch (\JsonException) { + return []; + } + } +} diff --git a/Classes/Controller/RequestController.php b/Classes/Controller/RequestController.php new file mode 100644 index 0000000..5913228 --- /dev/null +++ b/Classes/Controller/RequestController.php @@ -0,0 +1,22 @@ +requestHandler->handle($request); + } +} diff --git a/Classes/Dto/RequestContext.php b/Classes/Dto/RequestContext.php new file mode 100644 index 0000000..1626e94 --- /dev/null +++ b/Classes/Dto/RequestContext.php @@ -0,0 +1,31 @@ +url; + } + + public function getSite(): ?Site + { + return $this->site; + } + + public function getParameters(): ?RequestParameters + { + return $this->parameters; + } +} diff --git a/Classes/Dto/RequestError.php b/Classes/Dto/RequestError.php new file mode 100644 index 0000000..3ff5d5f --- /dev/null +++ b/Classes/Dto/RequestError.php @@ -0,0 +1,28 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'error' => [ + 'reason' => $this->reason, + 'url' => $this->url, + 'statusCode' => $this->statusCode, + ], + ]; + } +} diff --git a/Classes/Dto/RequestParameters.php b/Classes/Dto/RequestParameters.php new file mode 100644 index 0000000..a6f4650 --- /dev/null +++ b/Classes/Dto/RequestParameters.php @@ -0,0 +1,58 @@ +pageId; + } + + public function setPageId(int $pageId): void + { + $this->pageId = $pageId; + } + + public function getLanguageId(): ?int + { + return $this->languageId; + } + + public function setLanguageId(int $languageId): void + { + $this->languageId = $languageId; + } + + public function getAdditionalParameters(): ?string + { + return $this->additionalParameters; + } + + public function setAdditionalParameters(string $additionalParameters): void + { + $this->additionalParameters = $additionalParameters; + } + + public function addAdditionalParameters(string $additionalParameters): void + { + if ($this->additionalParameters === null) { + $this->additionalParameters = $additionalParameters; + } else { + $this->additionalParameters .= '&' . $additionalParameters; + } + } + + public function isValid(): bool + { + return $this->pageId !== null && $this->languageId !== null; + } +} diff --git a/Classes/Dto/RequestResult.php b/Classes/Dto/RequestResult.php new file mode 100644 index 0000000..d32dba5 --- /dev/null +++ b/Classes/Dto/RequestResult.php @@ -0,0 +1,36 @@ + $data + */ + public function __construct( + protected array $data = [], + ) {} + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } + + public function addData(string $key, mixed $value): void + { + $this->data[$key] = $value; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->data; + } +} diff --git a/Classes/Event/ModifyRequestEvent.php b/Classes/Event/ModifyRequestEvent.php new file mode 100644 index 0000000..cf447d8 --- /dev/null +++ b/Classes/Event/ModifyRequestEvent.php @@ -0,0 +1,31 @@ +request; + } + + public function setRequest(RequestInterface $request): void + { + $this->request = $request; + } + + public function getContext(): RequestContext + { + return $this->context; + } +} diff --git a/Classes/Event/ModifyUrlEvent.php b/Classes/Event/ModifyUrlEvent.php new file mode 100644 index 0000000..7305b8d --- /dev/null +++ b/Classes/Event/ModifyUrlEvent.php @@ -0,0 +1,37 @@ +url; + } + + public function getSite(): Site + { + return $this->site; + } + + public function getParameters(): RequestParameters + { + return $this->parameters; + } + + public function setUrl(string $url): void + { + $this->url = $url; + } +} diff --git a/Classes/Handler/RequestHandler.php b/Classes/Handler/RequestHandler.php new file mode 100644 index 0000000..36ce423 --- /dev/null +++ b/Classes/Handler/RequestHandler.php @@ -0,0 +1,56 @@ +parametersBuilder->build($request); + if (!$parameters->isValid()) { + return $this->jsonResponse(new RequestError('Invalid request parameters'), 400); + } + + $context = $this->contextBuilder->build($parameters); + try { + $html = $this->frontendRequest->getHtmlResponse($context); + $data = $this->pageParser->parsePage($html, $context); + } catch (\Exception $e) { + $data = new RequestError('Request failed', $context->getUrl(), (int)$e->getMessage()); + } + + return $this->jsonResponse($data); + } + + protected function jsonResponse(RequestResult|RequestError $data, int $statusCode = 200): ResponseInterface + { + $response = $this->responseFactory->createResponse()->withHeader( + 'Content-Type', + 'application/json; charset=utf-8' + )->withStatus($statusCode); + $response->getBody()->write( + json_encode($data, JSON_THROW_ON_ERROR), + ); + return $response; + } +} diff --git a/Classes/PageParser/PageParser.php b/Classes/PageParser/PageParser.php new file mode 100644 index 0000000..2528e76 --- /dev/null +++ b/Classes/PageParser/PageParser.php @@ -0,0 +1,24 @@ +parserCollector->getAll() as $parser) { + $result->addData($parser->getIdentifier(), $parser->parse($content, $context)); + } + return $result; + } +} diff --git a/Classes/PageParser/Parser/BodyParser.php b/Classes/PageParser/Parser/BodyParser.php new file mode 100644 index 0000000..1fdf9c2 --- /dev/null +++ b/Classes/PageParser/Parser/BodyParser.php @@ -0,0 +1,26 @@ +]*>(.*?)<\/body>/is', $html, $matchesBody); + if (!$bodyFound) { + return ''; + } + + return $matchesBody[1]; + } +} diff --git a/Classes/PageParser/Parser/FaviconParser.php b/Classes/PageParser/Parser/FaviconParser.php new file mode 100644 index 0000000..fe69775 --- /dev/null +++ b/Classes/PageParser/Parser/FaviconParser.php @@ -0,0 +1,37 @@ +urlService->getBaseUrl($context); + + $faviconSrc = $baseUrl . '/favicon.ico'; + $favIconFound = preg_match('/]*lang="([a-z\-A-Z]*)"/is', $html, $matchesLocale); + if ($localeFound) { + [$locale] = explode('-', trim($matchesLocale[1])); + return $locale; + } + + return 'en'; // Default locale if not found + } +} diff --git a/Classes/PageParser/Parser/MetadataParser.php b/Classes/PageParser/Parser/MetadataParser.php new file mode 100644 index 0000000..6e03126 --- /dev/null +++ b/Classes/PageParser/Parser/MetadataParser.php @@ -0,0 +1,30 @@ + + */ + public function parse(string $html, RequestContext $context): array + { + $metadata = []; + if (preg_match_all('/(.*?)<\/title>/i', $html, $matches)) { + return strip_tags(html_entity_decode($matches[1])); + } + return ''; + } +} diff --git a/Classes/PageParser/Parser/UrlParser.php b/Classes/PageParser/Parser/UrlParser.php new file mode 100644 index 0000000..01a28a8 --- /dev/null +++ b/Classes/PageParser/Parser/UrlParser.php @@ -0,0 +1,26 @@ +urlService->getUrl($context); + } +} diff --git a/Classes/PageParser/ParserCollector.php b/Classes/PageParser/ParserCollector.php new file mode 100644 index 0000000..56d7678 --- /dev/null +++ b/Classes/PageParser/ParserCollector.php @@ -0,0 +1,29 @@ + $taggedParsers + */ + public function __construct(iterable $taggedParsers) + { + foreach ($taggedParsers as $parser) { + $this->parsers[$parser->getIdentifier()] = $parser; + } + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->parsers; + } +} diff --git a/Classes/PageParser/ParserInterface.php b/Classes/PageParser/ParserInterface.php new file mode 100644 index 0000000..fd37021 --- /dev/null +++ b/Classes/PageParser/ParserInterface.php @@ -0,0 +1,14 @@ + + */ + protected array $baseUrls = []; + + public function getBaseUrl(RequestContext $context): string + { + if (isset($this->baseUrls[$context->getUrl()])) { + return $this->baseUrls[$context->getUrl()]; + } + $urlParts = $this->getUrlParts($context); + 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'] ?? ''); + } + + public function getUrl(RequestContext $context): string + { + $urlParts = $this->getUrlParts($context); + $baseUrl = $this->getBaseUrl($context); + return $baseUrl . ($urlParts['path'] ?? ''); + } + + /** + * @return array|null + */ + protected function getUrlParts(RequestContext $context): ?array + { + $urlParts = parse_url((string)preg_replace('/\/$/', '', $context->getUrl())); + return is_array($urlParts) ? $urlParts : null; + } +} diff --git a/Classes/Request/FrontendRequest.php b/Classes/Request/FrontendRequest.php new file mode 100644 index 0000000..e108b1b --- /dev/null +++ b/Classes/Request/FrontendRequest.php @@ -0,0 +1,42 @@ +requestFactory->createRequest('GET', $context->getUrl()); + + $event = $this->eventDispatcher->dispatch( + new ModifyRequestEvent($request, $context) + ); + + try { + $response = $this->client->sendRequest($event->getRequest()); + } catch (RequestException $e) { + throw new \RuntimeException((string)$e->getCode(), 1748200917, $e); + } + + if ($response->getStatusCode() !== 200) { + throw new \RuntimeException((string)$response->getStatusCode()); + } + + return $response->getBody()->getContents(); + } +} diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php new file mode 100644 index 0000000..4615264 --- /dev/null +++ b/Configuration/Backend/AjaxRoutes.php @@ -0,0 +1,10 @@ + [ + 'path' => 'frontendrequest/request', + 'target' => RequestController::class . '::requestAction', + ], +]; diff --git a/Configuration/JavaScriptModules.php b/Configuration/JavaScriptModules.php new file mode 100644 index 0000000..bf51536 --- /dev/null +++ b/Configuration/JavaScriptModules.php @@ -0,0 +1,8 @@ + ['backend'], + 'imports' => [ + '@maxserv/frontend-request/' => 'EXT:frontend_request/Resources/Public/JavaScript/', + ], +]; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..b1a835f --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,19 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + MaxServ\FrontendRequest\: + resource: '../Classes/*' + + _instanceof: + MaxServ\FrontendRequest\PageParser\ParserInterface: + tags: [ 'frontend_request.page_parser' ] + + MaxServ\FrontendRequest\PageParser\ParserCollector: + arguments: + $taggedParsers: !tagged_iterator frontend_request.page_parser + + MaxServ\FrontendRequest\Controller\RequestController: + public: true diff --git a/Documentation/Includes.rst.txt b/Documentation/Includes.rst.txt new file mode 100644 index 0000000..d1bbcda --- /dev/null +++ b/Documentation/Includes.rst.txt @@ -0,0 +1,34 @@ +.. More information about this file: + https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/GeneralConventions/FileStructure.html#includes-rst-txt + +.. ---------- +.. text roles +.. ---------- + +.. role:: aspect(emphasis) +.. role:: bash(code) +.. role:: html(code) +.. role:: js(code) +.. role:: php(code) +.. role:: rst(code) +.. role:: sep(strong) +.. role:: sql(code) + +.. role:: tsconfig(code) + :class: typoscript + +.. role:: typoscript(code) +.. role:: xml(code) + :class: html + +.. role:: yaml(code) + +.. default-role:: code + +.. --------- +.. highlight +.. --------- + +.. By default, code blocks use PHP syntax highlighting + +.. highlight:: php \ No newline at end of file diff --git a/Documentation/Index.rst b/Documentation/Index.rst new file mode 100644 index 0000000..4c832fc --- /dev/null +++ b/Documentation/Index.rst @@ -0,0 +1,59 @@ +.. include:: /Includes.rst.txt + +================ +Frontend Request +================ + +:Extension key: + frontend_request + +:Package name: + maxserv/frontend-request + +:Version: + |release| + +:Language: + en + +:Author: + MaxServ + +:License: + This document is published under the + `Open Publication License `__. + +:Rendered: + |today| + +---- + +This extension helps you to handle frontend requests in TYPO3 from within the backend. +It provides a javascript module that you can import in your own extension's javascript +and based on parameters like the page id, language, and more, it returns a JSON object +with the page information, such as the page title, description, and more. + +This extension is not useful on its own, but it is meant to be used in combination with +other extensions that need to handle frontend requests in the backend, such as +Yoast SEO for TYPO3, which uses this extension to analyze the page content and provide +analysis and suggestions for improving the page's SEO and readability. + +---- + +**Table of Contents:** + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + Introduction/Index + Installation/Index + Usage/Index + +.. Meta Menu + +.. toctree:: + :hidden: + + Sitemap + genindex diff --git a/Documentation/Installation/Index.rst b/Documentation/Installation/Index.rst new file mode 100644 index 0000000..60a35ce --- /dev/null +++ b/Documentation/Installation/Index.rst @@ -0,0 +1,19 @@ +.. include:: /Includes.rst.txt + + +.. _installation: + +Installation guide +================== + +Install the extension using your preferred method. + +Head over to your CLI and run the following command + +.. code-block:: bash + + composer require maxserv/frontend-request + +Not using composer? Download the zip file from the +`TER `__ and upload it through +the Extension Manager. diff --git a/Documentation/Introduction/About/Index.rst b/Documentation/Introduction/About/Index.rst new file mode 100644 index 0000000..eb8c2d5 --- /dev/null +++ b/Documentation/Introduction/About/Index.rst @@ -0,0 +1,16 @@ +.. include:: /Includes.rst.txt + + +.. _about: + +What does it do? +================ + +This extension provides a simple way to handle frontend requests in TYPO3 from within the backend. +It allows you to retrieve page information such as title, description, and more based on parameters like page id +and language. This is particularly useful for extensions that need to analyze frontend content,such as SEO tools. + +This extension does not do anything on its own but is designed to be used in conjunction with other extensions +that require frontend request handling. + +It provides a JavaScript module that can be imported into your own extension's JavaScript files. \ No newline at end of file diff --git a/Documentation/Introduction/Contribution/Index.rst b/Documentation/Introduction/Contribution/Index.rst new file mode 100644 index 0000000..e14b89c --- /dev/null +++ b/Documentation/Introduction/Contribution/Index.rst @@ -0,0 +1,18 @@ +.. include:: /Includes.rst.txt + + +.. _contribution: + +Contribution +============ + +Anyone is welcome to contribute to Frontend Request. + +There are various ways you can contribute: + +* `Raise an issue `__ on GitHub. +* Create a pull request with your bug fixes or new features. +* Improve this documentation via the "Edit on GitHub" workflow. + +Please `read the guidelines `__ +on how to contribute to this repository and how to report bugs. \ No newline at end of file diff --git a/Documentation/Introduction/Index.rst b/Documentation/Introduction/Index.rst new file mode 100644 index 0000000..86029f0 --- /dev/null +++ b/Documentation/Introduction/Index.rst @@ -0,0 +1,16 @@ +.. include:: /Includes.rst.txt + + +.. _introduction: + +Introduction +============ + +Read about "*frontend_request*" in this chapter. + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + About/Index + Contribution/Index diff --git a/Documentation/Settings.cfg b/Documentation/Settings.cfg new file mode 100644 index 0000000..c3288ac --- /dev/null +++ b/Documentation/Settings.cfg @@ -0,0 +1,62 @@ +# More information about this file: +# https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/GeneralConventions/FileStructure.html#settings-cfg + +[general] + +project = Frontend Request +version = main (development) +release = main (development) +copyright = since 2025 by MaxServ + +[html_theme_options] + +# "Edit on GitHub" button +github_repository = MaxServ/frontend-request +github_branch = main + +# Footer links +project_home = https://github.com/MaxServ/frontend-request +project_contact = https://github.com/MaxServ/frontend-request +project_repository = https://github.com/MaxServ/frontend-request +project_issues = https://github.com/MaxServ/frontend-request/issues +project_discussions = + +use_opensearch = + +[intersphinx_mapping] + +# Official TYPO3 manuals +# h2document = https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/ +# t3cheatsheets = https://docs.typo3.org/m/typo3/docs-cheatsheets/main/en-us/ +# t3contribute = https://docs.typo3.org/m/typo3/guide-contributionworkflow/main/en-us/ +# t3coreapi = https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ +# t3docteam = https://docs.typo3.org/m/typo3/team-t3docteam/main/en-us/ +# t3editors = https://docs.typo3.org/m/typo3/tutorial-editors/main/en-us/ +# t3extbasebook = https://docs.typo3.org/m/typo3/book-extbasefluid/main/en-us/ +# t3extexample = https://docs.typo3.org/m/typo3/guide-example-extension-manual/main/en-us/ +# t3home = https://docs.typo3.org/ +# t3install = https://docs.typo3.org/m/typo3/guide-installation/main/en-us/ +# t3l10n = https://docs.typo3.org/m/typo3/guide-frontendlocalization/main/en-us/ +# t3sitepackage = https://docs.typo3.org/m/typo3/tutorial-sitepackage/main/en-us/ +# t3start = https://docs.typo3.org/m/typo3/tutorial-getting-started/main/en-us/ +# t3tca = https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ +# t3templating = https://docs.typo3.org/m/typo3/tutorial-templating/main/en-us/ +# t3translate = https://docs.typo3.org/m/typo3/guide-frontendlocalization/main/en-us/ +t3tsconfig = https://docs.typo3.org/m/typo3/reference-tsconfig/12.4/en-us/ +# t3tsref = https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/ +# t3ts45 = https://docs.typo3.org/m/typo3/tutorial-typoscript-in-45-minutes/main/en-us/ +# t3viewhelper = https://docs.typo3.org/other/typo3/view-helper-reference/main/en-us/ +# t3upgrade = https://docs.typo3.org/m/typo3/guide-installation/main/en-us/ + +# TYPO3 system extensions +# ext_adminpanel = https://docs.typo3.org/c/typo3/cms-adminpanel/main/en-us/ +# ext_core = https://docs.typo3.org/c/typo3/cms-core/main/en-us/ +# ext_dashboard = https://docs.typo3.org/c/typo3/cms-dashboard/main/en-us/ +# ext_felogin = https://docs.typo3.org/c/typo3/cms-felogin/main/en-us/ +# ext_form = https://docs.typo3.org/c/typo3/cms-form/main/en-us/ +# ext_fsc = https://docs.typo3.org/c/typo3/cms-fluid-styled-content/main/en-us/ +# ext_indexed_search = https://docs.typo3.org/c/typo3/cms-indexed-search/main/en-us/ +# ext_rte_ckeditor = https://docs.typo3.org/c/typo3/cms-rte-ckeditor/main/en-us/ +# ext_scheduler = https://docs.typo3.org/c/typo3/cms-scheduler/main/en-us/ +# ext_seo = https://docs.typo3.org/c/typo3/cms-seo/main/en-us/ +# ext_workspaces = https://docs.typo3.org/c/typo3/cms-workspaces/main/en-us/ diff --git a/Documentation/Sitemap.rst b/Documentation/Sitemap.rst new file mode 100644 index 0000000..09d3c6f --- /dev/null +++ b/Documentation/Sitemap.rst @@ -0,0 +1,9 @@ +:template: sitemap.html + +.. include:: /Includes.rst.txt + +======= +Sitemap +======= + +.. The sitemap.html template will insert here the page tree automatically. diff --git a/Documentation/Usage/Events/Index.rst b/Documentation/Usage/Events/Index.rst new file mode 100644 index 0000000..2554902 --- /dev/null +++ b/Documentation/Usage/Events/Index.rst @@ -0,0 +1,81 @@ +.. include:: /Includes.rst.txt + + +.. _events: + +Events +====== + +Event to change the URL to request +---------------------------------- +By default the extension is generating the URL to analyse based on the Site Configuration and configured Route Enhancers. In some cases +you want to change the URL that needs to be analysed. + +With the `ModifyPreviewUrlEvent` you will be able to alter the URL that will be used to analyse your content. + +First you need to create an EventListener that will handle the event: + +.. code-block:: php + + getUrl(); + $site = $event->getSite(); + $parameters = $event->getParameters(); + + // Create a new url based on your needs + + $event->setUrl($newUrl); + } + } + + +Event to change the request +--------------------------- +The request is generated with the `RequestFactory` from TYPO3. + +With the `ModifyRequest` you will be able to change the request, this makes it possible to (f.e.) add your own headers +to the request. + +First you need to create an EventListener that will handle the event: + +.. code-block:: php + + getRequest(); + $context = $event->getContext(); + + $event->setRequest( + $request->withHeader('X-Your-Own-Header', GeneralUtility::hmac( + $context->getUrl() + )) + ); + } + } + +Registering an Event Listener +----------------------------- + +After you created an event listener, you need to register it. + +Check the TYPO3 documentation for your used TYPO3 version on how to register this correctly. + +After you have registered the listener, you need to clear the TYPO3 cache through the `Maintenance` module +(or `vendor/bin/typo3 cache:flush`) before your code will be activated. diff --git a/Documentation/Usage/Index.rst b/Documentation/Usage/Index.rst new file mode 100644 index 0000000..c7d3ec8 --- /dev/null +++ b/Documentation/Usage/Index.rst @@ -0,0 +1,17 @@ +.. include:: /Includes.rst.txt + + +.. _usage: + +Usage Guide +=========== + +The usage guide provides detailed information on how to use the features of the extension. + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + Javascript/Index + PageParser/Index + Events/Index diff --git a/Documentation/Usage/Javascript/Index.rst b/Documentation/Usage/Javascript/Index.rst new file mode 100644 index 0000000..ee5309f --- /dev/null +++ b/Documentation/Usage/Javascript/Index.rst @@ -0,0 +1,72 @@ +.. include:: /Includes.rst.txt + + +.. _javascript: + +Javascript +========== + +Javascript module +----------------- +This extension provides a Javascript module that you can import in your own Javascript code to +make the request and get the parsed page response. + +The `FrontendRequest` class provides a `request` method which expects an object with the following properties: + +* `pageId`: The ID of the page you want to request. + +* `languageId`: The ID of the language you want to request the page in. + +* `additionalGetVars`: (optional) A string with the additional get parameters to be sent with the request. + +Example usage: + +.. code-block:: javascript + + import FrontendRequest from "@maxserv/frontend-request/frontend-request.js" + + class YourJavascriptClass { + async fetchPage(pageId, languageId) { + FrontendRequest.request({ + pageId: pageId, + languageId: languageId, + }).then((response) => { + // Handle the response here + console.log(response); + }).catch((error) => { + // Handle any errors here + console.error(error); + }) + } + } + + +Fire event to listen to +----------------------- +The FrontendRequest class also has a method `requestWithEvent` which you can call from within PHP to +fire an event that can be listened to in your JavaScript code. + +An example using the `JavascriptRenderer` within the `PageRenderer`: + +.. code-block:: php + + $pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@maxserv/frontend-request/frontend-request.js')->invoke( + 'requestWithEvent', + [ + 'pageId' => $pageId, + 'languageId' => $languageId, + 'additionalGetVars' => $additionalGetVars ?? '', + ], + 'your-event-name' + ) + ); + +This will fire an event with the name `your-event-name` that you can listen to in your JavaScript code. + +.. code-block:: javascript + + document.addEventListener('your-event-name', (event) => { + // Handle the event here + console.log(event.detail); + }); \ No newline at end of file diff --git a/Documentation/Usage/PageParser/Index.rst b/Documentation/Usage/PageParser/Index.rst new file mode 100644 index 0000000..27c0c63 --- /dev/null +++ b/Documentation/Usage/PageParser/Index.rst @@ -0,0 +1,69 @@ +.. include:: /Includes.rst.txt + + +.. _pageparser: + +PageParser +========== + +The PageParser is responsible for parsing the frontend response and transforming it into a JSON object. + +The default parsers are: + +* **body**: Has the complete body of the page. + +* **title**: Contains the title of the page. + +* **metadata**: Contains all the metatags of the page, structured by name and content. + +* **locale**: Contains the locale of the page. + +* **url**: Contains the URL of the page. + +* **favicon**: Contains the URL of the favicon of the page. + + +Adding your own parser +---------------------- + +If you want to add your own parser, you can do so by adding a class which implements the `ParserInterface`: + +.. code-block:: php + + { + document.dispatchEvent(new CustomEvent(event, { + detail: response + })); + }) + .catch((error) => { + document.dispatchEvent(new CustomEvent(event, { + detail: error + })); + }); + } +} + +const frontendRequest = new FrontendRequest(); +export default frontendRequest; diff --git a/Tests/.gitkeep b/Tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..397b35f --- /dev/null +++ b/composer.json @@ -0,0 +1,77 @@ +{ + "name": "maxserv/frontend-request", + "description": "Helper extension to make a frontend request from the backend of TYPO3 using javascript.", + "license": "GPL-3.0-or-later", + "type": "typo3-cms-extension", + "keywords": [ + "TYPO3 CMS", + "MaxServ" + ], + "authors": [ + { + "name": "Team MaxServ", + "email": "support@maxserv.com", + "homepage": "https://maxserv.com" + } + ], + "homepage": "https://maxserv.com", + "support": { + "issues": "https://github.com/MaxServ/frontend-request/issues", + "source": "https://github.com/MaxServ/frontend-request", + "docs": "https://docs.typo3.org/p/maxserv/frontend_request/main/en-us/" + }, + "require": { + "php": "^8.1", + "ext-json": "*", + "typo3/cms-backend": "^12.4.15 || ^13.4", + "typo3/cms-core": "^12.4.15 || ^13.4", + "typo3/cms-frontend": "^12.4.15 || ^13.4" + }, + "require-dev": { + "composer/class-map-generator": "^1.3.4", + "dg/bypass-finals": "^1.8", + "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", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^10.1", + "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/frontend-request": "self.version" + }, + "autoload": { + "psr-4": { + "MaxServ\\FrontendRequest\\": "Classes/" + } + }, + "autoload-dev": { + "psr-4": { + "MaxServ\\FrontendRequest\\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 + }, + "bin-dir": ".Build/bin", + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "vendor-dir": ".Build/vendor" + }, + "extra": { + "typo3/cms": { + "extension-key": "frontend_request", + "web-dir": ".Build/public" + } + } +} diff --git a/ext_emconf.php b/ext_emconf.php new file mode 100644 index 0000000..32c20f6 --- /dev/null +++ b/ext_emconf.php @@ -0,0 +1,20 @@ + 'Frontend Request', + 'description' => 'This extension provides a way to make a frontend request to a TYPO3 page from within the backend and parse the response.', + 'category' => 'backend', + 'author' => 'MaxServ', + 'author_company' => 'MaxServ B.V.', + 'author_email' => 'support@maxserv.com', + 'state' => 'stable', + 'version' => '1.0.0', + 'constraints' => [ + 'depends' => [ + 'typo3' => '12.4.0-13.4.99', + ], + ], + 'autoload' => [ + 'psr-4' => ['MaxServ\\FrontendRequest\\' => 'Classes'], + ], +];