From ce1e3e9f9c6837c8f49ee9f6e2dc647a770e920a Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Sun, 6 Apr 2025 02:34:13 +0200 Subject: [PATCH 1/5] Add TYPO3 13 compatibility --- Classes/Driver/DropboxDriver.php | 74 +++-- Classes/Form/Element/RefreshTokenElement.php | 11 +- Classes/Helper/FlashMessageHelper.php | 29 +- Configuration/FlexForms/Dropbox.xml | 98 +++---- Configuration/JavaScriptModules.php | 16 ++ .../Public/JavaScript/AccessTokenModule.js | 257 +++++++++--------- composer.json | 2 +- ext_emconf.php | 4 +- 8 files changed, 265 insertions(+), 226 deletions(-) create mode 100644 Configuration/JavaScriptModules.php diff --git a/Classes/Driver/DropboxDriver.php b/Classes/Driver/DropboxDriver.php index fa2c7fc..2e72160 100644 --- a/Classes/Driver/DropboxDriver.php +++ b/Classes/Driver/DropboxDriver.php @@ -22,10 +22,12 @@ use StefanFroemken\Dropbox\Helper\FlashMessageHelper; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; -use TYPO3\CMS\Core\Messaging\AbstractMessage; +use TYPO3\CMS\Core\Charset\CharsetConverter; +use TYPO3\CMS\Core\Resource\Capabilities; use TYPO3\CMS\Core\Resource\Driver\AbstractDriver; +use TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException; use TYPO3\CMS\Core\Resource\Exception\InvalidPathException; -use TYPO3\CMS\Core\Resource\ResourceStorageInterface; +use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; @@ -34,20 +36,32 @@ */ class DropboxDriver extends AbstractDriver { + /** + * @var string + */ + private const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF'; + protected FrontendInterface $cache; protected DropboxClient $dropboxClient; /** * A list of all supported hash algorithms, written all lower case. - * - * @var array */ - protected $supportedHashAlgorithms = ['sha1', 'md5']; + protected array $supportedHashAlgorithms = ['sha1', 'md5']; + + public function __construct(array $configuration = []) + { + parent::__construct($configuration); + + // Do not allow PUBLIC here, as each file will initiate a request to Dropbox-Api to retrieve a public share + // link which is extremely slow. + $this->capabilities = new Capabilities(Capabilities::CAPABILITY_BROWSABLE | Capabilities::CAPABILITY_WRITABLE); + } public function processConfiguration(): void { - // no need to configure something. + // No need to configure something. } public function initialize(): void @@ -56,17 +70,9 @@ public function initialize(): void $this->dropboxClient = $this->getDropboxClientFactory()->createByConfiguration($this->configuration); } - public function getCapabilities(): int - { - // Do not allow PUBLIC here, as each file will initiate a request to Dropbox-Api to retrieve a public share - // link which is extremely slow. - - return ResourceStorageInterface::CAPABILITY_BROWSABLE + ResourceStorageInterface::CAPABILITY_WRITABLE; - } - - public function mergeConfigurationCapabilities($capabilities): int + public function mergeConfigurationCapabilities($capabilities): Capabilities { - $this->capabilities &= $capabilities; + $this->capabilities->and($capabilities); return $this->capabilities; } @@ -741,7 +747,7 @@ protected function copyFileToTemporaryPath(string $fileIdentifier): string $this->getFlashMessageHelper()->addFlashMessage( 'The file meta extraction has been interrupted, because file has been removed in the meanwhile.', 'File Meta Extraction aborted', - AbstractMessage::INFO + ContextualFeedbackSeverity::INFO ); return ''; @@ -783,7 +789,7 @@ private function getDropboxClientFactory(): DropboxClientFactory */ private function getPathInfoFactory(): PathInfoFactory { - // Prevent calling GU::makeINstance multiple times + // Prevent calling GU::makeInstance multiple times // Change, if DI can be used for this class static $pathInfoFactory = null; @@ -793,4 +799,36 @@ private function getPathInfoFactory(): PathInfoFactory return $pathInfoFactory; } + + /** + * Returns a string where any character not matching [.a-zA-Z0-9_-] is + * substituted by '_' + * Trailing dots are removed + */ + public function sanitizeFileName(string $fileName, string $charset = 'utf-8'): string + { + if ($charset === 'utf-8') { + $fileName = \Normalizer::normalize($fileName) ?: $fileName; + } + + // Handle UTF-8 characters + if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) { + // Allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave) + $cleanFileName = (string)preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . ']/u', '_', trim($fileName)); + } else { + $fileName = GeneralUtility::makeInstance(CharsetConverter::class)->specCharsToASCII($charset, $fileName); + // Replace unwanted characters with underscores + $cleanFileName = (string)preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/', '_', trim($fileName)); + } + + // Strip trailing dots and return + $cleanFileName = rtrim($cleanFileName, '.'); + if ($cleanFileName === '') { + throw new InvalidFileNameException( + 'File name ' . $fileName . ' is invalid.', + 1320288991 + ); + } + return $cleanFileName; + } } diff --git a/Classes/Form/Element/RefreshTokenElement.php b/Classes/Form/Element/RefreshTokenElement.php index 24c96f8..0150dcb 100644 --- a/Classes/Form/Element/RefreshTokenElement.php +++ b/Classes/Form/Element/RefreshTokenElement.php @@ -45,13 +45,18 @@ public function render(): array { $fieldId = StringUtility::getUniqueId('tceforms-trigger-access-token-wizard-'); $resultArray = $this->initializeResultArray(); - $resultArray['requireJsModules'][] = JavaScriptModuleInstruction::forRequireJS( - 'TYPO3/CMS/Dropbox/AccessTokenModule' + + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create( + '@stefanfroemken/dropbox/AccessTokenModule.js' )->instance($fieldId); $parameterArray = $this->data['parameterArray']; $itemName = $parameterArray['itemFormElName']; - $appKeyFieldName = str_replace($this->data['flexFormFieldName'], $parameterArray['fieldConf']['config']['fieldControl']['accessToken']['appKeyFieldName'], $parameterArray['itemFormElName']); + $appKeyFieldName = str_replace( + $this->data['flexFormFieldName'], + $parameterArray['fieldConf']['config']['fieldControl']['accessToken']['appKeyFieldName'], + $parameterArray['itemFormElName'] + ); $fieldWizardResult = $this->renderFieldWizard(); $fieldWizardHtml = $fieldWizardResult['html']; diff --git a/Classes/Helper/FlashMessageHelper.php b/Classes/Helper/FlashMessageHelper.php index 0f96554..d2da749 100644 --- a/Classes/Helper/FlashMessageHelper.php +++ b/Classes/Helper/FlashMessageHelper.php @@ -15,6 +15,7 @@ use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessageQueue; use TYPO3\CMS\Core\Messaging\FlashMessageService; +use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\Utility\GeneralUtility; /** @@ -29,7 +30,7 @@ public function __construct(FlashMessageService $flashMessageService) $this->flashMessageService = $flashMessageService; } - public function addFlashMessage(string $message, string $title = '', int $severity = AbstractMessage::OK): void + public function addFlashMessage(string $message, string $title = '', ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::OK): void { // We activate storeInSession, so that messages can be displayed when click on Save&Close button. $flashMessage = GeneralUtility::makeInstance( @@ -61,19 +62,17 @@ public function hasMessages(): bool } /** - * @param int $severity Must be one of the constants in AbstractMessage class * @return FlashMessage[] */ - protected function getFlashMessagesBySeverity(int $severity): array + protected function getFlashMessagesBySeverity(ContextualFeedbackSeverity $severity): array { return $this->getFlashMessageQueue()->getAllMessages($severity); } /** - * @param int $severity Must be one of the constants in AbstractMessage class * @return FlashMessage[] */ - public function getFlashMessagesBySeverityAndFlush(int $severity): array + public function getFlashMessagesBySeverityAndFlush(ContextualFeedbackSeverity $severity): array { return $this->getFlashMessageQueue()->getAllMessagesAndFlush($severity); } @@ -89,10 +88,10 @@ public function hasErrorMessages(): bool public function getErrorMessages(bool $flush = true): array { if ($flush) { - return $this->getFlashMessagesBySeverityAndFlush(AbstractMessage::ERROR); + return $this->getFlashMessagesBySeverityAndFlush(ContextualFeedbackSeverity::ERROR); } - return $this->getFlashMessagesBySeverity(AbstractMessage::ERROR); + return $this->getFlashMessagesBySeverity(ContextualFeedbackSeverity::ERROR); } public function hasWarningMessages(): bool @@ -106,10 +105,10 @@ public function hasWarningMessages(): bool public function getWarningMessages(bool $flush = true): array { if ($flush) { - return $this->getFlashMessagesBySeverityAndFlush(AbstractMessage::WARNING); + return $this->getFlashMessagesBySeverityAndFlush(ContextualFeedbackSeverity::WARNING); } - return $this->getFlashMessagesBySeverity(AbstractMessage::WARNING); + return $this->getFlashMessagesBySeverity(ContextualFeedbackSeverity::WARNING); } public function hasOkMessages(): bool @@ -123,10 +122,10 @@ public function hasOkMessages(): bool public function getOkMessages(bool $flush = true): array { if ($flush) { - return $this->getFlashMessagesBySeverityAndFlush(AbstractMessage::OK); + return $this->getFlashMessagesBySeverityAndFlush(ContextualFeedbackSeverity::OK); } - return $this->getFlashMessagesBySeverity(AbstractMessage::OK); + return $this->getFlashMessagesBySeverity(ContextualFeedbackSeverity::OK); } public function hasInfoMessages(): bool @@ -140,10 +139,10 @@ public function hasInfoMessages(): bool public function getInfoMessages(bool $flush = true): array { if ($flush) { - return $this->getFlashMessagesBySeverityAndFlush(AbstractMessage::INFO); + return $this->getFlashMessagesBySeverityAndFlush(ContextualFeedbackSeverity::INFO); } - return $this->getFlashMessagesBySeverity(AbstractMessage::INFO); + return $this->getFlashMessagesBySeverity(ContextualFeedbackSeverity::INFO); } public function hasNoticeMessages(): bool @@ -157,10 +156,10 @@ public function hasNoticeMessages(): bool public function getNoticeMessages(bool $flush = true): array { if ($flush) { - return $this->getFlashMessagesBySeverityAndFlush(AbstractMessage::NOTICE); + return $this->getFlashMessagesBySeverityAndFlush(ContextualFeedbackSeverity::NOTICE); } - return $this->getFlashMessagesBySeverity(AbstractMessage::NOTICE); + return $this->getFlashMessagesBySeverity(ContextualFeedbackSeverity::NOTICE); } protected function getFlashMessageQueue(): FlashMessageQueue diff --git a/Configuration/FlexForms/Dropbox.xml b/Configuration/FlexForms/Dropbox.xml index 285d184..ea4c6ad 100644 --- a/Configuration/FlexForms/Dropbox.xml +++ b/Configuration/FlexForms/Dropbox.xml @@ -7,65 +7,57 @@ array - - - LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:appKey.description - - input - trim - 30 - - + + LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:appKey.description + + input + trim + 30 + - - - LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:refreshToken.description - - input - trim - 30 - - - refreshToken - appKey - - - - + + LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:refreshToken.description + + input + trim + 30 + + + refreshToken + appKey + + + - - - LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:accessType.description - - select - selectSingle - - - LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:accessType.appFolder - AppFolder - - - LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:accessType.fullDropbox - FullDropbox - - - 1 - 1 - width:150px - - + + LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:accessType.description + + select + selectSingle + + + LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:accessType.appFolder + AppFolder + + + LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:accessType.fullDropbox + FullDropbox + + + 1 + 1 + width:150px + - - - LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:dropboxStatus.description - - user - dropboxStatus - - + + LLL:EXT:dropbox/Resources/Private/Language/FlexForm.xlf:dropboxStatus.description + + user + dropboxStatus + diff --git a/Configuration/JavaScriptModules.php b/Configuration/JavaScriptModules.php new file mode 100644 index 0000000..905feef --- /dev/null +++ b/Configuration/JavaScriptModules.php @@ -0,0 +1,16 @@ + [ + 'backend', + 'core', + ], + 'tags' => [ + 'backend.form', + ], + 'imports' => [ + '@stefanfroemken/dropbox/' => [ + 'path' => 'EXT:dropbox/Resources/Public/JavaScript/', + ], + ], +]; diff --git a/Resources/Public/JavaScript/AccessTokenModule.js b/Resources/Public/JavaScript/AccessTokenModule.js index 7ced90b..d2732ec 100644 --- a/Resources/Public/JavaScript/AccessTokenModule.js +++ b/Resources/Public/JavaScript/AccessTokenModule.js @@ -1,146 +1,135 @@ -/* - * This file is part of the stefanfroemken/dropbox project. - * - * It is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, either version 2 - * of the License, or any later version. - * - * For the full copyright and license information, please read the - * LICENSE.txt file that was distributed with this source code. - * - * The TYPO3 project - inspiring people to share! - */ -define(["exports", "TYPO3/CMS/Backend/Enum/Severity", "TYPO3/CMS/Backend/MultiStepWizard", "jquery"], function(exports, severity, multiStep, $) { - class AccessTokenModule { - constructor(triggerAccessTokenWizardButtonClass) { - $("." + triggerAccessTokenWizardButtonClass).on("click", function () { - let $getAccessTokenButton = $(this); - const codeVerifier = generateCodeVerifier(); - multiStep.addSlide( - "AppKeySecretForm", - "Set Dropbox App key and secret", - getFormForAppKeyAndSecret(), - severity.info, - "Step 1/3", - function ($slide) { - let $modal = $slide.closest('.modal'); - let $nextButton = $modal.find(".modal-footer").find('button[name="next"]'); - let $appKeyElement = $('[data-formengine-input-name="' + $getAccessTokenButton.data("appkeyfieldname") + '"]'); - $slide.find("input#appKey").val($appKeyElement.val()); - multiStep.lockPrevStep(); - $nextButton.off().on("click", function () { - multiStep.set("appKey", $slide.find("input#appKey").val()); - multiStep.setup.$carousel.carousel("next") - }); - } - ); +import {SeverityEnum} from "@typo3/backend/enum/severity.js" +import MultiStepWizard from "@typo3/backend/multi-step-wizard.js" +import $ from "jquery"; - multiStep.addSlide( - "RequestAuthCode", - "Use link to retrieve Dropbox Auth Code", - getPanelWithAuthCodeLink(), - severity.info, - "Step 2/3", - function ($slide, settings) { - multiStep.unlockPrevStep(); - $slide.find("a#authCodeLink").on("click", function () { - multiStep.setup.$carousel.carousel("next") - }).attr( - "href", - "https://www.dropbox.com/oauth2/authorize?client_id=" + settings.appKey + "&response_type=code&code_challenge=" + codeVerifier + "&code_challenge_method=plain&token_access_type=offline" - ); - } - ); +export default class AccessTokenModule { + constructor(triggerAccessTokenWizardButtonClass) { + $("." + triggerAccessTokenWizardButtonClass).on("click", function () { + let $getAccessTokenButton = $(this); + const codeVerifier = generateCodeVerifier(); - multiStep.addSlide( - "AuthCodeForm", - "Insert AuthCode retrieved by link of previous step", - getFormForAuthCode(), - severity.info, - "Finish", - function ($slide) { - let $modal = $slide.closest('.modal'); - let $nextButton = $modal.find(".modal-footer").find('button[name="next"]'); - multiStep.lockPrevStep(); - multiStep.unlockNextStep(); - multiStep.setup.forceSelection = false; - $nextButton.off().on("click", function () { - multiStep.set("authCode", $slide.find("input#authCode").val()); - multiStep.setup.$carousel.carousel("next") - }); - } - ); + MultiStepWizard.addSlide( + "AppKeySecretForm", + "Set Dropbox App key and secret", + getFormForAppKeyAndSecret(), + SeverityEnum.info, + "App Key", + function ($slide) { + let $modal = $slide.closest(".modal"); + let $nextButton = $modal.find(".modal-footer").find("button[name='next']"); + let $appKeyElement = $("[data-formengine-input-name='" + $getAccessTokenButton.data("appkeyfieldname") + "']"); + $slide.find("input#appKey").val($appKeyElement.val()); + MultiStepWizard.lockPrevStep(); + MultiStepWizard.unlockNextStep(); + $nextButton.off().on("click", function () { + MultiStepWizard.set("appKey", $slide.find("input#appKey").val()); + MultiStepWizard.next(); + }); + } + ); + + MultiStepWizard.addSlide( + "RequestAuthCode", + "Use link to retrieve Dropbox Auth Code", + getPanelWithAuthCodeLink(), + SeverityEnum.info, + "Request Auth Code", + function ($slide, settings) { + MultiStepWizard.unlockPrevStep(); + $slide.find("a#authCodeLink").on("click", function () { + MultiStepWizard.next(); + }).attr( + "href", + "https://www.dropbox.com/oauth2/authorize?client_id=" + settings.appKey + "&response_type=code&code_challenge=" + codeVerifier + "&code_challenge_method=plain&token_access_type=offline" + ); + } + ); + + MultiStepWizard.addSlide( + "AuthCodeForm", + "Insert AuthCode retrieved by link of previous step", + getFormForAuthCode(), + SeverityEnum.info, + "Set Auth Code", + function ($slide) { + let $modal = $slide.closest(".modal"); + let $nextButton = $modal.find(".modal-footer").find("button[name='next']"); + MultiStepWizard.lockPrevStep(); + MultiStepWizard.unlockNextStep(); + MultiStepWizard.setup.forceSelection = false; + $nextButton.off().on("click", function () { + MultiStepWizard.set("authCode", $slide.find("input#authCode").val()); + MultiStepWizard.next(); + }); + } + ); - multiStep.addFinalProcessingSlide(function () { - $.ajax({ - url: "https://api.dropboxapi.com/oauth2/token", - dataType: "json", - method: "POST", - data: { - code: multiStep.setup.settings["authCode"], - grant_type: "authorization_code", - client_id: multiStep.setup.settings["appKey"], - code_verifier: codeVerifier - }, - success: function(response) { - let $appKeyElement = $('[data-formengine-input-name="' + $getAccessTokenButton.data("appkeyfieldname") + '"]'); - $appKeyElement.val(multiStep.setup.settings["appKey"]); - if (response.refresh_token) { - let $accessTokenElement = $('[data-formengine-input-name="' + $getAccessTokenButton.data("itemname") + '"]'); - $accessTokenElement.val(response.refresh_token); - $accessTokenElement.trigger("change"); - } else if (response.access_token) { - let $accessTokenElement = $('[data-formengine-input-name="' + $getAccessTokenButton.data("itemname") + '"]'); - $accessTokenElement.val(response.access_token); - $accessTokenElement.trigger("change"); + MultiStepWizard.addFinalProcessingSlide(function () { + $.ajax({ + url: "https://api.dropboxapi.com/oauth2/token", + dataType: "json", + method: "POST", + data: { + code: MultiStepWizard.setup.settings["authCode"], + grant_type: "authorization_code", + client_id: MultiStepWizard.setup.settings["appKey"], + code_verifier: codeVerifier + }, + success: function (response) { + let $appKeyElement = $("[data-formengine-input-name='" + $getAccessTokenButton.data("appkeyfieldname") + "']"); + $appKeyElement.val(MultiStepWizard.setup.settings["appKey"]); + const tokenValue = response.refresh_token || response.access_token; + if (tokenValue) { + let $accessTokenElement = $("[data-formengine-input-name='" + $getAccessTokenButton.data("itemname") + "']"); + if ($accessTokenElement && $accessTokenElement.length) { + let humanReadableField = $accessTokenElement.get(0); + humanReadableField.value = tokenValue; + humanReadableField.dispatchEvent(new Event('change')); } - multiStep.dismiss(); - }, - fail: function() { - console.log("Ups"); - multiStep.dismiss(); } - }); - }).then(function () { - multiStep.show(); + MultiStepWizard.dismiss(); + }, + fail: function () { + console.log("Ups"); + MultiStepWizard.dismiss(); + } }); + }).then(function () { + MultiStepWizard.show(); }); - } + }); } +} - return AccessTokenModule; +function getFormForAppKeyAndSecret() { + return $(` +
+ + +
+ `); +} - function getFormForAppKeyAndSecret() - { - return '
' + - ' ' + - ' ' + - '
'; - } +function getPanelWithAuthCodeLink() { + return `
+
Authorize your Dropbox App
+ +
`; +} - function getPanelWithAuthCodeLink() - { - return '
' + - '
Authorize your Dropbox App
' + - ' ' + - '
' - } +function getFormForAuthCode() { + return `
+ + +
`; +} - function getFormForAuthCode() - { - return '
' + - ' ' + - ' ' + - '
'; - } - - function generateCodeVerifier(len) { - const uint8Array = new Uint8Array((len || 128) / 2); - const arrayBuffer = window.crypto.getRandomValues(uint8Array); - return Array.from(new Uint8Array(arrayBuffer)) - .map((item) => item.toString(16).padStart(2, "0")) - .join(""); - } -}); +function generateCodeVerifier(len) { + const uint8Array = new Uint8Array((len || 128) / 2); + const arrayBuffer = window.crypto.getRandomValues(uint8Array); + return Array.from(new Uint8Array(arrayBuffer)) + .map((item) => item.toString(16).padStart(2, "0")) + .join(""); +} diff --git a/composer.json b/composer.json index 67270a2..eab8590 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "source": "https://github.com/froemken/dropbox" }, "require": { - "typo3/cms-core": "^12.4.25", + "typo3/cms-core": "^13.4.3", "spatie/dropbox-api": "^1.21.1" }, "require-dev": { diff --git a/ext_emconf.php b/ext_emconf.php index d835d82..584932c 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -8,10 +8,10 @@ 'author_email' => 'froemken@gmail.com', 'state' => 'stable', 'clearCacheOnLoad' => true, - 'version' => '5.0.2', + 'version' => '6.0.0', 'constraints' => [ 'depends' => [ - 'typo3' => '12.4.25-12.4.99', + 'typo3' => '13.4.3-13.4.99', ], 'conflicts' => [ ], From 9a464bf0b43ee96d5fbc317dcfa273004e9868b1 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Sun, 6 Apr 2025 21:40:16 +0200 Subject: [PATCH 2/5] Update documentation --- Documentation/AdministratorManual/Index.rst | 5 +- .../AdministratorManual/Update/Index.rst | 3 +- Documentation/ChangeLog/Index.rst | 3 - Documentation/Configuration/Index.rst | 15 +++-- Documentation/FAQ/Index.rst | 3 - Documentation/Includes.rst.txt | 34 ---------- Documentation/Index.rst | 61 +++++++----------- Documentation/Installation/Index.rst | 3 - Documentation/Introduction/Index.rst | 5 +- Documentation/Settings.cfg | 62 ------------------- Documentation/Sitemap.rst | 9 --- Documentation/genindex.rst | 7 --- Documentation/guides.xml | 23 +++++++ 13 files changed, 56 insertions(+), 177 deletions(-) delete mode 100644 Documentation/Includes.rst.txt delete mode 100644 Documentation/Sitemap.rst delete mode 100644 Documentation/genindex.rst create mode 100644 Documentation/guides.xml diff --git a/Documentation/AdministratorManual/Index.rst b/Documentation/AdministratorManual/Index.rst index 4b8b14a..a66e5dd 100644 --- a/Documentation/AdministratorManual/Index.rst +++ b/Documentation/AdministratorManual/Index.rst @@ -1,7 +1,4 @@ -.. include:: /Includes.rst.txt - - -.. _admin-manual: +.. _admin-manual: ================== For administrators diff --git a/Documentation/AdministratorManual/Update/Index.rst b/Documentation/AdministratorManual/Update/Index.rst index b945294..3b89b87 100644 --- a/Documentation/AdministratorManual/Update/Index.rst +++ b/Documentation/AdministratorManual/Update/Index.rst @@ -1,5 +1,4 @@ -.. include:: /Includes.rst.txt - +.. _update: ======== Updating diff --git a/Documentation/ChangeLog/Index.rst b/Documentation/ChangeLog/Index.rst index fdb1d69..ec4cc2a 100644 --- a/Documentation/ChangeLog/Index.rst +++ b/Documentation/ChangeLog/Index.rst @@ -1,6 +1,3 @@ -.. include:: /Includes.rst.txt - - .. _changelog: ========= diff --git a/Documentation/Configuration/Index.rst b/Documentation/Configuration/Index.rst index e56a9b6..6af4e58 100644 --- a/Documentation/Configuration/Index.rst +++ b/Documentation/Configuration/Index.rst @@ -1,7 +1,4 @@ -.. include:: /Includes.rst.txt - - -.. _configuration: +.. _configuration: ============= Configuration @@ -58,6 +55,8 @@ Create APP at dropbox.com For next section you will need to copy `App key` from tab `Settings`. +.. _create-file-storage: + Create File Storage =================== @@ -76,7 +75,6 @@ Click on ``Get AuthCode Link`` .. figure:: ../Images/AdministratorManual/dropbox_insert_app_secret.jpg :width: 500px - :align: left :alt: Insert app key and app secret On the next page you have to click on the ``authorization link`` which will @@ -86,7 +84,6 @@ Copy the AuthCode from Dropbox page into the AuthCode field of the Wizard. .. figure:: ../Images/AdministratorManual/dropbox_wizard_access_token.jpg :width: 500px - :align: left :alt: Get Access Toekn from Dropbox With a click on ``Get AccessToken`` a further request to dropbox.com will @@ -97,10 +94,12 @@ Save the record. On success it will show you some user data. .. figure:: ../Images/AdministratorManual/dropbox_connect_success.jpg :width: 500px - :align: left :alt: Connection successfully -**Performance** +.. _performance: + +Performance +=========== .. note:: diff --git a/Documentation/FAQ/Index.rst b/Documentation/FAQ/Index.rst index 824e25d..cbf4a27 100644 --- a/Documentation/FAQ/Index.rst +++ b/Documentation/FAQ/Index.rst @@ -1,6 +1,3 @@ -.. include:: /Includes.rst.txt - - .. _faq: === diff --git a/Documentation/Includes.rst.txt b/Documentation/Includes.rst.txt deleted file mode 100644 index 210ac57..0000000 --- a/Documentation/Includes.rst.txt +++ /dev/null @@ -1,34 +0,0 @@ -.. 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 diff --git a/Documentation/Index.rst b/Documentation/Index.rst index 2cbdbe1..25a27c4 100644 --- a/Documentation/Index.rst +++ b/Documentation/Index.rst @@ -1,46 +1,15 @@ -.. include:: /Includes.rst.txt - - .. _start: ================== Dropbox FAL Driver ================== -:Extension key: - dropbox - -:Package name: - stefanfroemken/dropbox - -:Version: - |release| - -:Language: - en - -:Author: - Stefan Froemken - -:License: - This document is published under the - `Creative Commons BY 4.0 `__ - license. - -:Rendered: - |today| - ----- - Provides a Dropbox driver for TYPO3 File Abstraction Layer. ----- - -**Table of Contents:** - .. toctree:: - :maxdepth: 2 + :glob: :titlesonly: + :hidden: Introduction/Index Installation/Index @@ -49,10 +18,26 @@ Provides a Dropbox driver for TYPO3 File Abstraction Layer. FAQ/Index ChangeLog/Index -.. Meta Menu +.. card-grid:: + :columns: 1 + :columns-md: 2 + :gap: 4 + :class: pb-4 + :card-height: 100 -.. toctree:: - :hidden: + .. card:: :ref:`Introduction ` + + See how :t3ext:`dropbox` looks like in action. + + .. card:: :ref:`Installation ` + + Explains how to install this extension in Composer-based and Classic + TYPO3 installations. + + .. card:: :ref:`Configuration ` + + Learn how to configure this extension. + + .. card:: :ref:`FAQ ` - Sitemap - genindex + These questions have been frequently asked. diff --git a/Documentation/Installation/Index.rst b/Documentation/Installation/Index.rst index 5c9a231..df32465 100644 --- a/Documentation/Installation/Index.rst +++ b/Documentation/Installation/Index.rst @@ -1,6 +1,3 @@ -.. include:: /Includes.rst.txt - - .. _installation: ============ diff --git a/Documentation/Introduction/Index.rst b/Documentation/Introduction/Index.rst index 64628d8..67e559f 100644 --- a/Documentation/Introduction/Index.rst +++ b/Documentation/Introduction/Index.rst @@ -1,7 +1,4 @@ -.. include:: /Includes.rst.txt - - -.. _introduction: +.. _introduction: ================ What does it do? diff --git a/Documentation/Settings.cfg b/Documentation/Settings.cfg index 26f1b6b..e69de29 100644 --- a/Documentation/Settings.cfg +++ b/Documentation/Settings.cfg @@ -1,62 +0,0 @@ -# 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 = Dropbox FAL Driver -version = 5.0.2 -release = 5.0 -copyright = by Stefan Froemken - -[html_theme_options] - -# "Edit on GitHub" button -github_repository = froemken/dropbox -github_branch = main - -# Footer links -project_home = https://extensions.typo3.org/extension/dropbox -project_contact = froemken@gmail.com -project_repository = https://github.com/froemken/dropbox -project_issues = https://github.com/froemken/dropbox/issues - -[intersphinx_mapping] - -# Official TYPO3 manuals -h2document = https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/ -# t3content = https://docs.typo3.org/m/typo3/guide-contentandmarketing/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/ -# t3editors = https://docs.typo3.org/m/typo3/tutorial-editors/main/en-us/ -# t3extexample = https://docs.typo3.org/m/typo3/guide-example-extension-manual/main/en-us/ -# t3home = https://docs.typo3.org/ -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/ -# t3translate = https://docs.typo3.org/m/typo3/guide-frontendlocalization/main/en-us/ -# t3tsconfig = https://docs.typo3.org/m/typo3/reference-tsconfig/main/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_impexp = https://docs.typo3.org/c/typo3/cms-impexp/main/en-us/ -# ext_indexed_search = https://docs.typo3.org/c/typo3/cms-indexed-search/main/en-us/ -# ext_linkvalidator = https://docs.typo3.org/c/typo3/cms-linkvalidator/main/en-us/ -# ext_lowlevel = https://docs.typo3.org/c/typo3/cms-lowlevel/main/en-us/ -# ext_reactions = https://docs.typo3.org/c/typo3/cms-reactions/main/en-us/ -# ext_recycler = https://docs.typo3.org/c/typo3/cms-recycler/main/en-us/ -# ext_redirects = https://docs.typo3.org/c/typo3/cms-redirects/main/en-us/ -# ext_reports = https://docs.typo3.org/c/typo3/cms-reports/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_t3editor = https://docs.typo3.org/c/typo3/cms-t3editor/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 deleted file mode 100644 index cd06f7d..0000000 --- a/Documentation/Sitemap.rst +++ /dev/null @@ -1,9 +0,0 @@ -:template: sitemap.html - -.. include:: /Includes.rst.txt - -======= -Sitemap -======= - -.. The sitemap.html template will insert here the page tree automatically. diff --git a/Documentation/genindex.rst b/Documentation/genindex.rst deleted file mode 100644 index 38a804f..0000000 --- a/Documentation/genindex.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. include:: /Includes.rst.txt - -===== -Index -===== - -.. Sphinx will insert here the general index automatically. diff --git a/Documentation/guides.xml b/Documentation/guides.xml new file mode 100644 index 0000000..c23f92f --- /dev/null +++ b/Documentation/guides.xml @@ -0,0 +1,23 @@ + + + + + + From a9e2f0d76fabdd7555e66fb27a8e87a9221a1fcd Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Sun, 6 Apr 2025 21:47:47 +0200 Subject: [PATCH 3/5] Update test environment --- .github/workflows/ci.yml | 71 +- .gitignore | 65 +- .phpstorm.meta.php | 153 +++++ Build/Scripts/runTests.sh | 606 ++++++++++++++++++ .../.php-cs-fixer.dist.php} | 44 +- composer.json | 22 +- 6 files changed, 874 insertions(+), 87 deletions(-) create mode 100644 .phpstorm.meta.php create mode 100755 Build/Scripts/runTests.sh rename Build/{php-cs-fixer/php-cs-fixer.php => cgl/.php-cs-fixer.dist.php} (79%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e38b80..5872c71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,56 +1,35 @@ -# Adapted from https://github.com/TYPO3GmbH/blog/blob/master/.github/workflows/ci.yml -name: CI +name: Tests on: [pull_request] jobs: - build: + testing: + name: Testing + runs-on: ubuntu-latest strategy: - fail-fast: false + fail-fast: true + matrix: - typo3: ['^11.5', '^12.4'] - php: ['8.1'] - include: - - typo3: ^11.5 - php: '7.4' - - typo3: ^11.5 - php: '8.0' - - typo3: ^12.4 - php: '8.2' + php: + - '8.2' steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup PHP ${{ matrix.php }}, with composer and extensions - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: mbstring, dom, zip - - - name: Validate composer.json and composer.lock - run: composer validate - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache composer dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies with typo3/cms-core:${{ matrix.typo3 }} - run: | - composer require typo3/cms-core:${{ matrix.typo3 }} --no-progress - git checkout composer.json - - - name: Lint PHP - run: composer ci:php:lint - - - name: php-cs-fixer - run: composer ci:php:fixer + - name: 'Checkout' + uses: actions/checkout@v4 + + - name: 'Lint PHP' + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s lint + + - name: 'Install testing system' + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerUpdate + + - name: 'Composer validate' + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerValidate + + - name: 'Composer normalize' + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerNormalize -n + + - name: 'CGL' + run: Build/Scripts/runTests.sh -n -p ${{ matrix.php }} -s cgl diff --git a/.gitignore b/.gitignore index a9446c0..a1b1c03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,31 @@ ######################## -# dropbox -# global ignore file +# ignore file for +# EXT:dropbox ######################## - -# Ignore files generated by docs rendering -*GENERATED* -/docker-compose.yaml -/docker-compose.yml - +# +# Don't ignore .rej and .orig as we want to see/clean files after conflict resolution. +# +# For local exclude patterns please edit .git/info/exclude. +# +######################## +# # Ignore temporary files (left by editors and OS) *~ *.bak *.swp .DS_Store - +# # Ignore by common IDEs used directories/files nbproject *.idea *.project +*.vscode .buildpath .settings .TemporaryItems .webprj .fleet - +# # Temporary files and folders /.cache .php_cs.cache @@ -31,6 +33,45 @@ nbproject .sass-cache .session *.log - +# +# Ignore build stuff +/.ddev/* +/Build/.auth +/Build/.cache +/Build/phpunit/FunctionalTests-Job-* +/Build/bower_components/* +/Build/node_modules/* +/Build/JavaScript +!/Build/typings/no-def* +/Build/testing-docker/local/.env +/Build/testing-docker/local/macos_passwd +/typo3/sysext/*/Resources/Private/TypeScript/*.js +/typo3/sysext/*/Resources/Public/JavaScript/*.js.map +/typo3/sysext/core/Tests/AcceptanceTests-Job-* +# +# TypeScript stuff +.baseDir.ts +.tscache +# # Ignore composer stuff -/composer.lock +/bin/* +/vendor/* +/index.php +/typo3/install.php +# +# Ignore common TYPO3 CMS files/directories +/typo3temp/* +/typo3conf/* +!/typo3conf/ext +/typo3conf/ext/* +/fileadmin/* +/uploads/* +/FIRST_INSTALL +# +# root .htaccess file +/.htaccess +# Ignore files generated by tests +typo3/sysext/core/Tests/Acceptance/Support/_generated/ +/favicon.ico +# Ignore files generated by docs rendering +Documentation-GENERATED-temp/ diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php new file mode 100644 index 0000000..c9d264c --- /dev/null +++ b/.phpstorm.meta.php @@ -0,0 +1,153 @@ + \TYPO3\CMS\Core\Context\DateTimeAspect::class, + 'visibility' => \TYPO3\CMS\Core\Context\VisibilityAspect::class, + 'backend.user' => \TYPO3\CMS\Core\Context\UserAspect::class, + 'frontend.user' => \TYPO3\CMS\Core\Context\UserAspect::class, + 'workspace' => \TYPO3\CMS\Core\Context\WorkspaceAspect::class, + 'language' => \TYPO3\CMS\Core\Context\LanguageAspect::class, + 'frontend.preview' => \TYPO3\CMS\Frontend\Context\PreviewAspect::class, + ])); + expectedArguments( + \TYPO3\CMS\Core\Context\DateTimeAspect::get(), + 0, + 'timestamp', + 'iso', + 'timezone', + 'full', + 'accessTime' + ); + expectedArguments( + \TYPO3\CMS\Core\Context\VisibilityAspect::get(), + 0, + 'includeHiddenPages', + 'includeHiddenContent', + 'includeDeletedRecords' + ); + expectedArguments( + \TYPO3\CMS\Core\Context\UserAspect::get(), + 0, + 'id', + 'username', + 'isLoggedIn', + 'isAdmin', + 'groupIds', + 'groupNames' + ); + expectedArguments( + \TYPO3\CMS\Core\Context\WorkspaceAspect::get(), + 0, + 'id', + 'isLive', + 'isOffline' + ); + expectedArguments( + \TYPO3\CMS\Core\Context\LanguageAspect::get(), + 0, + 'id', + 'contentId', + 'fallbackChain', + 'overlayType', + 'legacyLanguageMode', + 'legacyOverlayType' + ); + expectedArguments( + \TYPO3\CMS\Frontend\Context\PreviewAspect::get(), + 0, + 'isPreview' + ); + + expectedArguments( + \Psr\Http\Message\ServerRequestInterface::getAttribute(), + 0, + 'frontend.user', + 'normalizedParams', + 'site', + 'language', + 'routing', + 'module', + 'moduleData', + 'frontend.controller', + 'frontend.typoscript', + 'frontend.cache.collector', + 'frontend.cache.instruction', + 'frontend.page.information', + ); + override(\Psr\Http\Message\ServerRequestInterface::getAttribute(), map([ + 'frontend.user' => \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication::class, + 'normalizedParams' => \TYPO3\CMS\Core\Http\NormalizedParams::class, + 'site' => \TYPO3\CMS\Core\Site\Entity\SiteInterface::class, + 'language' => \TYPO3\CMS\Core\Site\Entity\SiteLanguage::class, + 'routing' => '\TYPO3\CMS\Core\Routing\SiteRouteResult|\TYPO3\CMS\Core\Routing\PageArguments', + 'module' => \TYPO3\CMS\Backend\Module\ModuleInterface::class, + 'moduleData' => \TYPO3\CMS\Backend\Module\ModuleData::class, + 'frontend.controller' => \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::class, + 'frontend.typoscript' => \TYPO3\CMS\Core\TypoScript\FrontendTypoScript::class, + 'frontend.cache.collector' => \TYPO3\CMS\Core\Cache\CacheDataCollector::class, + 'frontend.cache.instruction' => \TYPO3\CMS\Frontend\Cache\CacheInstruction::class, + 'frontend.page.information' => \TYPO3\CMS\Frontend\Page\PageInformation::class, + ])); + + expectedArguments( + \TYPO3\CMS\Core\Http\ServerRequest::getAttribute(), + 0, + 'frontend.user', + 'normalizedParams', + 'site', + 'language', + 'routing', + 'module', + 'moduleData' + ); + override(\TYPO3\CMS\Core\Http\ServerRequest::getAttribute(), map([ + 'frontend.user' => \TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication::class, + 'normalizedParams' => \TYPO3\CMS\Core\Http\NormalizedParams::class, + 'site' => \TYPO3\CMS\Core\Site\Entity\SiteInterface::class, + 'language' => \TYPO3\CMS\Core\Site\Entity\SiteLanguage::class, + 'routing' => '\TYPO3\CMS\Core\Routing\SiteRouteResult|\TYPO3\CMS\Core\Routing\PageArguments', + 'module' => \TYPO3\CMS\Backend\Module\ModuleInterface::class, + 'moduleData' => \TYPO3\CMS\Backend\Module\ModuleData::class, + ])); + + override(\TYPO3\CMS\Core\Routing\SiteMatcher::matchRequest(), type( + \TYPO3\CMS\Core\Routing\SiteRouteResult::class, + \TYPO3\CMS\Core\Routing\RouteResultInterface::class, + ) + ); + + override(\TYPO3\CMS\Core\Routing\PageRouter::matchRequest(), type( + \TYPO3\CMS\Core\Routing\PageArguments::class, + \TYPO3\CMS\Core\Routing\RouteResultInterface::class, + )); + + override(\Psr\Container\ContainerInterface::get(0), map([ + '' => '@', + ])); + + override(\Psr\EventDispatcher\EventDispatcherInterface::dispatch(0), map([ + '' => '@', + ])); + + override(\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(0), map([ + '' => '@' + ])); +} diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh new file mode 100755 index 0000000..843bb0a --- /dev/null +++ b/Build/Scripts/runTests.sh @@ -0,0 +1,606 @@ +#!/usr/bin/env bash + +# +# EXT:examples 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" +} + +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 folds for files. + - cleanRenderedDocumentation: Clean existing rendered documentation output. + - composer: "composer" with all remaining arguments dispatched. + - composerNormalize: "composer normalize" + - composerUpdate: "composer update", handy if host has no PHP + - composerUpdateRector: "composer update", for rector subdirectory + - composerValidate: "composer validate" + - functional: PHP functional tests + - lint: PHP linting + - phpstan: PHPStan static analysis + - phpstanBaseline: Generate PHPStan baseline + - unit: PHP unit tests + - rector: Apply Rector rules + - 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 + + -p <8.1|8.2|8.3> + Specifies the PHP minor version to be used + - 8.1: use PHP 8.1 + - 8.2: use PHP 8.2 + - 8.3: use PHP 8.3 + + -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, rector + Activate dry-run in CGL check and composer normalize that does not actively change files and only prints broken ones. + + -u + Update existing typo3/core-testing-*:latest container images and remove dangling local volumes. + New images are published once in a while and only the latest ones are supported by core testing. + Use this if weird test errors occur. Also removes obsolete image versions of typo3/core-testing-*. + + -h + Show this help. + +Examples: + # Run unit tests using PHP 8.2 + ./Build/Scripts/runTests.sh -p 8.2 -s unit + + # Run functional tests using PHP 8.3 and MariaDB 10.6 using pdo_mysql + ./Build/Scripts/runTests.sh -p 8.3 -s functional -d mariadb -i 10.6 -a pdo_mysql + + # Run functional tests on postgres with xdebug, php 8.3 and execute a restricted set of tests + ./Build/Scripts/runTests.sh -x -p 8.3 -s functional -d postgres -- Tests/Functional/DummyTest.php +EOF +} + +# Test if docker exists, else exit out with error +if ! type "docker" >/dev/null 2>&1 && ! type "podman" >/dev/null 2>&1; then + echo "This script relies on docker or podman. Please install" >&2 + exit 1 +fi + +# Option defaults +# @todo Consider to switch from cgl to help as default +TEST_SUITE="cgl" +DATABASE_DRIVER="" +DBMS="sqlite" +DBMS_VERSION="" +PHP_VERSION="8.1" +PHP_XDEBUG_ON=0 +PHP_XDEBUG_PORT=9003 +CGLCHECK_DRY_RUN=0 +CI_PARAMS="${CI_PARAMS:-}" +DOCS_PARAMS="${DOCS_PARAMS:=--pull always}" +CONTAINER_BIN="" +CONTAINER_HOST="host.docker.internal" + +# Option parsing updates above default vars +# Reset in case getopts has been used previously in the shell +OPTIND=1 +# Array for invalid options +INVALID_OPTIONS=() +# Simple option parsing based on getopts (! not getopt) +while getopts "a:b:d:i:s:p: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.1|8.2|8.3)$ ]]; then + INVALID_OPTIONS+=("p ${OPTARG}") + fi + ;; + x) + PHP_XDEBUG_ON=1 + ;; + y) + PHP_XDEBUG_PORT=${OPTARG} + ;; + n) + CGLCHECK_DRY_RUN=1 + ;; + h) + loadHelp + echo "${HELP}" + exit 0 + ;; + u) + TEST_SUITE=update + ;; + \?) + INVALID_OPTIONS+=("${OPTARG}") + ;; + :) + INVALID_OPTIONS+=("${OPTARG}") + ;; + esac +done + +# Exit on invalid options +if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then + echo "Invalid option(s):" >&2 + for I in "${INVALID_OPTIONS[@]}"; do + echo "-"${I} >&2 + done + echo >&2 + echo "call \".Build/Scripts/runTests.sh -h\" to display help and valid options" + exit 1 +fi + +handleDbmsOptions + +COMPOSER_ROOT_VERSION="13.0.x-dev" +HOST_UID=$(id -u) +USERSET="" +if [ $(uname) != "Darwin" ]; then + USERSET="--user $HOST_UID" +fi + +# Go to the directory this script is located, so everything else is relative +# to this dir, no matter from where this script is called, then go up two dirs. +THIS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$THIS_SCRIPT_DIR" || exit 1 +cd ../../ || exit 1 +ROOT_DIR="${PWD}" + +# Create .cache dir: composer need this. +mkdir -p .Build/.cache +mkdir -p .Build/web/typo3temp/var/tests + +IMAGE_PREFIX="docker.io/" +# Non-CI fetches TYPO3 images (php and nodejs) from ghcr.io +TYPO3_IMAGE_PREFIX="ghcr.io/typo3/" +CONTAINER_INTERACTIVE="-it --init" + +IS_CORE_CI=0 +# ENV var "CI" is set by gitlab-ci. We use it here to distinct 'local' and 'CI' environment. +if [ "${CI}" == "true" ]; then + IS_CORE_CI=1 + IMAGE_PREFIX="" + CONTAINER_INTERACTIVE="" +fi + +# determine default container binary to use: 1. podman 2. docker +if [[ -z "${CONTAINER_BIN}" ]]; then + if type "podman" >/dev/null 2>&1; then + CONTAINER_BIN="podman" + elif type "docker" >/dev/null 2>&1; then + CONTAINER_BIN="docker" + fi +fi + +IMAGE_PHP="${TYPO3_IMAGE_PREFIX}core-testing-$(echo "php${PHP_VERSION}" | sed -e 's/\.//'):latest" +IMAGE_ALPINE="${IMAGE_PREFIX}alpine:3.8" +IMAGE_MARIADB="docker.io/mariadb:${DBMS_VERSION}" +IMAGE_MYSQL="docker.io/mysql:${DBMS_VERSION}" +IMAGE_POSTGRES="docker.io/postgres:${DBMS_VERSION}-alpine" +IMAGE_DOCS="ghcr.io/typo3-documentation/render-guides:latest" + +# Set $1 to first mass argument, this is the optional test file or test directory to execute +shift $((OPTIND - 1)) + +SUFFIX=$(echo $RANDOM) +NETWORK="t3docsexamples-${SUFFIX}" +${CONTAINER_BIN} network create ${NETWORK} >/dev/null + +if [ ${CONTAINER_BIN} = "docker" ]; then + # docker needs the add-host for xdebug remote debugging. podman has host.container.internal built in + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} --rm --network ${NETWORK} --add-host "${CONTAINER_HOST}:host-gateway" ${USERSET} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" + CONTAINER_DOCS_PARAMS="${CONTAINER_INTERACTIVE} ${DOCS_PARAMS} --rm --network ${NETWORK} --add-host "${CONTAINER_HOST}:host-gateway" ${USERSET} -v ${ROOT_DIR}:/project" +else + # podman + CONTAINER_HOST="host.containers.internal" + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} ${CI_PARAMS} --rm --network ${NETWORK} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" + CONTAINER_DOCS_PARAMS="${CONTAINER_INTERACTIVE} ${DOCS_PARAMS} --rm --network ${NETWORK} -v ${ROOT_DIR}:/project" +fi + +if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE="-e XDEBUG_MODE=off" + XDEBUG_CONFIG=" " +else + XDEBUG_MODE="-e XDEBUG_MODE=debug -e XDEBUG_TRIGGER=foo" + XDEBUG_CONFIG="client_port=${PHP_XDEBUG_PORT} client_host=${CONTAINER_HOST}" +fi + +# Suite execution +case ${TEST_SUITE} in + cgl) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND="php -dxdebug.mode=off .Build/bin/php-cs-fixer fix -v --dry-run --diff --config=Build/cgl/.php-cs-fixer.dist.php --using-cache=no ." + else + COMMAND="php -dxdebug.mode=off .Build/bin/php-cs-fixer fix -v --config=Build/cgl/.php-cs-fixer.dist.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 -n) + 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 + ;; + composerUpdateRector) + rm -rf Build/rector/.Build/bin/ Build/rector/.Build/vendor Build/rector/composer.lock + cp ${ROOT_DIR}/Build/rector/composer.json ${ROOT_DIR}/Build/rector/composer.json.orig + if [ -f "${ROOT_DIR}/Build/rector/composer.json.testing" ]; then + cp ${ROOT_DIR}/Build/rector/composer.json ${ROOT_DIR}/Build/rector/composer.json.orig + fi + COMMAND=(composer require --working-dir=${ROOT_DIR}/Build/rector --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}/Build/rector/composer.json ${ROOT_DIR}/Build/rector/composer.json.testing + mv ${ROOT_DIR}/Build/rector/composer.json.orig ${ROOT_DIR}/Build/rector/composer.json + ;; + 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.neon" + ${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=$? + ;; + rector) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND=(php -dxdebug.mode=off Build/rector/.Build/bin/rector -n --config=Build/rector/rector.php --clear-cache "$@") + else + COMMAND=(php -dxdebug.mode=off Build/rector/.Build/bin/rector --config=Build/rector/rector.php --clear-cache "$@") + fi + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name rector-${SUFFIX} -e COMPOSER_CACHE_DIR=.Build/.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${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: ${CORE_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 diff --git a/Build/php-cs-fixer/php-cs-fixer.php b/Build/cgl/.php-cs-fixer.dist.php similarity index 79% rename from Build/php-cs-fixer/php-cs-fixer.php rename to Build/cgl/.php-cs-fixer.dist.php index 0ea4472..413bff3 100644 --- a/Build/php-cs-fixer/php-cs-fixer.php +++ b/Build/cgl/.php-cs-fixer.dist.php @@ -1,29 +1,32 @@ setHeader( - 'This file is part of the package stefanfroemken/dropbox. +declare(strict_types=1); -For the full copyright and license information, please read the -LICENSE file that was distributed with this source code.', - true -); -$config->setFinder( - (new PhpCsFixer\Finder()) - ->in(realpath(__DIR__ . '/../../')) - ->ignoreVCSIgnored(true) - ->notPath('/^.Build\//') - ->notPath('/^Build\/php-cs-fixer\/php-cs-fixer.php/') - ->notPath('/^Build\/phpunit\/(UnitTestsBootstrap|FunctionalTestsBootstrap).php/') - ->notPath('/^Configuration\//') - ->notPath('/^Documentation\//') - ->notName('/^ext_(emconf|localconf|tables).php/') -) +/* + * This file is part of the package stefanfroemken/dropbox. + * + * For the full copyright and license information, please read the + * LICENSE file that was distributed with this source code. + */ + +if (PHP_SAPI !== 'cli') { + die('This script supports command line usage only. Please check your command.'); +} + +return (new \PhpCsFixer\Config()) + ->setParallelConfig(\PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) + ->setFinder( + (new PhpCsFixer\Finder()) + ->ignoreVCSIgnored(true) + ->in(__DIR__) + ->exclude('.Build') + ) ->setRiskyAllowed(true) ->setRules([ '@DoctrineAnnotation' => true, // @todo: Switch to @PER-CS2.0 once php-cs-fixer's todo list is done: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7247 '@PER-CS1.0' => true, + 'array_indentation' => true, 'array_syntax' => ['syntax' => 'short'], 'cast_spaces' => ['space' => 'none'], // @todo: Can be dropped once we enable @PER-CS2.0 @@ -59,6 +62,10 @@ 'no_unused_imports' => true, 'no_useless_else' => true, 'no_useless_nullsafe_operator' => true, + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, 'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], 'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']], 'php_unit_mock_short_will_return' => true, @@ -80,4 +87,3 @@ 'whitespace_after_comma_in_array' => ['ensure_single_space' => true], 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], ]); -return $config; diff --git a/composer.json b/composer.json index eab8590..da277cd 100644 --- a/composer.json +++ b/composer.json @@ -28,31 +28,33 @@ "spatie/dropbox-api": "^1.21.1" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.14", - "typo3/coding-standards": "^0.6" + "ergebnis/composer-normalize": "^2.44", + "typo3/coding-standards": "^0.8", + "typo3/testing-framework": "^8.2" }, "autoload": { "psr-4": { "StefanFroemken\\Dropbox\\": "Classes" } }, + "replace": { + "typo3-ter/dropbox": "self.version" + }, "config": { - "vendor-dir": ".Build/vendor", "allow-plugins": { + "ergebnis/composer-normalize": true, "typo3/class-alias-loader": true, "typo3/cms-composer-installers": true - } + }, + "bin-dir": ".Build/bin", + "sort-packages": true, + "vendor-dir": ".Build/vendor" }, "extra": { "typo3/cms": { - "extension-key": "dropbox", "app-dir": ".Build", + "extension-key": "dropbox", "web-dir": ".Build/public" } - }, - "scripts": { - "php:fix": ".Build/vendor/bin/php-cs-fixer --config=Build/php-cs-fixer/php-cs-fixer.php fix Classes", - "ci:php:lint": "find *.php Classes Configuration Tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l", - "ci:php:fixer": ".Build/vendor/bin/php-cs-fixer --config=Build/php-cs-fixer/php-cs-fixer.php fix --dry-run -v --show-progress=dots --diff Classes" } } From bcb87c28262508d6a95d5b4d4e95be1f6bd28cac Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Sun, 6 Apr 2025 21:50:46 +0200 Subject: [PATCH 4/5] Apply CGL --- Configuration/ContentSecurityPolicies.php | 1 - .../guzzle-factory/src/GuzzleFactory.php | 20 +++++----- .../PHP/spatie/dropbox-api/src/Client.php | 39 +++++++++---------- .../dropbox-api/src/Exceptions/BadRequest.php | 3 +- ext_emconf.php | 10 ++--- ext_localconf.php | 1 + 6 files changed, 36 insertions(+), 38 deletions(-) diff --git a/Configuration/ContentSecurityPolicies.php b/Configuration/ContentSecurityPolicies.php index f490e0e..f620fd8 100644 --- a/Configuration/ContentSecurityPolicies.php +++ b/Configuration/ContentSecurityPolicies.php @@ -8,7 +8,6 @@ use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode; use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope; use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme; -use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue; use TYPO3\CMS\Core\Type\Map; /** diff --git a/Resources/Private/PHP/graham-campbell/guzzle-factory/src/GuzzleFactory.php b/Resources/Private/PHP/graham-campbell/guzzle-factory/src/GuzzleFactory.php index 239152d..74928c0 100644 --- a/Resources/Private/PHP/graham-campbell/guzzle-factory/src/GuzzleFactory.php +++ b/Resources/Private/PHP/graham-campbell/guzzle-factory/src/GuzzleFactory.php @@ -35,33 +35,33 @@ final class GuzzleFactory * * @var int */ - const CONNECT_TIMEOUT = 10; + public const CONNECT_TIMEOUT = 10; /** * The default transport timeout. * * @var int */ - const TIMEOUT = 15; + public const TIMEOUT = 15; /** * The default backoff multiplier. * * @var int */ - const BACKOFF = 1000; + public const BACKOFF = 1000; /** * The default 4xx retry codes. * * @var int[] */ - const CODES = [429]; + public const CODES = [429]; /** * The default amount of retries. */ - const RETRIES = 3; + public const RETRIES = 3; /** * Create a new guzzle client. @@ -73,7 +73,7 @@ final class GuzzleFactory * * @return \GuzzleHttp\Client */ - public static function make(array $options = [], int $backoff = null, array $codes = null, int $retries = null) + public static function make(array $options = [], ?int $backoff = null, ?array $codes = null, ?int $retries = null) { $config = array_merge(['connect_timeout' => self::CONNECT_TIMEOUT, 'timeout' => self::TIMEOUT], $options); $config['handler'] = self::handler($backoff, $codes, $retries, $options['handler'] ?? null); @@ -91,7 +91,7 @@ public static function make(array $options = [], int $backoff = null, array $cod * * @return \GuzzleHttp\HandlerStack */ - public static function handler(int $backoff = null, array $codes = null, int $retries = null, HandlerStack $stack = null) + public static function handler(?int $backoff = null, ?array $codes = null, ?int $retries = null, ?HandlerStack $stack = null) { $stack = $stack ?? self::innerHandler(); @@ -111,7 +111,7 @@ public static function handler(int $backoff = null, array $codes = null, int $re * * @return \GuzzleHttp\HandlerStack */ - public static function innerHandler(callable $handler = null): HandlerStack + public static function innerHandler(?callable $handler = null): HandlerStack { $stack = new HandlerStack($handler ?? Utils::chooseHandler()); @@ -134,10 +134,10 @@ public static function innerHandler(callable $handler = null): HandlerStack */ private static function createRetryMiddleware(int $backoff, array $codes, int $maxRetries): callable { - return Middleware::retry(function ($retries, RequestInterface $request, ResponseInterface $response = null, TransferException $exception = null) use ($codes, $maxRetries) { + return Middleware::retry(function ($retries, RequestInterface $request, ?ResponseInterface $response = null, ?TransferException $exception = null) use ($codes, $maxRetries) { return $retries < $maxRetries && ($exception instanceof ConnectException || ($response && ($response->getStatusCode() >= 500 || in_array($response->getStatusCode(), $codes, true)))); }, function ($retries) use ($backoff) { - return (int) pow(2, $retries) * $backoff; + return (int)pow(2, $retries) * $backoff; }); } } diff --git a/Resources/Private/PHP/spatie/dropbox-api/src/Client.php b/Resources/Private/PHP/spatie/dropbox-api/src/Client.php index f214970..861d82b 100644 --- a/Resources/Private/PHP/spatie/dropbox-api/src/Client.php +++ b/Resources/Private/PHP/spatie/dropbox-api/src/Client.php @@ -2,7 +2,6 @@ namespace Spatie\Dropbox; -use Exception; use GrahamCampbell\GuzzleFactory\GuzzleFactory; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\ClientInterface; @@ -17,19 +16,19 @@ class Client { - const THUMBNAIL_FORMAT_JPEG = 'jpeg'; - const THUMBNAIL_FORMAT_PNG = 'png'; + public const THUMBNAIL_FORMAT_JPEG = 'jpeg'; + public const THUMBNAIL_FORMAT_PNG = 'png'; - const THUMBNAIL_SIZE_XS = 'w32h32'; - const THUMBNAIL_SIZE_S = 'w64h64'; - const THUMBNAIL_SIZE_M = 'w128h128'; - const THUMBNAIL_SIZE_L = 'w640h480'; - const THUMBNAIL_SIZE_XL = 'w1024h768'; + public const THUMBNAIL_SIZE_XS = 'w32h32'; + public const THUMBNAIL_SIZE_S = 'w64h64'; + public const THUMBNAIL_SIZE_M = 'w128h128'; + public const THUMBNAIL_SIZE_L = 'w640h480'; + public const THUMBNAIL_SIZE_XL = 'w1024h768'; - const MAX_CHUNK_SIZE = 1024 * 1024 * 150; + public const MAX_CHUNK_SIZE = 1024 * 1024 * 150; - const UPLOAD_SESSION_START = 0; - const UPLOAD_SESSION_APPEND = 1; + public const UPLOAD_SESSION_START = 0; + public const UPLOAD_SESSION_APPEND = 1; /** * @var TokenProvider @@ -64,7 +63,7 @@ class Client * @param int $maxUploadChunkRetries How many times to retry an upload session start or append after RequestException. * @param string $teamMemberID The team member ID to be specified for Dropbox business accounts */ - public function __construct($accessTokenOrAppCredentials = null, ClientInterface $client = null, int $maxChunkSize = self::MAX_CHUNK_SIZE, int $maxUploadChunkRetries = 0, string $teamMemberId = null) + public function __construct($accessTokenOrAppCredentials = null, ?ClientInterface $client = null, int $maxChunkSize = self::MAX_CHUNK_SIZE, int $maxUploadChunkRetries = 0, ?string $teamMemberId = null) { if (is_array($accessTokenOrAppCredentials)) { [$this->appKey, $this->appSecret] = $accessTokenOrAppCredentials; @@ -169,7 +168,7 @@ public function search(string $query, bool $includeHighlights = false): array * * @link https://www.dropbox.com/developers/documentation/http/documentation#sharing-list_shared_links */ - public function listSharedLinks(string $path = null, bool $direct_only = false, string $cursor = null): array + public function listSharedLinks(?string $path = null, bool $direct_only = false, ?string $cursor = null): array { $parameters = [ 'path' => $path ? $this->normalizePath($path) : null, @@ -296,7 +295,7 @@ public function getThumbnail(string $path, string $format = 'jpeg', string $size $response = $this->contentEndpointRequest('files/get_thumbnail', $arguments); - return (string) $response->getBody(); + return (string)$response->getBody(); } /** @@ -457,7 +456,7 @@ public function uploadChunked(string $path, $contents, $mode = 'add', $chunkSize * @param int $chunkSize * @param \Spatie\Dropbox\UploadSessionCursor|null $cursor * @return \Spatie\Dropbox\UploadSessionCursor - * @throws Exception + * @throws \Exception */ protected function uploadChunk($type, &$stream, $chunkSize, $cursor = null): UploadSessionCursor { @@ -480,7 +479,7 @@ protected function uploadChunk($type, &$stream, $chunkSize, $cursor = null): Upl return $this->uploadSessionAppend($chunkStream, $cursor); } - throw new Exception('Invalid type'); + throw new \Exception('Invalid type'); } catch (RequestException $exception) { if ($tries < $maximumTries) { // rewind @@ -604,7 +603,7 @@ protected function normalizePath(string $path): string $path = trim($path, '/'); - return ($path === '') ? '' : '/'.$path; + return ($path === '') ? '' : '/' . $path; } protected function getEndpointUrl(string $subdomain, string $endpoint): string @@ -652,7 +651,7 @@ public function contentEndpointRequest(string $endpoint, array $arguments, $body return $response; } - public function rpcEndpointRequest(string $endpoint, array $parameters = null, bool $isRefreshed = false): array + public function rpcEndpointRequest(string $endpoint, ?array $parameters = null, bool $isRefreshed = false): array { try { $options = ['headers' => $this->getHeaders()]; @@ -677,7 +676,7 @@ public function rpcEndpointRequest(string $endpoint, array $parameters = null, b return json_decode($response->getBody(), true) ?? []; } - protected function determineException(ClientException $exception): Exception + protected function determineException(ClientException $exception): \Exception { if (in_array($exception->getResponse()->getStatusCode(), [400, 409])) { return new BadRequest($exception->getResponse()); @@ -788,7 +787,7 @@ protected function getHeadersForBearerToken($token) protected function getHeadersForCredentials() { return [ - 'Authorization' => 'Basic '.base64_encode("{$this->appKey}:{$this->appSecret}"), + 'Authorization' => 'Basic ' . base64_encode("{$this->appKey}:{$this->appSecret}"), ]; } } diff --git a/Resources/Private/PHP/spatie/dropbox-api/src/Exceptions/BadRequest.php b/Resources/Private/PHP/spatie/dropbox-api/src/Exceptions/BadRequest.php index 20696be..d8f203e 100644 --- a/Resources/Private/PHP/spatie/dropbox-api/src/Exceptions/BadRequest.php +++ b/Resources/Private/PHP/spatie/dropbox-api/src/Exceptions/BadRequest.php @@ -2,10 +2,9 @@ namespace Spatie\Dropbox\Exceptions; -use Exception; use Psr\Http\Message\ResponseInterface; -class BadRequest extends Exception +class BadRequest extends \Exception { /** * @var \Psr\Http\Message\ResponseInterface diff --git a/ext_emconf.php b/ext_emconf.php index 584932c..60de9a2 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -18,11 +18,11 @@ 'suggests' => [ ], ], - "autoload" => [ - "psr-4" => [ - "StefanFroemken\\Dropbox\\" => "Classes", - "Spatie\\Dropbox\\" => "Resources/Private/PHP/spatie/dropbox-api/src", - "GrahamCampbell\\GuzzleFactory\\" => "Resources/Private/PHP/graham-campbell/guzzle-factory/src", + 'autoload' => [ + 'psr-4' => [ + 'StefanFroemken\\Dropbox\\' => 'Classes', + 'Spatie\\Dropbox\\' => 'Resources/Private/PHP/spatie/dropbox-api/src', + 'GrahamCampbell\\GuzzleFactory\\' => 'Resources/Private/PHP/graham-campbell/guzzle-factory/src', ], ], ]; diff --git a/ext_localconf.php b/ext_localconf.php index b0386b7..a52935e 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,4 +1,5 @@ Date: Sun, 6 Apr 2025 21:54:07 +0200 Subject: [PATCH 5/5] Remove empty Settings.cfg --- Documentation/Settings.cfg | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Documentation/Settings.cfg diff --git a/Documentation/Settings.cfg b/Documentation/Settings.cfg deleted file mode 100644 index e69de29..0000000