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

This introduces two new events to interact with the backend
controller page generation process. They mirror and replace
the renderPreProcess and renderPostProcess hooks, which are removed.

The vestigial constructorPostProcess hook is also removed entirely.

Resolves: #97451
Releases: main
Change-Id: I58bccf341b478eedc87c9a9dd81fac788bba0a91
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/74797
Tested-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 .../Classes/Controller/BackendController.php  | 38 ++--------
 .../Event/AfterBackendPageRenderEvent.php     | 48 +++++++++++++
 .../Controller/BackendControllerTest.php      | 72 +++++++++++++++++++
 ...97451-RemoveBackendControllerPageHooks.rst | 40 +++++++++++
 ...1-PSR-14EventsForBackendPageController.rst | 53 ++++++++++++++
 .../Php/ArrayDimensionMatcher.php             | 18 +++++
 .../Php/MethodCallMatcher.php                 |  7 ++
 7 files changed, 242 insertions(+), 34 deletions(-)
 create mode 100644 typo3/sysext/backend/Classes/Controller/Event/AfterBackendPageRenderEvent.php
 create mode 100644 typo3/sysext/backend/Tests/Functional/Controller/BackendControllerTest.php
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97451-RemoveBackendControllerPageHooks.rst
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Feature-97451-PSR-14EventsForBackendPageController.rst

diff --git a/typo3/sysext/backend/Classes/Controller/BackendController.php b/typo3/sysext/backend/Classes/Controller/BackendController.php
index f7c3472b5fcf..2d51a0066bbf 100644
--- a/typo3/sysext/backend/Classes/Controller/BackendController.php
+++ b/typo3/sysext/backend/Classes/Controller/BackendController.php
@@ -17,8 +17,10 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Backend\Controller;
 
+use Psr\EventDispatcher\EventDispatcherInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent;
 use TYPO3\CMS\Backend\Module\MenuModule;
 use TYPO3\CMS\Backend\Module\ModuleInterface;
 use TYPO3\CMS\Backend\Module\ModuleProvider;
@@ -50,8 +52,6 @@ class BackendController
 {
     use PageRendererBackendSetupTrait;
 
-    protected string $css = '';
-
     /**
      * @var ModuleInterface[]
      */
@@ -65,9 +65,8 @@ class BackendController
         protected readonly ToolbarItemsRegistry $toolbarItemsRegistry,
         protected readonly ExtensionConfiguration $extensionConfiguration,
         protected readonly BackendViewFactory $viewFactory,
+        protected readonly EventDispatcherInterface $eventDispatcher,
     ) {
-        // @todo: This hook is essentially useless.
-        $this->executeHook('constructPostProcess');
         $this->modules = $this->moduleProvider->getModulesForModuleMenu($this->getBackendUser());
     }
 
@@ -79,8 +78,6 @@ class BackendController
         $backendUser = $this->getBackendUser();
         $pageRenderer = $this->pageRenderer;
 
-        $this->executeHook('renderPreProcess');
-
         $this->setUpBasicPageRendererForBackend($pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService());
 
         $javaScriptRenderer = $pageRenderer->getJavaScriptRenderer();
@@ -131,7 +128,6 @@ class BackendController
         $dateFormat = ['DD-MM-Y', 'HH:mm DD-MM-Y'];
         // Needed for FormEngine manipulation (date picker)
         $pageRenderer->addInlineSetting('DateTimePicker', 'DateFormat', $dateFormat);
-        $pageRenderer->addCssInlineBlock('BackendInlineCSS', $this->css);
         $typo3Version = 'TYPO3 CMS ' . $this->typo3Version->getVersion();
         $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . ' [' . $typo3Version . ']' : $typo3Version;
         $pageRenderer->setTitle($title);
@@ -148,7 +144,7 @@ class BackendController
             'sitenameFirstInBackendTitle' => ($backendUser->uc['backendTitleFormat'] ?? '') === 'sitenameFirst',
         ]);
         $content = $view->render('Backend/Main');
-        $this->executeHook('renderPostProcess', ['content' => &$content]);
+        $content = $this->eventDispatcher->dispatch(new AfterBackendPageRenderEvent($content, $view))->getContent();
         $bodyTag = '<body class="scaffold t3js-scaffold' . (!$moduleMenuCollapsed && $this->modules ? ' scaffold-modulemenu-expanded' : '') . '">';
         $pageRenderer->addBodyContent($bodyTag . $content);
         return $pageRenderer->renderResponse();
