From a3b9c109f9f60727006bd9a61b96e6140315f169 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Wed, 20 May 2026 15:51:37 +0200 Subject: [PATCH 1/7] [TASK] Replace IsFileSelectableEventListener with new classes - Removed IsFileSelectableEventListener class entirely - Added GalleryDataProcessor to handle file validations in galleries - Added AddMandatoryControlForFileReferencesEventListener to manage file reference controls - Updated locallang_mod.xlf to include warning for images with missing mandatory fields - Adjusted Services.yaml to reflect new event listeners and data processors --- .../DataProcessor/GalleryDataProcessor.php | 109 +++++++++ ...yControlForFileReferencesEventListener.php | 122 ++++++++++ .../IsFileSelectableEventListener.php | 209 ------------------ Configuration/Services.yaml | 63 +++--- Resources/Private/Language/locallang_mod.xlf | 10 +- 5 files changed, 265 insertions(+), 248 deletions(-) create mode 100644 Classes/DataProcessor/GalleryDataProcessor.php create mode 100644 Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php delete mode 100644 Classes/EventListener/IsFileSelectableEventListener.php diff --git a/Classes/DataProcessor/GalleryDataProcessor.php b/Classes/DataProcessor/GalleryDataProcessor.php new file mode 100644 index 00000000..621798ea --- /dev/null +++ b/Classes/DataProcessor/GalleryDataProcessor.php @@ -0,0 +1,109 @@ +stdWrapValue( + 'filesProcessedDataKey', + $processorConfiguration, + 'files' + ); + + if (isset($processedData[$filesProcessedDataKey]) && is_array($processedData[$filesProcessedDataKey])) { + /** @var FileReference[] $fileObjects */ + $fileObjects = $processedData[$filesProcessedDataKey]; + } else { + throw new ContentRenderingException( + 'No files found for key ' . $filesProcessedDataKey . ' in $processedData.', + 1779280032, + ); + } + + $requiredColumns = $this->getRequiredColumns(); + + if ($requiredColumns !== []) { + foreach ($fileObjects as $key => $fileObject) { + if (!$this->isValidFileObject($fileObject, $requiredColumns)) { + unset($fileObjects[$key]); + } + } + + $processedData[$filesProcessedDataKey] = $fileObjects; + } + + return $this->originalGalleryProcessor->process( + $cObj, + $contentObjectConfiguration, + $processorConfiguration, + $processedData, + ); + } + + private function isValidFileObject(FileReference $fileObject, array $requiredColumns): bool + { + foreach ($requiredColumns as $requiredColumn) { + if (!$fileObject->hasProperty($requiredColumn)) { + return false; + } + + $value = $fileObject->getProperty($requiredColumn); + $trimmedValue = is_string($value) ? trim($value) : $value; + + if ($trimmedValue === null || $trimmedValue === '') { + return false; + } + } + + return true; + } + + private function getRequiredColumns(): array + { + $requiredColumns = []; + + try { + $requiredColumns = GeneralUtility::trimExplode( + ',', + $this->extensionConfiguration->get('jwtools2', 'typo3RequiredColumnsForFiles'), + true, + ); + } catch (ExtensionConfigurationExtensionNotConfiguredException|ExtensionConfigurationPathDoesNotExistException) { + } + + return $requiredColumns; + } +} diff --git a/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php b/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php new file mode 100644 index 00000000..a05944f5 --- /dev/null +++ b/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php @@ -0,0 +1,122 @@ +getRequiredColumns(); + if ($requiredColumns === []) { + return; + } + + foreach ($requiredColumns as $requiredColumn) { + $value = $this->data['databaseRow']['uid_local'][0][$requiredColumn] ?? $this->data[$requiredColumn] ?? ''; + $trimmedValue = is_string($value) ? trim($value) : $value; + + if ($trimmedValue === null || $trimmedValue === '') { + $event->setControl( + 'requiredColumns', + ' + '); + return; + } + } + } + + private function getRequiredColumns(): array + { + $requiredColumns = []; + + try { + $requiredColumns = GeneralUtility::trimExplode( + ',', + $this->extensionConfiguration->get('jwtools2', 'typo3RequiredColumnsForFiles'), + true, + ); + } catch (ExtensionConfigurationExtensionNotConfiguredException|ExtensionConfigurationPathDoesNotExistException) { + } + + return $requiredColumns; + } + + protected function getMandatoryMessage(array $requiredColumns): string + { + return LocalizationUtility::translate( + 'LLL:EXT:jwtools2/Resources/Private/Language/locallang_mod.xlf:warning.requiredColumns', + 'Jwtools2', + [ + implode( + ', ', + $this->getTranslatedColumnNames( + $requiredColumns, + ), + ), + ], + ); + } + + protected function getTranslatedColumnNames(array $requiredColumns): array + { + if (!$this->getLanguageService() instanceof LanguageService) { + return $requiredColumns; + } + + foreach ($requiredColumns as $key => $requiredColumn) { + $label = BackendUtility::getItemLabel('sys_file', $requiredColumn); + if ($label === '' || $label === null) { + $label = BackendUtility::getItemLabel('sys_file_metadata', $requiredColumn); + } + + if ($label === '' || $label === null) { + continue; + } + + $translatedLabel = $this->getLanguageService()->sL($label); + if ($translatedLabel === '') { + continue; + } + + $requiredColumns[$key] = $translatedLabel; + } + + return $requiredColumns; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/EventListener/IsFileSelectableEventListener.php b/Classes/EventListener/IsFileSelectableEventListener.php deleted file mode 100644 index deebe418..00000000 --- a/Classes/EventListener/IsFileSelectableEventListener.php +++ /dev/null @@ -1,209 +0,0 @@ -getRequiredColumnsFromExtensionConfiguration()) { - $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); - $flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); - $message = $this->getFlashMessageDescription($requiredColumns); - - if (!$this->checkMessageExists($flashMessageQueue, $message)) { - $this->addFlashMessage($flashMessageQueue, $message); - } - - if (!$event->getFile() instanceof File) { - // Do not process folders or processed files - return; - } - - if ($event->getFile()->getType() !== 2) { - // Process only images - return; - } - - foreach ($this->getRequiredColumnsForFileMetaData() as $requiredColumn) { - $properties = $event->getFile()->getProperties(); - - // Do not use isset() as "null" values have to be tested, too. - if (!array_key_exists($requiredColumn, $properties)) { - $event->denyFileSelection(); - } - - $value = is_string($properties[$requiredColumn]) - ? trim($properties[$requiredColumn]) - : $properties[$requiredColumn]; - - if (!isset($value) || trim($value) === null || trim($value) === '') { - $event->denyFileSelection(); - } - } - } - } - - protected function addFlashMessage(FlashMessageQueue $flashMessageQueue, string $message): void - { - $flashMessageQueue->addMessage( - GeneralUtility::makeInstance( - FlashMessage::class, - $message, - LocalizationUtility::translate( - 'LLL:EXT:jwtools2/Resources/Private/Language/locallang_mod.xlf:fileBrowser.flashMessage.requiredColumns.title', - ), - ContextualFeedbackSeverity::INFO, - ), - ); - } - - protected function getFlashMessageDescription(array $requiredColumns): string - { - return LocalizationUtility::translate( - 'LLL:EXT:jwtools2/Resources/Private/Language/locallang_mod.xlf:fileBrowser.flashMessage.requiredColumns.description', - null, - [ - implode( - ', ', - $this->getTranslatedColumnNames( - $requiredColumns, - ), - ), - ], - ); - } - - protected function getTranslatedColumnNames(array $requiredColumns): array - { - if (!$this->getLanguageService() instanceof LanguageService) { - return $requiredColumns; - } - - foreach ($requiredColumns as $key => $requiredColumn) { - $label = BackendUtility::getItemLabel('sys_file', $requiredColumn); - if ($label === '' || $label === null) { - $label = BackendUtility::getItemLabel('sys_file_metadata', $requiredColumn); - } - - if ($label === '' || $label === null) { - continue; - } - - $translatedLabel = $this->getLanguageService()->sL($label); - if ($translatedLabel === '' || $translatedLabel === null) { - continue; - } - - $requiredColumns[$key] = $translatedLabel; - } - - return $requiredColumns; - } - - protected function getRequiredColumnsForFileMetaData(): array - { - // Cache result, is this method will be called from within a loop - static $requiredColumns = null; - - if ($requiredColumns === null) { - $validColumns = []; - foreach ($this->getRequiredColumnsFromExtensionConfiguration() as $column) { - if ($this->isValidColumn($column)) { - $validColumns[] = $column; - } - } - - $requiredColumns = $validColumns; - } - - return $requiredColumns; - } - - protected function getRequiredColumnsFromExtensionConfiguration(): array - { - static $requiredColumns = null; - - if ($requiredColumns === null) { - try { - $requiredColumns = GeneralUtility::trimExplode( - ',', - $this->extensionConfiguration->get('jwtools2', 'typo3RequiredColumnsForFiles'), - true, - ); - } catch (ExtensionConfigurationExtensionNotConfiguredException|ExtensionConfigurationPathDoesNotExistException $exception) { - } - } - - return $requiredColumns; - } - - protected function isValidColumn(string $column, string $table = 'sys_file'): bool - { - $columnExists = false; - $connection = $this->getConnectionPool()->getConnectionForTable($table); - $schemaManager = $connection->createSchemaManager(); - if ( - $schemaManager->tablesExist([$table]) - ) { - $columnExists = $schemaManager->introspectTable($table)->hasColumn($column); - if ($columnExists === false && $table !== 'sys_file_metadata') { - $columnExists = $this->isValidColumn($column, 'sys_file_metadata'); - } - } - - return $columnExists; - } - - protected function checkMessageExists(FlashMessageQueue $flashMessageQueue, string $message): bool - { - $messageExists = false; - foreach ($flashMessageQueue as $flashMessage) { - if ($flashMessage->getMessage() === $message) { - $messageExists = true; - break; - } - } - - return $messageExists; - } - - protected function getLanguageService(): LanguageService - { - return $GLOBALS['LANG']; - } - - protected function getConnectionPool(): ConnectionPool - { - return GeneralUtility::makeInstance(ConnectionPool::class); - } -} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 3b868a50..e078cbf0 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -7,45 +7,48 @@ services: JWeiland\Jwtools2\: resource: '../Classes/*' - # Called by makeInstance in getCallableFromTarget of Dispatcher - JWeiland\Jwtools2\Controller\Ajax\SysFileController: - public: true - JWeiland\Jwtools2\Command\CacheQueryCommand: tags: - - name: 'console.command' - command: 'jwtools2:cacheQuery' - schedulable: false + - name: 'console.command' + command: 'jwtools2:cacheQuery' + schedulable: false + JWeiland\Jwtools2\Command\ConvertPlainPasswordToHashCommand: tags: - - name: 'console.command' - command: 'jwtools2:convertpasswords' - schedulable: false + - name: 'console.command' + command: 'jwtools2:convertpasswords' + schedulable: false + JWeiland\Jwtools2\Command\StatusReportCommand: calls: - method: setTaskRepository arguments: - '@TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository' tags: - - name: 'console.command' - command: 'jwtools2:statusreport' - schedulable: false + - name: 'console.command' + command: 'jwtools2:statusreport' + schedulable: false - # Called by makeInstance in start() of ContentObjectRenderer - JWeiland\Jwtools2\Hooks\InitializeStdWrap: - public: true + # Backend Context Menu Provider + JWeiland\Jwtools2\ContextMenu\ItemProviders\UpdateFileMetaDataProvider: + tags: + - name: backend.contextmenu.itemprovider + identifier: 'jwtools2.contextmenu.itemproviders.updatefilemetadata' - JWeiland\Jwtools2\LinkHandler\FileLinkHandler: + # Called by makeInstance in getCallableFromTarget of Dispatcher + JWeiland\Jwtools2\Controller\Ajax\SysFileController: public: true - JWeiland\Jwtools2\LinkHandler\FolderLinkHandler: - public: true + JWeiland\Jwtools2\DataProcessor\GalleryDataProcessor: + shared: false + tags: + - { name: 'data.processor', identifier: 'gallery' } # Event Listeners JWeiland\Jwtools2\EventListener\ReduceCategoryTreeToPageTree: tags: - - name: event.listener - event: TYPO3\CMS\Core\Tree\Event\ModifyTreeDataEvent + - name: event.listener + event: TYPO3\CMS\Core\Tree\Event\ModifyTreeDataEvent JWeiland\Jwtools2\EventListener\IndexServiceEventListener: tags: @@ -53,14 +56,12 @@ services: identifier: 'jwtools2.events.beforeItemIsIndexedEvent' event: ApacheSolrForTypo3\Solr\Event\Indexing\BeforeItemIsIndexedEvent - JWeiland\Jwtools2\EventListener\IsFileSelectableEventListener: - tags: - - name: event.listener - identifier: 'jwtools2.events.isFileSelectableEvent' - event: TYPO3\CMS\Backend\ElementBrowser\Event\IsFileSelectableEvent + # Called by makeInstance in start() of ContentObjectRenderer + JWeiland\Jwtools2\Hooks\InitializeStdWrap: + public: true - # Backend Context Menu Provider - JWeiland\Jwtools2\ContextMenu\ItemProviders\UpdateFileMetaDataProvider: - tags: - - name: backend.contextmenu.itemprovider - identifier: 'jwtools2.contextmenu.itemproviders.updatefilemetadata' + JWeiland\Jwtools2\LinkHandler\FileLinkHandler: + public: true + + JWeiland\Jwtools2\LinkHandler\FolderLinkHandler: + public: true diff --git a/Resources/Private/Language/locallang_mod.xlf b/Resources/Private/Language/locallang_mod.xlf index 83cb5439..807d83a7 100644 --- a/Resources/Private/Language/locallang_mod.xlf +++ b/Resources/Private/Language/locallang_mod.xlf @@ -9,14 +9,8 @@ Successfully created/updated file metadata and deleted related processed files. - - - Info about non selectable files - - - For some files, certain mandatory fields (%s) must be filled in. If this is not the case, these files - cannot currently be linked to content elements or other records. Please complete these mandatory fields first. - + + For some image files, certain mandatory fields (%s) are missing. These images will not be displayed in the frontend until the required fields have been completed. From 45baf9d618172716ceea9e7f86ff945353ae9451 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Wed, 20 May 2026 15:51:45 +0200 Subject: [PATCH 2/7] [DOCS] Clarify required fields for image display - Updated documentation to specify that images with missing required fields will not appear in the frontend - Improved explanation for configuring mandatory fields for images --- Documentation/Configuration/Index.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Documentation/Configuration/Index.rst b/Documentation/Configuration/Index.rst index caeec569..1ea2c506 100644 --- a/Documentation/Configuration/Index.rst +++ b/Documentation/Configuration/Index.rst @@ -91,8 +91,9 @@ typo3RequiredColumnsForFiles Add a comma separated list of column names of table ``sys_file`` or ``sys_file_metadata`` to set these columns as required. If these columns are not -filled for an image it is not selectable in FileBrowser. It can not be inserted -into a ContentElement or record. +filled for an image, the image will not be displayed in the frontend until the +required fields have been completed. + .. _typo3ExcludeVideoFilesFromFalFilter: From 0a006b18e1b353739d862f0ef702c77041db941a Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Wed, 20 May 2026 15:51:51 +0200 Subject: [PATCH 3/7] [TASK] Update version to 8.1.2 - Bumped extension version in ext_emconf.php to 8.1.2 - Adjusted project version in documentation guides.xml - Improved indentation in guides.xml for consistency --- Documentation/guides.xml | 49 ++++++++++++++++------------------------ ext_emconf.php | 2 +- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/Documentation/guides.xml b/Documentation/guides.xml index 0ae85529..3eca6af4 100644 --- a/Documentation/guides.xml +++ b/Documentation/guides.xml @@ -1,33 +1,22 @@ - - - - - - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="https://www.phpdoc.org/guides ../vendor/phpdocumentor/guides-cli/resources/schema/guides.xsd" + links-are-relative="true"> + + + + + + diff --git a/ext_emconf.php b/ext_emconf.php index c8a0a11d..8e61331a 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -15,7 +15,7 @@ 'author_email' => 'projects@jweiland.net', 'author_company' => 'jweiland.net', 'state' => 'stable', - 'version' => '8.1.1', + 'version' => '8.1.2', 'constraints' => [ 'depends' => [ 'typo3' => '13.4.0-13.4.99', From b5f2300cfc87752931de0d065a1a0610d57c7ba1 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Wed, 20 May 2026 15:59:51 +0200 Subject: [PATCH 4/7] [DOCS] Update upgrade guide for version 8.1.2 - Document breaking change: removal of IsFileSelectableEventListener - Explain reimplementation of `typo3RequiredColumnsForFiles` feature - Add details on new control icon for file references in backend forms --- .../AdministratorManual/Upgrade/Index.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Documentation/AdministratorManual/Upgrade/Index.rst b/Documentation/AdministratorManual/Upgrade/Index.rst index ad2b402b..7bc5edbf 100644 --- a/Documentation/AdministratorManual/Upgrade/Index.rst +++ b/Documentation/AdministratorManual/Upgrade/Index.rst @@ -8,6 +8,23 @@ Upgrade If you upgrade EXT:jwtools2 to a newer version, please read this section carefully! +Upgrade to Version 8.1.2 +======================== + +The IsFileSelectableEventListener class has been removed completely. This is a +breaking change. However, since the previous implementation required patching the +TYPO3 Core to make this feature work, removing this class should not cause +additional issues in practice. + +The configuration option `typo3RequiredColumnsForFiles` is still available, but +the feature has been reimplemented in a different way. Once enabled, the TYPO3 +GalleryProcessor is overridden and all files that do not have the required column +values assigned are removed from the processed output. + +In addition, a new control icon is added to each `sys_file_reference` record in +backend forms where file references are configured. When hovering over this icon, +editors can see which required columns still need to be completed. + Upgrade to Version 7.0.0 ======================== From 80ff69e782e4b07b3469558c76438b5936adf38f Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Wed, 20 May 2026 16:01:31 +0200 Subject: [PATCH 5/7] [DOCS] Refine explanation for required image fields - Clarified wording in ExtConf.xlf for mandatory image fields - Specified impact on image visibility in the frontend --- Resources/Private/Language/ExtConf.xlf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/Private/Language/ExtConf.xlf b/Resources/Private/Language/ExtConf.xlf index c2f0b95e..6662292f 100644 --- a/Resources/Private/Language/ExtConf.xlf +++ b/Resources/Private/Language/ExtConf.xlf @@ -16,9 +16,9 @@ - Required columns for image files: Enter here columns of the DB tables sys_file and/or sys_file_metadata - which are to be regarded as mandatory fields. Files for which these columns are not set are available in the - file browser, but cannot be selected. + Required columns for image files: Enter columns of the DB tables sys_file and/or sys_file_metadata + that should be treated as mandatory fields. Image files for which these columns are not set will not be + displayed in the frontend. From ec949458b375c88d129c624455b6670a6c6c2d2d Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Wed, 20 May 2026 16:09:50 +0200 Subject: [PATCH 6/7] [BUGFIX] Validate only image files in data processors - Added type checks to validate only image files in GalleryDataProcessor - Updated AddMandatoryControlForFileReferencesEventListener to skip non-image records during required column checks --- Classes/DataProcessor/GalleryDataProcessor.php | 5 +++++ .../AddMandatoryControlForFileReferencesEventListener.php | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Classes/DataProcessor/GalleryDataProcessor.php b/Classes/DataProcessor/GalleryDataProcessor.php index 621798ea..06ddf1ce 100644 --- a/Classes/DataProcessor/GalleryDataProcessor.php +++ b/Classes/DataProcessor/GalleryDataProcessor.php @@ -75,6 +75,11 @@ public function process( private function isValidFileObject(FileReference $fileObject, array $requiredColumns): bool { + // Only check image files here + if ($fileObject->getType() !== 2) { + return true; + } + foreach ($requiredColumns as $requiredColumn) { if (!$fileObject->hasProperty($requiredColumn)) { return false; diff --git a/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php b/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php index a05944f5..5d125c0f 100644 --- a/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php +++ b/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php @@ -35,13 +35,19 @@ public function __construct( public function __invoke(ModifyFileReferenceControlsEvent $event): void { + $record = $event->getRecord(); $requiredColumns = $this->getRequiredColumns(); if ($requiredColumns === []) { return; } + // Only check image files here + if ((int)($record['type'] ?? 0) !== 2) { + return; + } + foreach ($requiredColumns as $requiredColumn) { - $value = $this->data['databaseRow']['uid_local'][0][$requiredColumn] ?? $this->data[$requiredColumn] ?? ''; + $value = $record['uid_local'][0][$requiredColumn] ?? $record[$requiredColumn] ?? ''; $trimmedValue = is_string($value) ? trim($value) : $value; if ($trimmedValue === null || $trimmedValue === '') { From 52a2993d6e9d0fbc75f5ed7fa5f3e0b03f42fcbd Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Wed, 20 May 2026 16:11:48 +0200 Subject: [PATCH 7/7] [BUGFIX] Fix formatting issues in PHP syntax - Corrected trailing comma inconsistency in GalleryDataProcessor - Fixed indentation for button HTML in AddMandatoryControlForFileReferencesEventListener --- Classes/DataProcessor/GalleryDataProcessor.php | 2 +- .../AddMandatoryControlForFileReferencesEventListener.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/DataProcessor/GalleryDataProcessor.php b/Classes/DataProcessor/GalleryDataProcessor.php index 06ddf1ce..ce7ea69c 100644 --- a/Classes/DataProcessor/GalleryDataProcessor.php +++ b/Classes/DataProcessor/GalleryDataProcessor.php @@ -40,7 +40,7 @@ public function process( $filesProcessedDataKey = (string)$cObj->stdWrapValue( 'filesProcessedDataKey', $processorConfiguration, - 'files' + 'files', ); if (isset($processedData[$filesProcessedDataKey]) && is_array($processedData[$filesProcessedDataKey])) { diff --git a/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php b/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php index 5d125c0f..b73b3060 100644 --- a/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php +++ b/Classes/EventListener/AddMandatoryControlForFileReferencesEventListener.php @@ -55,8 +55,8 @@ public function __invoke(ModifyFileReferenceControlsEvent $event): void 'requiredColumns', ' - '); + ', + ); return; } }