diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97454-RemoveLinkBrowserHooks.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97454-RemoveLinkBrowserHooks.rst new file mode 100644 index 0000000000000000000000000000000000000000..3b01c8cbfc17b3b4d7c03d1b8c07cfef6e6f15b7 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97454-RemoveLinkBrowserHooks.rst @@ -0,0 +1,36 @@ +.. include:: /Includes.rst.txt + +============================================= +Breaking: #97454 - Removed Link Browser hooks +============================================= + +See :issue:`97454` + +Description +=========== + +The hooks array :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks']` has been +removed in favor of new PSR-14 Events :php:`\TYPO3\CMS\Recordlist\Event\ModifyLinkHandlersEvent` +and :php:`\TYPO3\CMS\Recordlist\Event\ModifyAllowedItemsEvent`. + +Impact +====== + +Any hook implementation registered is not executed anymore in +TYPO3 v12.0+. The extension scanner will report possible usages. + +Affected Installations +====================== + +All TYPO3 installations using this hook in custom extension code. + +Migration +========= + +The hooks are removed without deprecation in order to allow extensions +to work with TYPO3 v11 (using the hook) and v12+ (using the new Event) +when implementing the Event as well without any further deprecations. +Use the :doc:`PSR-14 Event <../12.0/Feature-97454-PSR-14EventsForLinkBrowserLifecycle>` +as an improved replacement. + +.. index:: Backend, PHP-API, FullyScanned, ext:backend diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97454-PSR14EventsForLinkBrowserLifecycle.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97454-PSR14EventsForLinkBrowserLifecycle.rst new file mode 100644 index 0000000000000000000000000000000000000000..1e0fdddc3bc2f11927778c411f86d06c952c9326 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97454-PSR14EventsForLinkBrowserLifecycle.rst @@ -0,0 +1,55 @@ +.. include:: /Includes.rst.txt + +=================================================================== +Feature: #97454 - PSR-14 Events for modifying link browser behavior +=================================================================== + +See :issue:`97454` + +Description +=========== + +Two new PSR-14 Events :php:`\TYPO3\CMS\Recordlist\Event\ModifyLinkHandlersEvent` and +:php:`\TYPO3\CMS\Recordlist\Event\ModifyAllowedItemsEvent` have been introduced which +serves as a direct replacement for the now removed +:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks']` array. +:doc:`hooks <../12.0/Breaking-97454-RemoveLinkBrowserHooks>`. +The first is triggered before link handlers are executed, allowing listeners +to modify the set of handlers that will be used. The second + + +Example +======= + +Registration of the Event in your extension's :file:`Services.yaml`: + +.. code-block:: yaml + + MyVendor\MyPackage\MyEventListener: + tags: + - name: event.listener + identifier: 'my-package/recordlist/link-handlers' + +The corresponding event listener class: + +.. code-block:: php + + use TYPO3\CMS\Recordlist\Event\ModifyLinkHandlersEvent; + + final class MyEventListener + { + public function __invoke(ModifyLinkHandlersEvent $event): void + { + $handler = $event->getHandler('url.'); + $handler['label'] = 'My custom label'; + $event->setHandler('url.', $handler); + } + } + +Impact +====== + +It's now possible to modify link handlers behavior using the new PSR-14 +:php:`ModifyLinkHandlersEvent` and :php:`ModifyAllowedItemsEvent`. + +.. index:: Backend, PHP-API, ext:backend diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php index 7abd32237386fb2eb03bbde140d74a750a2e48be..afc40d6f7c3f8d6f030888b656fe01872367a833 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php @@ -802,4 +802,10 @@ return [ 'Feature-97451-PSR-14EventsForBackendPageController.rst', ], ], + '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'LinkBrowser\'][\'hooks\']' => [ + 'restFiles' => [ + 'Breaking-97454-RemoveLinkBrowserHooks.rst', + 'Feature-97454-PSR14EventsForLinkBrowserLifecycle.rst', + ], + ], ]; diff --git a/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php b/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php index 012cff17ab68b320cdb868b8940e5c6105cbf6f9..04d28df109b8c42d3d0530dd5308c667007164f0 100644 --- a/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php +++ b/typo3/sysext/recordlist/Classes/Controller/AbstractLinkBrowserController.php @@ -17,6 +17,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Recordlist\Controller; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Routing\UriBuilder; @@ -32,6 +33,8 @@ use TYPO3\CMS\Core\Service\DependencyOrderingService; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\HttpUtility; use TYPO3\CMS\Core\View\ViewInterface; +use TYPO3\CMS\Recordlist\Event\ModifyAllowedItemsEvent; +use TYPO3\CMS\Recordlist\Event\ModifyLinkHandlersEvent; use TYPO3\CMS\Recordlist\LinkHandler\LinkHandlerInterface; /** @@ -94,14 +97,15 @@ abstract class AbstractLinkBrowserController * @var string[] */ protected array $linkAttributeValues = []; + protected array $parameters; - protected array $hookObjects = []; protected DependencyOrderingService $dependencyOrderingService; protected PageRenderer $pageRenderer; protected UriBuilder $uriBuilder; protected ExtensionConfiguration $extensionConfiguration; protected BackendViewFactory $backendViewFactory; + protected EventDispatcherInterface $eventDispatcher; public function injectDependencyOrderingService(DependencyOrderingService $dependencyOrderingService): void { @@ -128,6 +132,11 @@ abstract class AbstractLinkBrowserController $this->backendViewFactory = $backendViewFactory; } + public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->eventDispatcher = $eventDispatcher; + } + abstract public function getConfiguration(): array; abstract protected function initDocumentTemplate(): void; @@ -143,10 +152,6 @@ abstract class AbstractLinkBrowserController */ public function mainAction(ServerRequestInterface $request): ResponseInterface { - $hooks = $this->dependencyOrderingService->orderByDependencies($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks'] ?? []); - foreach ($hooks as $hook) { - $this->hookObjects[] = GeneralUtility::makeInstance($hook['handler']); - } $this->getLanguageService()->includeLLFile('EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf'); $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService()); @@ -280,13 +285,9 @@ abstract class AbstractLinkBrowserController protected function getLinkHandlers(): array { $linkHandlers = (array)(BackendUtility::getPagesTSconfig($this->getCurrentPageId())['TCEMAIN.']['linkHandler.'] ?? []); - foreach ($this->hookObjects as $hookObject) { - if (method_exists($hookObject, 'modifyLinkHandlers')) { - $linkHandlers = $hookObject->modifyLinkHandlers($linkHandlers, $this->currentLinkParts); - } - } - - return $linkHandlers; + return $this->eventDispatcher + ->dispatch(new ModifyLinkHandlersEvent($linkHandlers, $this->currentLinkParts)) + ->getLinkHandlers(); } /** @@ -391,13 +392,9 @@ abstract class AbstractLinkBrowserController */ protected function getAllowedItems(): array { - $allowedItems = array_keys($this->linkHandlers); - - foreach ($this->hookObjects as $hookObject) { - if (method_exists($hookObject, 'modifyAllowedItems')) { - $allowedItems = $hookObject->modifyAllowedItems($allowedItems, $this->currentLinkParts); - } - } + $allowedItems = $this->eventDispatcher + ->dispatch(new ModifyAllowedItemsEvent(array_keys($this->linkHandlers), $this->currentLinkParts)) + ->getAllowedItems(); if (isset($this->parameters['params']['allowedTypes'])) { $allowedItems = array_intersect($allowedItems, GeneralUtility::trimExplode(',', $this->parameters['params']['allowedTypes'], true)); diff --git a/typo3/sysext/recordlist/Classes/Event/ModifyAllowedItemsEvent.php b/typo3/sysext/recordlist/Classes/Event/ModifyAllowedItemsEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..81146c2d310cee6130695a566fa6a400c014e85c --- /dev/null +++ b/typo3/sysext/recordlist/Classes/Event/ModifyAllowedItemsEvent.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS 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! + */ + +namespace TYPO3\CMS\Recordlist\Event; + +/** + * This event allows extensions to add or remove from the list of allowed link types. + */ +final class ModifyAllowedItemsEvent +{ + /** + * @param string[] $allowedItems + * @param array<string, mixed> $currentLinkParts + */ + public function __construct( + protected array $allowedItems, + protected array $currentLinkParts, + ) { + } + + /** + * @return string[] + */ + public function getAllowedItems(): array + { + return $this->allowedItems; + } + + public function addAllowedItem(string $item): self + { + $this->allowedItems[] = $item; + return $this; + } + + public function removeAllowedItem(string $new): self + { + $this->allowedItems = array_filter($this->allowedItems, static fn (string $item): bool => $item !== $new); + return $this; + } + + /** + * @return array<string, mixed> + */ + public function getCurrentLinkParts(): array + { + return $this->currentLinkParts; + } +} diff --git a/typo3/sysext/recordlist/Classes/Event/ModifyLinkHandlersEvent.php b/typo3/sysext/recordlist/Classes/Event/ModifyLinkHandlersEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..334a2058d021d31c087545e06e6f7488e70ccde8 --- /dev/null +++ b/typo3/sysext/recordlist/Classes/Event/ModifyLinkHandlersEvent.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS 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! + */ + +namespace TYPO3\CMS\Recordlist\Event; + +/** + * This event allows extensions to modify the list of link handlers and their configuration before they are invoked. + */ +final class ModifyLinkHandlersEvent +{ + /** + * @param array<string, array> $linkHandlers + * @param array<string, mixed> $currentLinkParts + */ + public function __construct( + protected array $linkHandlers, + protected array $currentLinkParts, + ) { + } + + /** + * @return array<string, array> + */ + public function getLinkHandlers(): array + { + return $this->linkHandlers; + } + + /** + * Gets an individual handler by name. + * + * @param string $name The handler name, including trailing period. + * @return array<string, mixed>|null The handler definition, or null if not defined. + */ + public function getLinkHandler(string $name): ?array + { + return $this->linkHandlers[$name] ?? null; + } + + /** + * Sets a handler by name, overwriting it if it already exists. + * + * @param string $name The handler name, including trailing period. + * @param array<string, mixed> $handler + * @return $this + */ + public function setLinkHandler(string $name, array $handler): self + { + $this->linkHandlers[$name] = $handler; + return $this; + } + + /** + * @return array<string, mixed> + */ + public function getCurrentLinkParts(): array + { + return $this->currentLinkParts; + } +} diff --git a/typo3/sysext/recordlist/Tests/Functional/RecordList/Controller/BrowseLinksControllerTest.php b/typo3/sysext/recordlist/Tests/Functional/RecordList/Controller/BrowseLinksControllerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..bc731da6d7c7b94e5d2856a6428f4085a2d1060b --- /dev/null +++ b/typo3/sysext/recordlist/Tests/Functional/RecordList/Controller/BrowseLinksControllerTest.php @@ -0,0 +1,104 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS 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! + */ + +namespace TYPO3\CMS\Recordlist\Tests\Functional\RecordList\Controller; + +use Symfony\Component\DependencyInjection\Container; +use TYPO3\CMS\Backend\Routing\Route; +use TYPO3\CMS\Core\Core\Bootstrap; +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\EventDispatcher\ListenerProvider; +use TYPO3\CMS\Core\Http\NormalizedParams; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Recordlist\Event\ModifyAllowedItemsEvent; +use TYPO3\CMS\Recordlist\Event\ModifyLinkHandlersEvent; +use TYPO3\CMS\RteCKEditor\Controller\BrowseLinksController; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class BrowseLinksControllerTest extends FunctionalTestCase +{ + protected array $coreExtensionsToLoad = [ + 'rte_ckeditor', + ]; + + public function setUp(): void + { + parent::setUp(); + $this->setUpBackendUserFromFixture(1); + Bootstrap::initializeLanguageObject(); + } + + /** + * @test + */ + public function linkEventsAreTriggered(): void + { + /** @var Container $container */ + $container = $this->getContainer(); + + $state = [ + 'modify-link-handl-listener' => null, + 'after-backend-page-render-listener' => null, + ]; + + // Dummy listeners that just record that the event existed. + $container->set( + 'modify-link-handler-listener', + static function (ModifyLinkHandlersEvent $event) use (&$state) { + $state['modify-link-handler-listener'] = $event; + } + ); + $container->set( + 'modify-allowed-items-listener', + static function (ModifyAllowedItemsEvent $event) use (&$state) { + $state['modify-allowed-items-listener'] = $event; + } + ); + + $eventListener = GeneralUtility::makeInstance(ListenerProvider::class); + $eventListener->addListener(ModifyLinkHandlersEvent::class, 'modify-link-handler-listener'); + $eventListener->addListener(ModifyAllowedItemsEvent::class, 'modify-allowed-items-listener'); + + $request = (new ServerRequest()) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE) + ->withAttribute('route', new Route('/main', [ + 'packageName' => 'typo3/cms-recordlist', + '_identifier' => 'main', + ])) + ->withQueryParams([ + 'editorId' => 'cke_1', + 'contentsLanguage' => 'en', + 'P' => [ + 'table' => 'tt_content', + 'uid' => '1', + 'fieldName' => 'bodytext', + 'recordType' => 'text', + 'pid' => '1', + 'richtextConfigurationName' => '', + ], + ]); + $request = $request->withAttribute('normalizedParams', NormalizedParams::createFromRequest($request)); + + $subject = $this->get(BrowseLinksController::class); + + $subject->mainAction($request); + + self::assertInstanceOf(ModifyLinkHandlersEvent::class, $state['modify-link-handler-listener']); + self::assertInstanceOf(ModifyAllowedItemsEvent::class, $state['modify-allowed-items-listener']); + } +}