@@ -177,16 +173,6 @@ class BackendController
         return new JsonResponse(['topbar' => $view->render('Backend/Topbar')]);
     }
 
-    /**
-     * Adds a css snippet to the backend. This method is old and its purpose
-     * seems to be that hooks (see executeHook()) can add css?
-     * @todo: Candidate for deprecation / removal.
-     */
-    public function addCss(string $css): void
-    {
-        $this->css .= $css;
-    }
-
     /**
      * Renders the topbar, containing the backend logo, sitename etc.
      */
@@ -365,22 +351,6 @@ class BackendController
         return $modules;
     }
 
-    /**
-     * Executes defined hooks functions for the given identifier.
-     *
-     * These hook identifiers are valid:
-     * + constructPostProcess
-     * + renderPreProcess
-     * + renderPostProcess
-     */
-    protected function executeHook(string $identifier, array $hookConfiguration = []): void
-    {
-        $options = &$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php'];
-        foreach ($options[$identifier] ?? [] as $hookFunction) {
-            GeneralUtility::callUserFunction($hookFunction, $hookConfiguration, $this);
-        }
-    }
-
     protected function getCollapseStateOfMenu(): bool
     {
         $backendUser = $this->getBackendUser();
diff --git a/typo3/sysext/backend/Classes/Controller/Event/AfterBackendPageRenderEvent.php b/typo3/sysext/backend/Classes/Controller/Event/AfterBackendPageRenderEvent.php
new file mode 100644
index 000000000000..c4c3aa531a90
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Controller/Event/AfterBackendPageRenderEvent.php
@@ -0,0 +1,48 @@
+<?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\Backend\Controller\Event;
+
+use TYPO3\CMS\Core\View\ViewInterface;
+
+/**
+ * This event triggers after a page has been rendered.
+ *
+ * Listeners may update the page content string with a modified
+ * version if appropriate.
+ */
+final class AfterBackendPageRenderEvent
+{
+    public function __construct(private string $content, private readonly ViewInterface $view)
+    {
+    }
+
+    public function getContent(): string
+    {
+        return $this->content;
+    }
+
+    public function setContent(string $content): void
+    {
+        $this->content = $content;
+    }
+
+    public function getView(): ViewInterface
+    {
+        return $this->view;
+    }
+}
diff --git a/typo3/sysext/backend/Tests/Functional/Controller/BackendControllerTest.php b/typo3/sysext/backend/Tests/Functional/Controller/BackendControllerTest.php
new file mode 100644
index 000000000000..2bf63e6622b0
--- /dev/null
+++ b/typo3/sysext/backend/Tests/Functional/Controller/BackendControllerTest.php
@@ -0,0 +1,72 @@
+<?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\Backend\Tests\Functional\Controller;
+
+use Symfony\Component\DependencyInjection\Container;
+use TYPO3\CMS\Backend\Controller\BackendController;
+use TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent;
+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\ServerRequest;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class BackendControllerTest extends FunctionalTestCase
+{
+    public function setUp(): void
+    {
+        parent::setUp();
+        $this->setUpBackendUserFromFixture(1);
+        Bootstrap::initializeLanguageObject();
+    }
+
+    /**
+     * @test
+     */
+    public function backendPageRenderEventIsTriggered(): void
+    {
+        /** @var Container $container */
+        $container = $this->getContainer();
+
+        $state = [
+            'after-backend-page-render-listener' => null,
+        ];
+
+        // Dummy listeners that just record that the event existed.
+        $container->set(
+            'after-backend-page-render-listener',
+            static function (AfterBackendPageRenderEvent $event) use (&$state) {
+                $state['after-backend-page-render-listener'] = $event;
+            }
+        );
+
+        $eventListener = GeneralUtility::makeInstance(ListenerProvider::class);
+        $eventListener->addListener(AfterBackendPageRenderEvent::class, 'after-backend-page-render-listener');
+
+        $request = (new ServerRequest())
+            ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE)
+            ->withAttribute('route', new Route('/main', ['packageName' => 'typo3/cms-backend', '_identifier' => 'main']));
+
+        $subject = $this->get(BackendController::class);
+        $subject->mainAction($request);
+
+        self::assertInstanceOf(AfterBackendPageRenderEvent::class, $state['after-backend-page-render-listener']);
+    }
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97451-RemoveBackendControllerPageHooks.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97451-RemoveBackendControllerPageHooks.rst
new file mode 100644
index 000000000000..9459245d7ae8
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-97451-RemoveBackendControllerPageHooks.rst
@@ -0,0 +1,40 @@
+.. include:: /Includes.rst.txt
+
+=======================================================
+Breaking: #97451 - Removed BackendController page hooks
+=======================================================
+
+See :issue:`97451`
+
+Description
+===========
+
+The hooks :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['constructorPostProcess']`,
+:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['renderPreProcess']`, and
+:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['renderPostProcess']` have
+been removed in favor of a new PSR-14 Event :php:`\TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent`.
+
+Additionally, the :php:`BackendController->addCss()` method has been removed without replacement,
+as it is no longer used.
+
+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-97451-PSR-14EventsForBackendPageController>`
+as an improved replacement.
+
+.. index:: Backend, PHP-API, FullyScanned, ext:backend
diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97451-PSR-14EventsForBackendPageController.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97451-PSR-14EventsForBackendPageController.rst
new file mode 100644
index 000000000000..addd0e3cd1b2
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97451-PSR-14EventsForBackendPageController.rst
@@ -0,0 +1,53 @@
+.. include:: /Includes.rst.txt
+
+==================================================================
+Feature: #97451 - PSR-14 Events for modifying backend page content
+==================================================================
+
+See :issue:`97451`
+
+Description
+===========
+A new PSR-14 Event :php:`\TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent` has
+been introduced which serves as a direct replacement for the now removed
+:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['constructorPostProcess']`,
+:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['renderPreProcess']`, and
+:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php']['renderPostProcess']`
+:doc:`hooks <../12.0/Breaking-97451-RemoveBackendControllerPageHooks>`.
+
+The new Event triggers after the page is rendered and includes
+the rendered page body. Listeners may overwrite the page string if desired.
+
+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/backend/after-backend-controller-render'
+
+The corresponding event listener class:
+
+.. code-block:: php
+
+    use TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent;
+
+    final class MyEventListener
+    {
+        public function __invoke(AfterBackendPageRenderEvent $event): void
+        {
+            $content = $event->getContent() . ' I was here';
+            $event->setContent($content);
+        }
+    }
+
+Impact
+======
+
+It's now possible to modify the backend page using the new PSR-14 :php:`AfterBackendPageRenderEvent`.
+
+.. 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 7598197321de..7abd32237386 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php
@@ -784,4 +784,22 @@ return [
             'Feature-97862-NewPSR-14EventsForManipulatingFrontendPageGenerationAndCacheBehaviour.rst',
         ],
     ],
