diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4304ca2..046f96f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,8 +6,6 @@ on: push: branches: - main - - v4 - - php85 - typo3-v14 pull_request: permissions: @@ -71,8 +69,6 @@ jobs: - "php:csfix" - "php:sniff" - "ts:lint" - - "xliff:lint" - - "php:stan" php-version: - "8.2" - "8.3" @@ -118,16 +114,16 @@ jobs: fail-fast: false matrix: include: - - typo3-version: "^13.4" + - typo3-version: "^14.0" php-version: "8.2" composer-dependencies: highest - - typo3-version: "^13.4" + - typo3-version: "^14.0" php-version: "8.3" composer-dependencies: highest - - typo3-version: "^13.4" + - typo3-version: "^14.0" php-version: "8.4" composer-dependencies: highest - - typo3-version: "^13.4" + - typo3-version: "^14.0" php-version: "8.5" composer-dependencies: highest functional-tests: @@ -146,22 +142,25 @@ jobs: uses: actions/checkout@v4 - name: Install testing system - run: Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php }} -s ${{ matrix.composerInstall }} + run: Build/Scripts/runTests.sh -t 14 -b docker -p ${{ matrix.php }} -s ${{ matrix.composerInstall }} - name: Functional Tests with mariadb (min) - run: Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php }} -d mariadb -i 10.4 -s functional + run: Build/Scripts/runTests.sh -t 14 -b docker -p ${{ matrix.php }} -d mariadb -i 10.4 -s functional - name: Functional Tests with mariadb (max) - run: Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php }} -d mariadb -i 10.11 -s functional + run: Build/Scripts/runTests.sh -t 14 -b docker -p ${{ matrix.php }} -d mariadb -i 10.11 -s functional - - name: Functional Tests with mysql (min/max) - run: Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php }} -d mysql -i 8.0 -s functional + - name: Functional Tests with mysql (min) + run: Build/Scripts/runTests.sh -t 14 -b docker -p ${{ matrix.php }} -d mysql -i 8.0 -s functional + + - name: Functional Tests with mysql (max) + run: Build/Scripts/runTests.sh -t 14 -b docker -p ${{ matrix.php }} -d mysql -i 8.4 -s functional - name: Functional Tests with postgres (min) - run: Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php }} -d postgres -i 10 -s functional + run: Build/Scripts/runTests.sh -t 14 -b docker -p ${{ matrix.php }} -d postgres -i 10 -s functional - name: Functional Tests with postgres (max) - run: Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php }} -d postgres -i 16 -s functional + run: Build/Scripts/runTests.sh -t 14 -b docker -p ${{ matrix.php }} -d postgres -i 18 -s functional - name: Functional Tests with sqlite - run: Build/Scripts/runTests.sh -t 13 -p ${{ matrix.php }} -d sqlite -s functional \ No newline at end of file + run: Build/Scripts/runTests.sh -t 14 -b docker -p ${{ matrix.php }} -d sqlite -s functional \ No newline at end of file diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache index 18ae84b..1901470 100644 --- a/.php-cs-fixer.cache +++ b/.php-cs-fixer.cache @@ -1 +1 @@ -{"php":"8.2.19","version":"3.58.1:v3.58.1#04e9424025677a86914b9a4944dbbf4060bb0aff","indent":" ","lineEnding":"\n","rules":{"doctrine_annotation_array_assignment":{"operator":":"},"doctrine_annotation_braces":true,"doctrine_annotation_indentation":true,"doctrine_annotation_spaces":{"before_array_assignments_colon":false},"blank_line_after_namespace":true,"braces_position":true,"class_definition":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_import_per_statement":true,"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"visibility_required":{"elements":["method","property"]},"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"blank_line_after_opening_tag":true,"braces":{"allow_single_line_closure":true},"cast_spaces":{"space":"none"},"compact_nullable_typehint":true,"concat_space":{"spacing":"one"},"declare_equal_normalize":{"space":"none"},"dir_constant":true,"function_to_constant":{"functions":["get_called_class","get_class","get_class_this","php_sapi_name","phpversion","pi"]},"function_typehint_space":true,"lowercase_cast":true,"modernize_types_casting":true,"native_function_casing":true,"new_with_braces":true,"no_alias_functions":true,"no_blank_lines_after_phpdoc":true,"no_empty_phpdoc":true,"no_empty_statement":true,"no_extra_blank_lines":true,"no_leading_import_slash":true,"no_leading_namespace_whitespace":true,"no_null_property_initialization":true,"no_short_bool_cast":true,"no_singleline_whitespace_before_semicolons":true,"no_superfluous_elseif":true,"no_trailing_comma_in_singleline_array":true,"no_unneeded_control_parentheses":true,"no_unused_imports":true,"no_useless_else":true,"no_whitespace_in_blank_line":true,"ordered_imports":true,"php_unit_construct":{"assertions":["assertEquals","assertSame","assertNotEquals","assertNotSame"]},"php_unit_mock_short_will_return":true,"php_unit_test_case_static_method_calls":{"call_type":"self"},"phpdoc_no_access":true,"phpdoc_no_empty_return":true,"phpdoc_no_package":true,"phpdoc_scalar":true,"phpdoc_trim":true,"phpdoc_types":true,"phpdoc_types_order":{"null_adjustment":"always_last","sort_algorithm":"none"},"return_type_declaration":{"space_before":"none"},"single_quote":true,"single_line_comment_style":{"comment_types":["hash"]},"single_trait_insert_per_statement":true,"trailing_comma_in_multiline":{"elements":["arrays"]},"whitespace_after_comma_in_array":true,"yoda_style":{"equal":false,"identical":false,"less_and_greater":false}},"hashes":{"Build\/phpunit\/FunctionalTestsBootstrap.php":"67775c325d9f7d89fbe602ef26bcf693","Build\/phpunit\/UnitTestsBootstrap.php":"7ac3592092d6a62519aa070e92e7fb3e","Build\/php-cs-fixer\/php-cs-fixer.php":"c5247f77ac9eb121e6c839306717b471","public\/typo3\/index.php":"73316f95f833758b7b12a54bfde58549","public\/index.php":"018e726396942106e0a53130b1ca7f73","Resources\/Examples\/comprehend.php":"969abbfff68c1c5024ea1910fae304bd","Resources\/Examples\/detectLabels.php":"2681edcac23ce9daecd6c64635c6c6f2","Resources\/Examples\/textract.php":"b2587c1b82815df2777b46ad41d06ae4","Resources\/Examples\/transcribe.php":"2c7b0756fe96e0b66081a3e4728025d7","Resources\/Examples\/detectText.php":"fdb904131ccf872531501908dd1e95b0","Configuration\/TCA\/Overrides\/sys_file_metadata.php":"01e6c11a79497bf2498d1ffba203fa32","Classes\/EventListener\/AfterFileAddedEventListener.php":"d204b5c5734a632934ee018af8c5f679","Classes\/Service\/AwsImageRecognizeService.php":"3dbf95ae32cf04c8c4a249010cb63a93","Classes\/Event\/ModifyValidatorEvent.php":"1c2dd612c7007974713761385aad7924","Classes\/Event\/ModifyReportServiceEvent.php":"2e84bec82ebb3c55cdc7bec9efb87463","Classes\/Service\/Validator\/AbstractVideoValidatorInterface.php":"ada5a0d65b3faf4b050e88cefe297d56","Classes\/Service\/Validator\/AbstractVideoValidator.php":"4fd25532f041a013bf96df32315e1961","Classes\/Service\/Validator\/VimeoValidator.php":"862c30bb6eb7753a517403c017f254b9","Classes\/Service\/Validator\/YoutubeValidator.php":"82a67f5dfc63d2245ea9e8f7ee4843bd","Classes\/Service\/VideoService.php":"641d4e56d72e812fc564503904023f5c","Classes\/Service\/Report\/AbstractReportServiceInterface.php":"2527247ebed271988cbef24763cb84a6","Classes\/Service\/Report\/EmailReportService.php":"1ccf17cb51d59c15826dfdefef9ce99b","Classes\/Command\/ReportCommand.php":"5103d92250eef4104bf333d3a2d750e3","Classes\/Command\/ValidatorCommand.php":"da722a705ffb75b800f80047ee38b7f6","Classes\/Command\/CountCommand.php":"fcb29000733c81c6c5b644a6bd84bc49","Classes\/Command\/ResetCommand.php":"ce36df291fe516cb7eaca785f938c901","Classes\/Domain\/Repository\/FileRepository.php":"10524e697bcbb83819737eba82d84ebe","Classes\/Domain\/Dto\/ValidatorDemand.php":"dc00a5518ee96bafb1646e649d6e669a","ext_localconf.php":"8d2a7c7f05899dc182511e49c7f16ce7","ext_emconf.php":"d3cf4a36c6d0d3c3585f8f350f757128","Tests\/Functional\/Domain\/Repository\/FileRepositoryTest.php":"69687248055594486275eca929420666","Tests\/Unit\/Service\/Validator\/YoutubeValidatorTest.php":"6e762e976c4036e295a34c45c391d39d","Tests\/Unit\/Service\/Validator\/VimeoValidatorTest.php":"11daa6f2eee2e3a77325e027601b8185"}} \ No newline at end of file +{"php":"8.4.12","version":"3.92.3:v3.92.3#2ba8f5a60f6f42fb65758cfb3768434fa2d1c7e8","indent":" ","lineEnding":"\n","rules":{"doctrine_annotation_array_assignment":{"operator":":"},"doctrine_annotation_braces":true,"doctrine_annotation_indentation":true,"doctrine_annotation_spaces":{"before_array_assignments_colon":false},"blank_line_after_namespace":true,"braces_position":true,"class_definition":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"on_multiline":"ensure_fully_multiline"},"modifier_keywords":{"elements":["method","property"]},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_import_per_statement":true,"single_line_after_imports":true,"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","do","else","elseif","final","for","foreach","function","if","interface","namespace","private","protected","public","static","switch","trait","try","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"blank_line_after_opening_tag":true,"braces":{"allow_single_line_closure":true},"cast_spaces":{"space":"none"},"compact_nullable_typehint":true,"concat_space":{"spacing":"one"},"declare_equal_normalize":{"space":"none"},"dir_constant":true,"function_to_constant":{"functions":["get_called_class","get_class","get_class_this","php_sapi_name","phpversion","pi"]},"function_typehint_space":true,"lowercase_cast":true,"modernize_types_casting":true,"native_function_casing":true,"new_with_braces":true,"no_alias_functions":true,"no_blank_lines_after_phpdoc":true,"no_empty_phpdoc":true,"no_empty_statement":true,"no_extra_blank_lines":true,"no_leading_import_slash":true,"no_leading_namespace_whitespace":true,"no_null_property_initialization":true,"no_short_bool_cast":true,"no_singleline_whitespace_before_semicolons":true,"no_superfluous_elseif":true,"no_trailing_comma_in_singleline_array":true,"no_unneeded_control_parentheses":true,"no_unused_imports":true,"no_useless_else":true,"no_whitespace_in_blank_line":true,"ordered_imports":true,"php_unit_construct":{"assertions":["assertEquals","assertSame","assertNotEquals","assertNotSame"]},"php_unit_mock_short_will_return":true,"php_unit_test_case_static_method_calls":{"call_type":"self"},"phpdoc_no_access":true,"phpdoc_no_empty_return":true,"phpdoc_no_package":true,"phpdoc_scalar":true,"phpdoc_trim":true,"phpdoc_types":true,"phpdoc_types_order":{"null_adjustment":"always_last","sort_algorithm":"none"},"return_type_declaration":{"space_before":"none"},"single_quote":true,"single_line_comment_style":{"comment_types":["hash"]},"single_trait_insert_per_statement":true,"trailing_comma_in_multiline":{"elements":["arrays"]},"whitespace_after_comma_in_array":true,"yoda_style":{"equal":false,"identical":false,"less_and_greater":false}},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"Tests\/Functional\/Domain\/Repository\/FileRepositoryTest.php":"afd52a476e01298ad74c0f25a7218f14","Tests\/Unit\/Service\/Validator\/YoutubeValidatorTest.php":"d17931b49a4d5035c9adc45bcd2f74e5","Tests\/Unit\/Service\/Validator\/VimeoValidatorTest.php":"d9073a1dae9ac88d3d3c2e4a9bdb3c39","Configuration\/TCA\/Overrides\/sys_file.php":"c635b8b8082577459e5797ddc4b1804d","Classes\/Event\/ModifyValidatorEvent.php":"55a8e7fbd45dbcbfa00db9dcd723a029","Classes\/Event\/ModifyReportServiceEvent.php":"4d2aead4b3de92dec532e0fc4698644b","Classes\/Event\/ModifyVideoValidateEvent.php":"6b27a366f486127fa5d3d9709b6eee78","Classes\/Service\/Validator\/AbstractVideoValidatorInterface.php":"890eb2bc87e544fc1ae57d3cd719c217","Classes\/Service\/Validator\/AbstractVideoValidator.php":"3beb7b1857be46e0ffecbc79db941481","Classes\/Service\/Validator\/VimeoValidator.php":"95f0150a1426fd7f1a42236e5e4a27ec","Classes\/Service\/Validator\/YoutubeValidator.php":"3197ba29e84663bcbfe81fe99f87e13c","Classes\/Service\/VideoService.php":"d3850dbe21a11c92a7cc750a5a34346a","Classes\/Service\/Report\/AbstractReportServiceInterface.php":"1ef120da19c2cc9b793f18cc843a89c1","Classes\/Service\/Report\/EmailReportService.php":"43388cb5694d5192c3b4f993a1c35533","Classes\/Command\/ReportCommand.php":"6a29a514c1066dea77856bf2d34fb6b1","Classes\/Command\/ValidatorCommand.php":"703e0f7274c6af81cd326a6b5406fbbd","Classes\/Command\/CountCommand.php":"318e8b2812758ed00629ab1b7f8913db","Classes\/Command\/ResetCommand.php":"b3ccad993c2a16f738c8ff86aa9c0a35","Classes\/Domain\/Repository\/FileRepository.php":"6674ca65a0645f0151531f257700a52b","Classes\/Domain\/Dto\/ValidatorDemand.php":"88ad373f1cb9a7ed91a0b2eb76fed8c7","ext_localconf.php":"ed7309ecf1f88f4a8005de0998bea2aa","Build\/phpunit\/FunctionalTestsBootstrap.php":"ec9308ce859d87456317df00c67bc9f1","Build\/phpunit\/UnitTestsBootstrap.php":"0ee630571c7a6461d6c35e7586b1aab3","Build\/php-cs-fixer\/php-cs-fixer.php":"e96029c84b5a51b9473a722c389f3aee","ext_emconf.php":"770bb3104a265772b83ee83b2b6b60bf"}} \ No newline at end of file diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh index 05d79a0..58d4508 100755 --- a/Build/Scripts/runTests.sh +++ b/Build/Scripts/runTests.sh @@ -45,8 +45,8 @@ handleDbmsOptions() { echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 exit 1 fi - [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="10.2" - if ! [[ ${DBMS_VERSION} =~ ^(10.2|10.3|10.4|10.5|10.6|10.7|10.8|10.9|10.10|10.11|11.0|11.1)$ ]]; then + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="10.11" + if ! [[ ${DBMS_VERSION} =~ ^(10.4|10.5|10.6|10.7|10.8|10.9|10.10|10.11|11.0|11.1|11.4|11.8)$ ]]; 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 @@ -61,8 +61,8 @@ handleDbmsOptions() { echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 exit 1 fi - [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="5.5" - if ! [[ ${DBMS_VERSION} =~ ^(5.5|5.6|5.7|8.0)$ ]]; then + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="8.4" + if ! [[ ${DBMS_VERSION} =~ ^(8.0|8.1|8.2|8.3|8.4|9.5)$ ]]; 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 @@ -76,8 +76,8 @@ handleDbmsOptions() { 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 + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="16" + if ! [[ ${DBMS_VERSION} =~ ^(10|11|12|13|14|15|16|17|18)$ ]]; 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 @@ -151,7 +151,7 @@ restoreComposerFiles() { loadHelp() { # Load help text into $HELP - read -r -d '' HELP < + -t <13> Only with -s composerInstall|composerInstallMin|composerInstallMax Specifies the TYPO3 CORE Version to be used - - 11.5: use TYPO3 v11 (default) - - 12.4: use TYPO3 v12 - - 13.4: use TYPO3 v13 + - 13.4: use TYPO3 v13 (default) + - 14.0: use TYPO3 v14 - -p <7.4|8.0|8.1|8.2|8.3|8.5> + -p <8.2|8.3|8.4|8.5> Specifies the PHP minor version to be used - - 7.4: use PHP 7.4 (default) - - 8.0: use PHP 8.0 - - 8.1: use PHP 8.1 - 8.2: use PHP 8.2 - - 8.3: use PHP 8.3 + - 8.3: use PHP 8.3 (default) - 8.4: use PHP 8.4 - 8.5: use PHP 8.5 @@ -278,24 +274,24 @@ Options: Show this help. Examples: - # Run all core unit tests using PHP 7.4 + # Run all core unit tests using PHP 8.5 ./Build/Scripts/runTests.sh -s unit # Run all core units tests and enable xdebug (have a PhpStorm listening on port 9003!) ./Build/Scripts/runTests.sh -x -s unit - # Run unit tests in phpunit verbose mode with xdebug on PHP 8.1 and filter for test canRetrieveValueWithGP - ./Build/Scripts/runTests.sh -x -p 8.1 -- --filter 'classCanBeRegistered' + # Run unit tests in phpunit verbose mode with xdebug on PHP 8.3 and filter for test canRetrieveValueWithGP + ./Build/Scripts/runTests.sh -x -p 8.3 -- --filter 'classCanBeRegistered' # Run functional tests in phpunit with a filtered test method name in a specified file # example will currently execute two tests, both of which start with the search term ./Build/Scripts/runTests.sh -s functional -- --filter 'findRecordByImportSource' Tests/Functional/Repository/CategoryRepositoryTest.php - # Run functional tests on postgres with xdebug, php 8.1 and execute a restricted set of tests - ./Build/Scripts/runTests.sh -x -p 8.1 -s functional -d postgres -- Tests/Functional/Repository/CategoryRepositoryTest.php + # 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/Repository/CategoryRepositoryTest.php - # Run functional tests on postgres 11 - ./Build/Scripts/runTests.sh -s functional -d postgres -i 11 + # Run functional tests on postgres 16 + ./Build/Scripts/runTests.sh -s functional -d postgres -i 16 EOF } @@ -314,17 +310,17 @@ ROOT_DIR="${PWD}" # Option defaults TEST_SUITE="" -TYPO3_VERSION="11" +TYPO3_VERSION="14" DBMS="sqlite" DBMS_VERSION="" -PHP_VERSION="8.1" +PHP_VERSION="8.3" PHP_XDEBUG_ON=0 PHP_XDEBUG_PORT=9003 EXTRA_TEST_OPTIONS="" CGLCHECK_DRY_RUN=0 DATABASE_DRIVER="" CONTAINER_BIN="" -COMPOSER_ROOT_VERSION="12.4.2" +COMPOSER_ROOT_VERSION="14.0.1" CONTAINER_INTERACTIVE="-it --init" HOST_UID=$(id -u) HOST_PID=$(id -g) @@ -364,7 +360,7 @@ while getopts "a:b:s:d:i:p:e:t:xy:nhu" OPT; do ;; p) PHP_VERSION=${OPTARG} - if ! [[ ${PHP_VERSION} =~ ^(7.4|8.0|8.1|8.2|8.3|8.4|8.5)$ ]]; then + if ! [[ ${PHP_VERSION} =~ ^(8.2|8.3|8.4|8.5)$ ]]; then INVALID_OPTIONS+=("-p ${OPTARG}") fi ;; @@ -373,7 +369,7 @@ while getopts "a:b:s:d:i:p:e:t:xy:nhu" OPT; do ;; t) TYPO3_VERSION=${OPTARG} - if ! [[ ${TYPO3_VERSION} =~ ^(11|12|13)$ ]]; then + if ! [[ ${TYPO3_VERSION} =~ ^(14)$ ]]; then INVALID_OPTIONS+=("-t ${OPTARG}") fi ;; @@ -446,7 +442,7 @@ mkdir -p .cache mkdir -p .Build/public/typo3temp/var/tests IMAGE_PHP="ghcr.io/typo3/core-testing-$(echo "php${PHP_VERSION}" | sed -e 's/\.//'):latest" -IMAGE_ALPINE="docker.io/alpine:3.8" +IMAGE_ALPINE="docker.io/alpine:3.22" IMAGE_DOCS="ghcr.io/typo3-documentation/render-guides:latest" IMAGE_MARIADB="docker.io/mariadb:${DBMS_VERSION}" IMAGE_MYSQL="docker.io/mysql:${DBMS_VERSION}" @@ -511,17 +507,9 @@ case ${TEST_SUITE} in 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 + if [ ${TYPO3_VERSION} -eq 14 ]; then composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^11.5.24 || exit 1 - fi - if [ ${TYPO3_VERSION} -eq 12 ]; then - composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^12.4.2 || exit 1 - fi - if [ ${TYPO3_VERSION} -eq 13 ]; then - composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^13.4 || exit 1 + typo3/cms-core:^14.0 || exit 1 fi composer update --no-progress --no-interaction || exit 1 composer show || exit 1 @@ -533,17 +521,9 @@ case ${TEST_SUITE} in 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.24 || exit 1 - fi - if [ ${TYPO3_VERSION} -eq 12 ]; then - composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^12.4.2 || exit 1 - fi - if [ ${TYPO3_VERSION} -eq 13 ]; then + if [ ${TYPO3_VERSION} -eq 14 ]; then composer require --no-ansi --no-interaction --no-progress --no-install \ - typo3/cms-core:^13.4 || exit 1 + typo3/cms-core:^14.0 || exit 1 fi composer update --no-ansi --no-interaction --no-progress --with-dependencies --prefer-lowest || exit 1 composer show || exit 1 @@ -583,8 +563,11 @@ case ${TEST_SUITE} in 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 + POSTGRES_TMPFS="/var/lib/postgresql/data" + if [[ ${DBMS_VERSION} -eq 18 ]]; then + POSTGRES_TMPFS="/var/lib/postgresql" + fi + ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name postgres-func-${SUFFIX} --network ${NETWORK} -d -e POSTGRES_PASSWORD=funcp -e POSTGRES_USER=funcu --tmpfs ${POSTGRES_TMPFS}:rw,noexec,nosuid ${IMAGE_POSTGRES} >/dev/null 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=$? diff --git a/Build/phpunit/FunctionalTestsBootstrap.php b/Build/phpunit/FunctionalTestsBootstrap.php index a95bc52..9882f8f 100644 --- a/Build/phpunit/FunctionalTestsBootstrap.php +++ b/Build/phpunit/FunctionalTestsBootstrap.php @@ -1,4 +1,5 @@ setDescription('Counts all videos of a media extension, e.g. YouTube'); $this->addOption( 'extension', null, diff --git a/Classes/Command/ReportCommand.php b/Classes/Command/ReportCommand.php index e67834a..254225a 100644 --- a/Classes/Command/ReportCommand.php +++ b/Classes/Command/ReportCommand.php @@ -10,21 +10,26 @@ use Ayacoo\VideoValidator\Service\Report\EmailReportService; use Ayacoo\VideoValidator\Service\VideoService; use Psr\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Messaging\FlashMessage; +use TYPO3\CMS\Core\Messaging\FlashMessageService; use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException; use TYPO3\CMS\Core\Resource\ResourceFactory; +use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +#[AsCommand('videoValidator:report', 'Send report of video validation for a defined media extension (e.g. YouTube)')] class ReportCommand extends Command { protected function configure(): void { - $this->setDescription('Send report of video validation for a defined media extension (e.g. YouTube)'); $this->addOption( 'recipients', null, @@ -66,7 +71,8 @@ public function __construct( protected ResourceFactory $resourceFactory, protected FileRepository $fileRepository, protected LocalizationUtility $localizationUtility, - protected EventDispatcherInterface $eventDispatcher + protected EventDispatcherInterface $eventDispatcher, + protected FlashMessageService $flashMessageService ) { parent::__construct(); } @@ -86,9 +92,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $sender = trim($GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] ?? ''); if ($sender === '') { - $io->warning( - $this->localizationUtility::translate('report.validMailAddress', 'video_validator') - ); + $message = $this->localizationUtility::translate('report.validMailAddress', 'video_validator'); + $io->warning($message); + $this->addFlashMessage($message, ContextualFeedbackSeverity::WARNING); return 0; } @@ -122,19 +128,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->localizationUtility::translate('report.status.success', 'video_validator') ); } else { - $io->warning( - $this->localizationUtility::translate('report.status.noVideos', 'video_validator') - ); + $message = $this->localizationUtility::translate('report.status.noVideos', 'video_validator'); + $io->warning($message); + $this->addFlashMessage($message, ContextualFeedbackSeverity::WARNING); } } else { - $io->warning( - $this->localizationUtility::translate('report.status.disallowedExtension', 'video_validator') - ); + $message = $this->localizationUtility::translate('report.status.disallowedExtension', 'video_validator'); + $io->warning($message); + $this->addFlashMessage($message, ContextualFeedbackSeverity::WARNING); } return Command::SUCCESS; } + protected function addFlashMessage(string $message, ContextualFeedbackSeverity $type): void + { + if (!Environment::isCli()) { + $flashMessage = GeneralUtility::makeInstance( + FlashMessage::class, + $message, + '', + $type + ); + $this->flashMessageService->getMessageQueueByIdentifier()->enqueue($flashMessage); + } + } + protected function getVideosByStatus(ValidatorDemand $validatorDemand, int $status): array { $videos = []; diff --git a/Classes/Command/ResetCommand.php b/Classes/Command/ResetCommand.php index 5bb063b..ffc733c 100644 --- a/Classes/Command/ResetCommand.php +++ b/Classes/Command/ResetCommand.php @@ -5,6 +5,7 @@ namespace Ayacoo\VideoValidator\Command; use Ayacoo\VideoValidator\Domain\Repository\FileRepository; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -12,11 +13,11 @@ use Symfony\Component\Console\Style\SymfonyStyle; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +#[AsCommand('videoValidator:reset', 'Resets all videos of a media extension, e.g. YouTube')] class ResetCommand extends Command { protected function configure(): void { - $this->setDescription('Resets all videos of a media extension, e.g. YouTube'); $this->addOption( 'extension', null, diff --git a/Classes/Command/ValidatorCommand.php b/Classes/Command/ValidatorCommand.php index c55c939..e7091c4 100644 --- a/Classes/Command/ValidatorCommand.php +++ b/Classes/Command/ValidatorCommand.php @@ -6,6 +6,7 @@ use Ayacoo\VideoValidator\Domain\Dto\ValidatorDemand; use Ayacoo\VideoValidator\Service\VideoService; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -13,11 +14,11 @@ use Symfony\Component\Console\Style\SymfonyStyle; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +#[AsCommand('videoValidator:validate', 'Video validation of a defined media extension (e.g. YouTube)')] class ValidatorCommand extends Command { protected function configure(): void { - $this->setDescription('Video validation of a defined media extension (e.g. YouTube)'); $this->addOption( 'extension', null, diff --git a/Classes/Controller/Backend/VideoOverviewController.php b/Classes/Controller/Backend/VideoOverviewController.php new file mode 100644 index 0000000..5072e79 --- /dev/null +++ b/Classes/Controller/Backend/VideoOverviewController.php @@ -0,0 +1,231 @@ + Known icon identifiers per media extension */ + private const EXTENSION_ICONS = [ + 'youtube' => 'mimetypes-media-video-youtube', + 'vimeo' => 'mimetypes-media-video-vimeo', + ]; + + /** + * @param ModuleTemplateFactory $moduleTemplateFactory + * @param UriBuilder $uriBuilder + * @param EventDispatcherInterface $eventDispatcher + * @param FileRepository $fileRepository + * @param ResourceFactory $resourceFactory + */ + public function __construct( + private readonly ModuleTemplateFactory $moduleTemplateFactory, + private readonly UriBuilder $uriBuilder, + private readonly EventDispatcherInterface $eventDispatcher, + private readonly FileRepository $fileRepository, + private readonly ResourceFactory $resourceFactory, + private readonly VideoService $videoService, + ) { + } + + public function handleRequest(ServerRequestInterface $request): ResponseInterface + { + $queryParams = $request->getQueryParams(); + $extensionFilter = strtolower((string)($queryParams['extension'] ?? '')); + $search = trim((string)($queryParams['search'] ?? '')); + $currentPage = max(1, (int)($queryParams['page'] ?? 1)); + $rawStatus = (int)($queryParams['status'] ?? -1); + $statusFilter = in_array( + $rawStatus, + [ + -1, + VideoService::STATUS_SUCCESS, + VideoService::STATUS_ERROR, + VideoService::STATUS_SKIP + ], + true + ) ? $rawStatus : -1; + + ['extensions' => $supportedExtensions, 'iconMap' => $iconMap] = $this->resolveSupportedExtensions(); + + if (!in_array($extensionFilter, $supportedExtensions, true)) { + $extensionFilter = ''; + } + + $extensions = $extensionFilter !== '' ? [$extensionFilter] : $supportedExtensions; + + $storageRestrictions = $this->resolveStorageRestrictions(); + $allRawVideos = $this->fileRepository->findVideosForModule( + $extensions, + $search, + $statusFilter, + $storageRestrictions + ); + + $paginator = new ArrayPaginator($allRawVideos, $currentPage, self::ITEMS_PER_PAGE); + $pagination = new SimplePagination($paginator); + + $videos = $this->enrichVideos((array)$paginator->getPaginatedItems(), $iconMap); + + $moduleBaseUri = (string)$this->uriBuilder->buildUriFromRoute('file_videovalidator'); + $paginationUris = $this->buildPaginationUris( + $extensionFilter, + $search, + $statusFilter, + (int)$paginator->getCurrentPageNumber(), + (int)$pagination->getPreviousPageNumber(), + (int)$pagination->getNextPageNumber(), + (int)$pagination->getLastPageNumber(), + ); + + $moduleTemplate = $this->moduleTemplateFactory->create($request); + $moduleTemplate->assignMultiple([ + 'videos' => $videos, + 'paginator' => $paginator, + 'pagination' => $pagination, + 'totalCount' => count($allRawVideos), + 'itemsPerPage' => self::ITEMS_PER_PAGE, + 'extensionFilter' => $extensionFilter, + 'search' => $search, + 'statusFilter' => $statusFilter, + 'availableExtensions' => $supportedExtensions, + 'statusError' => VideoService::STATUS_ERROR, + 'moduleBaseUri' => $moduleBaseUri, + 'paginationUris' => $paginationUris, + ]); + + return $moduleTemplate->renderResponse('Overview'); + } + + /** + * Returns per-storage path restrictions for the current BE user. + * Admins get an empty array (= no restriction). + * Regular users get a map of storageUid => mount paths; path "/" means full storage access. + * + * @return array + */ + private function resolveStorageRestrictions(): array + { + $backendUser = $GLOBALS['BE_USER'] ?? null; + if ($backendUser === null || $backendUser->isAdmin()) { + return []; + } + + $restrictions = []; + foreach ($backendUser->getFileStorages() as $storage) { + $paths = []; + foreach ($storage->getFileMounts() as $mount) { + $paths[] = (string)($mount['path'] ?? '/'); + } + $restrictions[$storage->getUid()] = $paths ?: ['/']; + } + + return $restrictions; + } + + /** + * @return array{extensions: string[], iconMap: array} + */ + private function resolveSupportedExtensions(): array + { + $allowedExtensions = array_keys( + $GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['onlineMediaHelpers'] ?? [] + ); + + $event = new ModifyVideoOverviewExtensionsEvent( + self::DEFAULT_EXTENSIONS, + $allowedExtensions, + self::EXTENSION_ICONS, + ); + + /** @var ModifyVideoOverviewExtensionsEvent $event */ + $event = $this->eventDispatcher->dispatch($event); + + return [ + 'extensions' => $event->getExtensions(), + 'iconMap' => $event->getIconMap(), + ]; + } + + /** + * @return array + * @throws RouteNotFoundException + */ + private function buildPaginationUris( + string $extensionFilter, + string $search, + int $statusFilter, + int $currentPage, + int $previousPage, + int $nextPage, + int $lastPage, + ): array { + $build = function (int $page) use ($extensionFilter, $search, $statusFilter): string { + return (string)$this->uriBuilder->buildUriFromRoute('file_videovalidator', [ + 'extension' => $extensionFilter, + 'search' => $search, + 'status' => $statusFilter, + 'page' => $page, + ]); + }; + + return [ + 'first' => $build(1), + 'previous' => $previousPage > 0 ? $build($previousPage) : '', + 'next' => $nextPage > 0 ? $build($nextPage) : '', + 'last' => $build(max(1, $lastPage)), + 'hasPrevious' => $currentPage > 1, + 'hasNext' => $currentPage < max(1, $lastPage), + ]; + } + + /** + * @param array> $rawVideos + * @param array $iconMap extension => TYPO3 icon identifier + * @return array> + */ + private function enrichVideos(array $rawVideos, array $iconMap = []): array + { + $hasValidatorCache = []; + foreach ($rawVideos as $key => $row) { + $extension = strtolower((string)($row['extension'] ?? '')); + try { + $file = $this->resourceFactory->getFileObject((int)$row['uid']); + $publicUrl = $file->getPublicUrl(); + } catch (FileDoesNotExistException) { + // ignore — file may have been removed meanwhile + $publicUrl = ''; + } + if (!array_key_exists($extension, $hasValidatorCache)) { + $hasValidatorCache[$extension] = $extension !== '' + && $this->videoService->hasValidator($extension); + } + $rawVideos[$key]['public_url'] = $publicUrl; + $rawVideos[$key]['is_invalid'] = (int)($row['validation_status'] ?? 0) === VideoService::STATUS_ERROR; + $rawVideos[$key]['icon_identifier'] = $iconMap[$extension] ?? 'mimetypes-media-video'; + $rawVideos[$key]['has_validator'] = $hasValidatorCache[$extension]; + } + + return $rawVideos; + } +} diff --git a/Classes/Controller/Backend/VideoRefreshAjaxController.php b/Classes/Controller/Backend/VideoRefreshAjaxController.php new file mode 100644 index 0000000..1a74349 --- /dev/null +++ b/Classes/Controller/Backend/VideoRefreshAjaxController.php @@ -0,0 +1,57 @@ +getParsedBody() ?? []; + $fileUid = (int)($parsedBody['fileUid'] ?? 0); + if ($fileUid <= 0) { + return new JsonResponse(['success' => false, 'message' => 'Invalid file uid'], 400); + } + + // Access check: reuse BE user's storage/filemount access + try { + $file = $this->resourceFactory->getFileObject($fileUid); + } catch (FileDoesNotExistException) { + return new JsonResponse(['success' => false, 'message' => 'File not found'], 404); + } + + $backendUser = $GLOBALS['BE_USER'] ?? null; + if ($backendUser === null) { + return new JsonResponse(['success' => false, 'message' => 'Not authenticated'], 401); + } + if (!$backendUser->isAdmin() && !isset($backendUser->getFileStorages()[$file->getStorage()->getUid()])) { + return new JsonResponse(['success' => false, 'message' => 'Access denied'], 403); + } + + try { + $status = $this->videoService->validateFile($fileUid); + } catch (\Throwable $e) { + return new JsonResponse(['success' => false, 'message' => $e->getMessage()], 500); + } + + return new JsonResponse(['success' => true, 'status' => $status]); + } +} diff --git a/Classes/Domain/Repository/FileRepository.php b/Classes/Domain/Repository/FileRepository.php index cd5fb97..f23314d 100644 --- a/Classes/Domain/Repository/FileRepository.php +++ b/Classes/Domain/Repository/FileRepository.php @@ -5,9 +5,11 @@ namespace Ayacoo\VideoValidator\Domain\Repository; use Ayacoo\VideoValidator\Domain\Dto\ValidatorDemand; +use Doctrine\DBAL\Exception; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Platform\PlatformInformation; +use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression; use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -129,6 +131,174 @@ public function resetValidationState(string $extension): void $queryBuilder->executeStatement(); } + /** + * Fetch videos (YouTube/Vimeo) for the backend overview module. + * Joins sys_file_metadata so the resulting rows carry the metadata uid for edit links. + * + * @param string[] $extensions lowercase sys_file.extension values to include + * @param string $search case-insensitive title substring + * @param array $storageRestrictions storageUid => mount paths; empty = no restriction (admin) + * @return array> + * @throws Exception + */ + public function findVideosForModule( + array $extensions, + string $search, + int $statusFilter = -1, + array $storageRestrictions = [] + ): array { + $queryBuilder = $this->getQueryBuilder(self::SYS_FILE_TABLE); + + $statement = $queryBuilder + ->select( + self::SYS_FILE_TABLE . '.uid', + self::SYS_FILE_TABLE . '.extension', + self::SYS_FILE_TABLE . '.identifier', + self::SYS_FILE_TABLE . '.validation_status', + self::SYS_FILE_TABLE . '.validation_date', + 'sys_file_metadata.uid AS metadata_uid', + 'sys_file_metadata.title AS title', + ) + ->from(self::SYS_FILE_TABLE) + ->leftJoin( + self::SYS_FILE_TABLE, + 'sys_file_metadata', + 'sys_file_metadata', + $queryBuilder->expr()->eq( + 'sys_file_metadata.file', + $queryBuilder->quoteIdentifier(self::SYS_FILE_TABLE . '.uid') + ) + ) + ->where( + ...$this->buildModuleConstraints( + $queryBuilder, + $extensions, + $search, + $statusFilter, + $storageRestrictions + ) + ) + ->orderBy(self::SYS_FILE_TABLE . '.uid', 'DESC'); + + return $statement->executeQuery()->fetchAllAssociative(); + } + + /** + * @param string[] $extensions + * @throws Exception + */ + public function countVideosForModule(array $extensions, string $search): int + { + $queryBuilder = $this->getQueryBuilder(self::SYS_FILE_TABLE); + + $statement = $queryBuilder + ->count(self::SYS_FILE_TABLE . '.uid') + ->from(self::SYS_FILE_TABLE) + ->leftJoin( + self::SYS_FILE_TABLE, + 'sys_file_metadata', + 'sys_file_metadata', + $queryBuilder->expr()->eq( + 'sys_file_metadata.file', + $queryBuilder->quoteIdentifier(self::SYS_FILE_TABLE . '.uid') + ) + ) + ->where(...$this->buildModuleConstraints($queryBuilder, $extensions, $search)); + + return (int)$statement->executeQuery()->fetchOne(); + } + + /** + * @param string[] $extensions + * @param array $storageRestrictions storageUid => mount paths; empty = no restriction + * @return array + */ + protected function buildModuleConstraints( + QueryBuilder $queryBuilder, + array $extensions, + string $search, + int $statusFilter = -1, + array $storageRestrictions = [], + ): array { + $constraints = []; + + $normalizedExtensions = array_values(array_filter(array_map('strtolower', $extensions))); + if ($normalizedExtensions !== []) { + $constraints[] = $queryBuilder->expr()->in( + self::SYS_FILE_TABLE . '.extension', + $queryBuilder->createNamedParameter($normalizedExtensions, Connection::PARAM_STR_ARRAY) + ); + } + + $constraints[] = $queryBuilder->expr()->eq( + self::SYS_FILE_TABLE . '.missing', + $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) + ); + + if ($statusFilter >= 0) { + $constraints[] = $queryBuilder->expr()->eq( + self::SYS_FILE_TABLE . '.validation_status', + $queryBuilder->createNamedParameter($statusFilter, Connection::PARAM_INT) + ); + } + + $search = trim($search); + if ($search !== '') { + $constraints[] = $queryBuilder->expr()->like( + 'sys_file_metadata.title', + $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards($search) . '%') + ); + } + + if ($storageRestrictions !== []) { + $constraints[] = $this->buildStorageConstraint($queryBuilder, $storageRestrictions); + } + + return $constraints; + } + + /** + * Builds an OR constraint covering all accessible storages and their file mount paths. + * A path of "/" means the user has access to the entire storage (no path filter needed). + * + * @param array $storageRestrictions storageUid => mount paths + */ + protected function buildStorageConstraint( + QueryBuilder $queryBuilder, + array $storageRestrictions + ): CompositeExpression { + $storageConstraints = []; + + foreach ($storageRestrictions as $storageUid => $paths) { + $storageEq = $queryBuilder->expr()->eq( + self::SYS_FILE_TABLE . '.storage', + $queryBuilder->createNamedParameter($storageUid, Connection::PARAM_INT) + ); + + $hasRootMount = in_array('/', $paths, true); + if ($hasRootMount || $paths === []) { + // Full storage access — no path restriction needed + $storageConstraints[] = $storageEq; + continue; + } + + $pathConstraints = []; + foreach ($paths as $path) { + $pathConstraints[] = $queryBuilder->expr()->like( + self::SYS_FILE_TABLE . '.identifier', + $queryBuilder->createNamedParameter($queryBuilder->escapeLikeWildcards($path) . '%') + ); + } + + $storageConstraints[] = $queryBuilder->expr()->and( + $storageEq, + $queryBuilder->expr()->or(...$pathConstraints) + ); + } + + return $queryBuilder->expr()->or(...$storageConstraints); + } + public function updatePropertiesByFile(int $fileUid, array $properties = []): void { $queryBuilder = $this->getQueryBuilder(self::SYS_FILE_TABLE); diff --git a/Classes/Event/ModifyVideoOverviewExtensionsEvent.php b/Classes/Event/ModifyVideoOverviewExtensionsEvent.php new file mode 100644 index 0000000..ac5c3ce --- /dev/null +++ b/Classes/Event/ModifyVideoOverviewExtensionsEvent.php @@ -0,0 +1,73 @@ + $iconMap Map of extension key => TYPO3 icon identifier + */ + public function __construct( + private array $extensions, + private readonly array $allowedExtensions, + private array $iconMap = [], + ) { + } + + /** + * @return string[] + */ + public function getExtensions(): array + { + return $this->extensions; + } + + /** + * Extensions registered in $GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['onlineMediaHelpers']. + * + * @return string[] + */ + public function getAllowedExtensions(): array + { + return $this->allowedExtensions; + } + + /** + * Returns a map of extension key => TYPO3 icon identifier for all registered extensions. + * + * @return array + */ + public function getIconMap(): array + { + return $this->iconMap; + } + + /** + * Add a media extension to the overview. + * + * @param string $extension Lowercase extension key (e.g. "tiktok") + * @param string $iconIdentifier TYPO3 icon identifier; defaults to generic video icon + * + * Silently ignored if the extension is not registered as an onlineMediaHelper. + */ + public function addExtension(string $extension, string $iconIdentifier = 'mimetypes-media-video'): void + { + $extension = strtolower($extension); + if (!in_array($extension, $this->allowedExtensions, true)) { + return; + } + if (!in_array($extension, $this->extensions, true)) { + $this->extensions[] = $extension; + } + $this->iconMap[$extension] = $iconIdentifier; + } +} diff --git a/Classes/Service/Validator/VimeoValidator.php b/Classes/Service/Validator/VimeoValidator.php index c05e62c..37ceb80 100644 --- a/Classes/Service/Validator/VimeoValidator.php +++ b/Classes/Service/Validator/VimeoValidator.php @@ -4,10 +4,12 @@ namespace Ayacoo\VideoValidator\Service\Validator; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\VimeoHelper; use TYPO3\CMS\Core\Utility\GeneralUtility; +#[AutoconfigureTag('video_validator.validator', ['extension' => 'vimeo'])] class VimeoValidator extends AbstractVideoValidator { private VimeoHelper $vimeoHelper; diff --git a/Classes/Service/Validator/YoutubeValidator.php b/Classes/Service/Validator/YoutubeValidator.php index 344520e..e9d2e6e 100644 --- a/Classes/Service/Validator/YoutubeValidator.php +++ b/Classes/Service/Validator/YoutubeValidator.php @@ -4,10 +4,12 @@ namespace Ayacoo\VideoValidator\Service\Validator; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\YouTubeHelper; use TYPO3\CMS\Core\Utility\GeneralUtility; +#[AutoconfigureTag('video_validator.validator', ['extension' => 'youtube'])] class YoutubeValidator extends AbstractVideoValidator { private YouTubeHelper $youtubeHelper; diff --git a/Classes/Service/VideoService.php b/Classes/Service/VideoService.php index a0058b4..2ed7da4 100644 --- a/Classes/Service/VideoService.php +++ b/Classes/Service/VideoService.php @@ -9,12 +9,11 @@ use Ayacoo\VideoValidator\Event\ModifyValidatorEvent; use Ayacoo\VideoValidator\Event\ModifyVideoValidateEvent; use Ayacoo\VideoValidator\Service\Validator\AbstractVideoValidatorInterface; -use Ayacoo\VideoValidator\Service\Validator\VimeoValidator; -use Ayacoo\VideoValidator\Service\Validator\YoutubeValidator; use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; +use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException; use TYPO3\CMS\Core\Resource\ResourceFactory; -use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; class VideoService @@ -30,6 +29,8 @@ public function __construct( private readonly FileRepository $fileRepository, private readonly ResourceFactory $resourceFactory, private readonly LocalizationUtility $localizationUtility, + #[AutowireIterator('video_validator.validator', indexAttribute: 'extension')] + private readonly iterable $validator, private ?SymfonyStyle $io = null, ) { } @@ -44,6 +45,57 @@ public function setIo(?SymfonyStyle $io): void $this->io = $io; } + /** + * Returns whether a validator is registered for the given file extension. + * Takes ModifyValidatorEvent into account, so extensions that plug in + * validators dynamically are also detected. + */ + public function hasValidator(string $extension): bool + { + $demand = new ValidatorDemand(); + $demand->setExtension($extension); + return $this->getValidator($demand) !== null; + } + + /** + * Validates a single file by its uid and persists the result. + * Used by the backend "Refresh status" action. Reuses the existing validator lookup, + * file repository, and ModifyVideoValidateEvent dispatch — no CLI I/O. + * + * @param int $fileUid + * @return int One of STATUS_SUCCESS, STATUS_ERROR + * @throws FileDoesNotExistException|\RuntimeException + */ + public function validateFile(int $fileUid): int + { + $file = $this->resourceFactory->getFileObject($fileUid); + + $demand = new ValidatorDemand(); + $demand->setExtension(strtolower($file->getExtension())); + $validator = $this->getValidator($demand); + if ($validator === null) { + throw new \RuntimeException( + sprintf('No validator registered for extension "%s"', $file->getExtension()) + ); + } + + $mediaId = $validator->getOnlineMediaId($file) ?? ''; + if ($mediaId === '' || !$validator->isVideoOnline($mediaId)) { + $status = self::STATUS_ERROR; + } else { + $status = self::STATUS_SUCCESS; + } + + $properties = [ + 'validation_status' => $status, + 'validation_date' => time(), + ]; + $this->fileRepository->updatePropertiesByFile($fileUid, $properties); + $this->eventDispatcher->dispatch(new ModifyVideoValidateEvent($file, $properties)); + + return $status; + } + public function validate(ValidatorDemand $validatorDemand): void { $validator = $this->getValidator($validatorDemand); @@ -152,10 +204,13 @@ public function validate(ValidatorDemand $validatorDemand): void protected function getValidator(ValidatorDemand $validatorDemand): ?AbstractVideoValidatorInterface { $extension = strtolower($validatorDemand->getExtension()); - $validator = match ($extension) { - 'youtube' => GeneralUtility::makeInstance(YoutubeValidator::class, $extension), - 'vimeo' => GeneralUtility::makeInstance(VimeoValidator::class, $extension) - }; + $validator = null; + foreach ($this->validator as $validatorKey => $currentValidator) { + if ($validatorKey === $extension) { + $validator = $currentValidator; + break; + } + } $modifyValidatorEvent = $this->eventDispatcher->dispatch( new ModifyValidatorEvent($validator, $extension) diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php new file mode 100644 index 0000000..9bdcf40 --- /dev/null +++ b/Configuration/Backend/AjaxRoutes.php @@ -0,0 +1,11 @@ + [ + 'path' => '/videovalidator/refresh', + 'target' => VideoRefreshAjaxController::class . '::refresh', + 'access' => 'user', + ], +]; diff --git a/Configuration/Backend/Modules.php b/Configuration/Backend/Modules.php new file mode 100644 index 0000000..558d6c7 --- /dev/null +++ b/Configuration/Backend/Modules.php @@ -0,0 +1,23 @@ + [ + 'parent' => 'file', + 'access' => 'user', + 'path' => '/module/file/videovalidator', + 'iconIdentifier' => 'module-video-validator', + 'labels' => 'LLL:EXT:video_validator/Resources/Private/Language/locallang_mod.xlf', + 'extensionName' => 'VideoValidator', + 'inheritNavigationComponentFromMainModule' => false, + 'navigationComponentId' => '', + 'routes' => [ + '_default' => [ + 'target' => VideoOverviewController::class . '::handleRequest', + ], + ], + ], +]; diff --git a/Configuration/Icons.php b/Configuration/Icons.php new file mode 100644 index 0000000..4a67f6d --- /dev/null +++ b/Configuration/Icons.php @@ -0,0 +1,12 @@ + [ + 'provider' => SvgIconProvider::class, + 'source' => 'EXT:video_validator/Resources/Public/Icons/Extension.svg', + ], +]; diff --git a/Configuration/JavaScriptModules.php b/Configuration/JavaScriptModules.php new file mode 100644 index 0000000..09dda53 --- /dev/null +++ b/Configuration/JavaScriptModules.php @@ -0,0 +1,11 @@ + ['backend'], + 'tags' => ['backend.module'], + 'imports' => [ + '@ayacoo/video-validator/' => 'EXT:video_validator/Resources/Public/JavaScript/', + ], +]; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 78c14b9..e3efe15 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -9,31 +9,4 @@ services: resource: '../Classes/*' exclude: - '../Classes/Domain/Model/*' - - Ayacoo\VideoValidator\Command\ValidatorCommand: - tags: - - name: 'console.command' - command: 'videoValidator:validate' - description: 'Video validation of a defined media extension (e.g. YouTube)' - schedulable: true - - Ayacoo\VideoValidator\Command\ReportCommand: - tags: - - name: 'console.command' - command: 'videoValidator:report' - description: 'Send report of video validation for a defined media extension (e.g. YouTube)' - schedulable: true - - Ayacoo\VideoValidator\Command\ResetCommand: - tags: - - name: 'console.command' - command: 'videoValidator:reset' - description: 'Resets all videos of a media extension, e.g. YouTube' - schedulable: true - - Ayacoo\VideoValidator\Command\CountCommand: - tags: - - name: 'console.command' - command: 'videoValidator:count' - description: 'Counts all videos of a media extension, e.g. YouTube' - schedulable: false + - '../Classes/Event/*' \ No newline at end of file diff --git a/Configuration/TCA/Overrides/sys_file.php b/Configuration/TCA/Overrides/sys_file.php new file mode 100644 index 0000000..aab026b --- /dev/null +++ b/Configuration/TCA/Overrides/sys_file.php @@ -0,0 +1,27 @@ + [ + 'exclude' => true, + 'label' => 'Validation Date', + 'config' => [ + 'type' => 'datetime', + 'default' => 0, + ], + ], + 'validation_status' => [ + 'exclude' => true, + 'label' => 'Validation Status', + 'config' => [ + 'type' => 'number', + 'default' => 0, + ], + ], +]; + +ExtensionManagementUtility::addTCAcolumns('sys_file', $additionalColumns); +ExtensionManagementUtility::addToAllTCAtypes('sys_file', 'validation_date, validation_status'); diff --git a/README.md b/README.md index 5e5a4cb..1dbd587 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,10 @@ you can take care of the corresponding corrections. | Version | TYPO3 | PHP | Support / Development | |---------|-------------|-----------|--------------------------------------| -| 4.x | 13.x | 8.2 - 8.5 | features, bugfixes, security updates | +| 5.x | 14.x | 8.2 - 8.5 | features, bugfixes, security updates | +| 4.x | 13.x | 8.2 - 8.5 | bugfixes, security updates | | 3.x | 12.x | 8.1 - 8.4 | bugfixes, security updates | -| 2.x | 10.x - 11.x | 7.4 - 8.0 | bugfixes, security updates | +| 2.x | 10.x - 11.x | 7.4 - 8.0 | no support anymore | Hint: Version 1 users should update to version 2 @@ -439,7 +440,60 @@ class VideoValidateListener ``` -### 5.5 Email settings +### 5.5 Extend the backend video overview with custom media extensions + +The backend module shows YouTube and Vimeo videos by default. You can add +further media extensions to the overview — and to the extension filter — via the +`ModifyVideoOverviewExtensionsEvent`. + +**Security note:** Only extensions that are registered +in `$GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['onlineMediaHelpers']` are +accepted. Calling `addExtension()` with an unknown extension is silently ignored. + +#### EventListener registration + +```yaml +services: + Extension\Namespace\Listener\VideoOverviewExtensionsListener: + tags: + - name: event.listener + identifier: 'extensionkey/videoOverviewExtensions' + event: Ayacoo\VideoValidator\Event\ModifyVideoOverviewExtensionsEvent +``` + +#### EventListener + +```php +addExtension('tiktok', 'mimetypes-media-video-tiktok'); + } +} +``` + +The event provides the following API: + +| Method | Description | +|---|---| +| `getExtensions(): array` | Returns the current list of extensions shown in the module | +| `getAllowedExtensions(): array` | Returns all extensions registered in `onlineMediaHelpers` | +| `getIconMap(): array` | Returns a map of `extension => icon identifier` for all registered extensions | +| `addExtension(string $extension, string $iconIdentifier = 'mimetypes-media-video'): void` | Adds an extension with its icon; silently ignored if not in `onlineMediaHelpers` | + +### 5.6 Email settings To define a sender for the email, the configuration ```$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress']``` diff --git a/Resources/Private/Language/de.locallang_mod.xlf b/Resources/Private/Language/de.locallang_mod.xlf new file mode 100644 index 0000000..e72814a --- /dev/null +++ b/Resources/Private/Language/de.locallang_mod.xlf @@ -0,0 +1,162 @@ + + + +
+ + + Video Validator + Video Validator + + + Overview of YouTube and Vimeo videos and their validation status. + Übersicht der YouTube- und Vimeo-Videos und deren Validierungsstatus. + + + Online video overview + Online-Video-Übersicht + + + + Video Validator + Video Validator + + + %s video(s) found + %s Video(s) gefunden + + + No videos found. + Keine Videos gefunden. + + + + File extension + Dateiendung + + + All + Alle + + + Search title + Titel suchen + + + Search in title + Im Titel suchen + + + Apply filter + Filter anwenden + + + Reset + Zurücksetzen + + + Status + Status + + + All statuses + Alle Status + + + + + + + + UID + UID + + + Extension + Extension + + + Title + Titel + + + Status + Status + + + Actions + Aktionen + + + [no title] + [kein Titel] + + + + Invalid + Ungültig + + + Valid + Gültig + + + Not validated + Nicht geprüft + + + Skipped + Übersprungen + + + + Edit metadata + Metadaten bearbeiten + + + Show + Anzeigen + + + Refresh status + Status aktualisieren + + + Status refreshed + Status aktualisiert + + + The video status has been updated. + Der Videostatus wurde aktualisiert. + + + Refresh failed + Aktualisierung fehlgeschlagen + + + The video status could not be refreshed. + Der Videostatus konnte nicht aktualisiert werden. + + + + First + Erste + + + Previous + Vorherige + + + Next + Nächste + + + Last + Letzte + + + Page %s of %s + Seite %s von %s + + + + diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 1963158..9d6e5ad 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -20,7 +20,7 @@ - Please set a valid defaultMailFromAddress in the LocalConfiguration + Please set a valid $TYPO3_CONF_VARS['MAIL']['defaultMailFromAddress'] in the additional.php or settings.php The video status mail was sent diff --git a/Resources/Private/Language/locallang_mod.xlf b/Resources/Private/Language/locallang_mod.xlf new file mode 100644 index 0000000..768741d --- /dev/null +++ b/Resources/Private/Language/locallang_mod.xlf @@ -0,0 +1,125 @@ + + + +
+ + + Video Validator + + + Overview of YouTube and Vimeo videos and their validation status. + + + Online video overview + + + + Video Validator + + + %s video(s) found + + + No videos found. + + + + File extension + + + All + + + Search title + + + Search in title + + + Apply filter + + + Reset + + + Status + + + All statuses + + + + + + + UID + + + Extension + + + Title + + + Status + + + Actions + + + [no title] + + + + Invalid + + + Valid + + + Not validated + + + Skipped + + + + Edit metadata + + + Show + + + Refresh status + + + Status refreshed + + + The video status has been updated. + + + Refresh failed + + + The video status could not be refreshed. + + + + First + + + Previous + + + Next + + + Last + + + Page %s of %s + + + + diff --git a/Resources/Private/Partials/FilterForm.html b/Resources/Private/Partials/FilterForm.html new file mode 100644 index 0000000..f1b943c --- /dev/null +++ b/Resources/Private/Partials/FilterForm.html @@ -0,0 +1,55 @@ + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
+
+ diff --git a/Resources/Private/Partials/Pagination.html b/Resources/Private/Partials/Pagination.html new file mode 100644 index 0000000..6fcc6df --- /dev/null +++ b/Resources/Private/Partials/Pagination.html @@ -0,0 +1,38 @@ + + + + + + diff --git a/Resources/Private/Templates/Default/Overview.html b/Resources/Private/Templates/Default/Overview.html new file mode 100644 index 0000000..32e7b35 --- /dev/null +++ b/Resources/Private/Templates/Default/Overview.html @@ -0,0 +1,120 @@ + + + + +
+

+
+ +
+ + + + +

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + {video.uid} + + {video.title} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+
+ +
+
+
+
+
+ diff --git a/Resources/Public/JavaScript/backend/video-overview.js b/Resources/Public/JavaScript/backend/video-overview.js new file mode 100644 index 0000000..f34fd0f --- /dev/null +++ b/Resources/Public/JavaScript/backend/video-overview.js @@ -0,0 +1,99 @@ +import Viewport from '@typo3/backend/viewport.js'; +import AjaxRequest from '@typo3/core/ajax/ajax-request.js'; +import Notification from '@typo3/backend/notification.js'; + +const navigateTo = (url) => { + const parsed = new URL(url, window.location.origin); + Viewport.ContentContainer.setUrl(parsed.pathname + parsed.search); +}; + +const translate = (key, fallback) => { + if (typeof TYPO3 !== 'undefined' && TYPO3.lang && TYPO3.lang[key]) { + return TYPO3.lang[key]; + } + return fallback; +}; + +const initFilterForm = () => { + const form = document.getElementById('video-validator-filter'); + if (!form) { + return; + } + form.addEventListener('submit', (e) => { + e.preventDefault(); + const actionUrl = new URL(form.action, window.location.origin); + new URLSearchParams(new FormData(form)).forEach((value, key) => { + actionUrl.searchParams.set(key, value); + }); + actionUrl.searchParams.set('page', '1'); + navigateTo(actionUrl.pathname + actionUrl.search); + }); +}; + +const initPagination = () => { + document.querySelectorAll('.video-validator-pagination a[href]').forEach((link) => { + link.addEventListener('click', (e) => { + e.preventDefault(); + navigateTo(link.getAttribute('href')); + }); + }); +}; + +const initResetLink = () => { + const link = document.getElementById('video-validator-reset'); + if (!link) { + return; + } + link.addEventListener('click', (e) => { + e.preventDefault(); + navigateTo(link.getAttribute('href')); + }); +}; + +const initRefreshButtons = () => { + document.querySelectorAll('.video-validator-refresh').forEach((button) => { + button.addEventListener('click', async (e) => { + e.preventDefault(); + const fileUid = parseInt(button.dataset.fileUid || '0', 10); + if (!fileUid) { + return; + } + const url = (TYPO3 && TYPO3.settings && TYPO3.settings.ajaxUrls) + ? TYPO3.settings.ajaxUrls['videovalidator_refresh'] + : null; + if (!url) { + return; + } + + button.disabled = true; + button.classList.add('disabled'); + try { + const response = await new AjaxRequest(url).post({ fileUid }); + const data = await response.resolve(); + if (data && data.success) { + Notification.success( + translate('videovalidator.refresh.success.title', 'Status refreshed'), + translate('videovalidator.refresh.success.message', 'The video status has been updated.'), + ); + // Reload the module content so the status badge reflects the new state + navigateTo(window.location.pathname + window.location.search); + } else { + throw new Error((data && data.message) || 'Refresh failed'); + } + } catch (err) { + Notification.error( + translate('videovalidator.refresh.error.title', 'Refresh failed'), + (err && err.message) || translate('videovalidator.refresh.error.message', 'The video status could not be refreshed.'), + ); + } finally { + button.disabled = false; + button.classList.remove('disabled'); + } + }); + }); +}; + +initFilterForm(); +initPagination(); +initResetLink(); +initRefreshButtons(); diff --git a/composer.json b/composer.json index 6cb9ea5..58befd7 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "name": "ayacoo/video-validator", "type": "typo3-cms-extension", - "version": "4.1.3", - "description": "Checks online videos in TYPO3 for availability", + "version": "5.0.0", + "description": "VideoValidator - Checks online videos in TYPO3 for availability", "homepage": "https://www.ayacoo.de", "authors": [ { @@ -19,22 +19,21 @@ ], "require": { "php": ">=8.2 < 8.6", - "typo3/cms-core": "^13.4" + "typo3/cms-core": "^14.3" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.57.0", - "helmich/typo3-typoscript-lint": "^3.1.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "phpunit/phpunit": "^11.0.3", - "saschaegerer/phpstan-typo3": "^1.10.0", - "squizlabs/php_codesniffer": "^3.8.1", + "friendsofphp/php-cs-fixer": "^3.91", + "helmich/typo3-typoscript-lint": "^3.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "^11.5.44", + "squizlabs/php_codesniffer": "^3.13", "symfony/console": "^7.0", "symfony/translation": "^7.0", "typo3/coding-standards": "^0.5.5", - "typo3/testing-framework": "^9.0" + "typo3/testing-framework": "^9.3" }, "autoload": { "psr-4": { diff --git a/ext_emconf.php b/ext_emconf.php index 4cbfa75..b421ea4 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -9,11 +9,11 @@ 'state' => 'stable', 'createDirs' => '', 'clearCacheOnLoad' => 0, - 'version' => '4.1.3', + 'version' => '5.0.0', 'constraints' => [ 'depends' => [ 'php' => '8.2.0-8.5.99', - 'typo3' => '13.0.0-13.4.99', + 'typo3' => '14.3.0-14.3.99', ], 'conflicts' => [ ], diff --git a/ext_tables.sql b/ext_tables.sql deleted file mode 100644 index 0a40525..0000000 --- a/ext_tables.sql +++ /dev/null @@ -1,8 +0,0 @@ -# -# Table structure for table 'sys_file' -# -CREATE TABLE sys_file -( - validation_date int(11) unsigned DEFAULT '0', - validation_status tinyint(3) DEFAULT '0', -);