From e34543b3e7969ca3c527ed626ddccf7e9de4e1f9 Mon Sep 17 00:00:00 2001
From: Larry Garfield <larry@garfieldtech.com>
Date: Tue, 31 May 2022 19:21:00 -0500
Subject: [PATCH] [!!!][FEATURE] Add PSR-14 events for the Link Browser
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This introduces two new events to replace the old modify hooks
in LinkBrowser.  They are a direct 1:1 replacement.

Resolves: #97454
Releases: main
Change-Id: Ic79b3c9c56452188323574d92b9fe91c37a2da29
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/74798
Tested-by: core-ci <typo3@b13.com>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 .../Breaking-97454-RemoveLinkBrowserHooks.rst |  36 ++++++
 ...454-PSR14EventsForLinkBrowserLifecycle.rst |  55 +++++++++
 .../Php/ArrayDimensionMatcher.php             |   6 +
 .../AbstractLinkBrowserController.php         |  35 +++---
 .../Classes/Event/ModifyAllowedItemsEvent.php |  62 +++++++++++
 .../Classes/Event/ModifyLinkHandlersEvent.php |  74 +++++++++++++
 .../Controller/BrowseLinksControllerTest.php  | 104 ++++++++++++++++++
 7 files changed, 353 insertions(+), 19 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97454-RemoveLinkBrowserHooks.rst
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Feature-97454-PSR14EventsForLinkBrowserLifecycle.rst
 create mode 100644 typo3/sysext/recordlist/Classes/Event/ModifyAllowedItemsEvent.php
 create mode 100644 typo3/sysext/recordlist/Classes/Event/ModifyLinkHandlersEvent.php
 create mode 100644 typo3/sysext/recordlist/Tests/Functional/RecordList/Controller/BrowseLinksControllerTest.php

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 000000000000..3b01c8cbfc17
--- /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 000000000000..1e0fdddc3bc2
--- /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 7abd32237386..afc40d6f7c3f 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 012cff17ab68..04d28df109b8 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 000000000000..81146c2d310c
--- /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 000000000000..334a2058d021
--- /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 000000000000..bc731da6d7c7
--- /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']);
+    }
+}
-- 
GitLab