+    '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'typo3/backend.php\'][\'constructorPostProcess\']' => [
+        'restFiles' => [
+            'Breaking-97451-RemoveBackendControllerPageHooks.rst',
+            'Feature-97451-PSR-14EventsForBackendPageController.rst',
+        ],
+    ],
+    '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'typo3/backend.php\'][\'renderPreProcess\']' => [
+        'restFiles' => [
+            'Breaking-97451-RemoveBackendControllerPageHooks.rst',
+            'Feature-97451-PSR-14EventsForBackendPageController.rst',
+        ],
+    ],
+    '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'typo3/backend.php\'][\'renderPostProcess\']' => [
+        'restFiles' => [
+            'Breaking-97451-RemoveBackendControllerPageHooks.rst',
+            'Feature-97451-PSR-14EventsForBackendPageController.rst',
+        ],
+    ],
 ];
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
index d22e0374c2c2..cbcb02cbd880 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
@@ -5323,4 +5323,11 @@ return [
             'Deprecation-97531-ContextRelatedMethodsWithinTSFE.rst',
         ],
     ],
+    'TYPO3\CMS\Backend\Controller\BackendController->addCss' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Breaking-97451-RemoveBackendControllerPageHooks.rst',
+        ],
+    ],
 ];
-- 
GitLab