From 19dc7fa901111bebb6b56dd2768dca8d38c311fd Mon Sep 17 00:00:00 2001
From: Alexander Schnitzler <git@alexanderschnitzler.de>
Date: Mon, 30 Mar 2020 17:32:32 +0200
Subject: [PATCH] [BUGFIX] Re-enable dynamic resolving of view objects

With https://review.typo3.org/c/Packages/TYPO3.CMS/+/59514/ all
possibilities have been taken to define which view class to use
based on the current plugin environment parameters like controller
name, action name and format. Since this is used a lot in user land
code, this functionality has been re-enabled for version 10. But
still, the main flaw of the original code, i.e. checking for
possibly defined classes, is avoided. Instead, users can implement
a deterministic view resolver which ensures view objects that
implement the ViewInterface.

Releases: master
Resolves: #90892
Change-Id: Ide2919a6d86b2904087d3d7aa8dfece1abee4658
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64017
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
---
 .../Mvc/Controller/ActionController.php       | 36 ++++++++++--
 .../Classes/Mvc/View/GenericViewResolver.php  | 55 +++++++++++++++++++
 .../Mvc/View/ViewResolverInterface.php        | 16 ++++++
 .../extbase/Configuration/Services.yaml       |  3 +
 .../Mvc/Controller/ActionControllerTest.php   | 26 +++++++++
 .../Fixture/Controller/TestController.php     |  5 ++
 6 files changed, 137 insertions(+), 4 deletions(-)
 create mode 100644 typo3/sysext/extbase/Classes/Mvc/View/GenericViewResolver.php
 create mode 100644 typo3/sysext/extbase/Classes/Mvc/View/ViewResolverInterface.php

diff --git a/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php b/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php
index e8da5211583e..ec6f3867f34a 100644
--- a/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php
+++ b/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php
@@ -23,7 +23,9 @@ use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
 use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionCallEvent;
 use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
+use TYPO3\CMS\Extbase\Mvc\View\GenericViewResolver;
 use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
+use TYPO3\CMS\Extbase\Mvc\View\ViewResolverInterface;
 use TYPO3\CMS\Extbase\Mvc\Web\ReferringRequest;
 use TYPO3\CMS\Extbase\Security\Cryptography\HashService;
 use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
@@ -50,6 +52,11 @@ class ActionController implements ControllerInterface
      */
     protected $hashService;
 
+    /**
+     * @var ViewResolverInterface
+     */
+    private $viewResolver;
+
     /**
      * The current view, as resolved by resolveView()
      *
@@ -103,6 +110,15 @@ class ActionController implements ControllerInterface
      */
     protected $response;
 
+    /**
+     * @param ViewResolverInterface $viewResolver
+     * @internal
+     */
+    public function injectViewResolver(ViewResolverInterface $viewResolver)
+    {
+        $this->viewResolver = $viewResolver;
+    }
+
     /**
      * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
      */
@@ -371,10 +387,22 @@ class ActionController implements ControllerInterface
      */
     protected function resolveView()
     {
-        $view = null;
-        if ($this->defaultViewObjectName != '') {
-            /** @var ViewInterface $view */
-            $view = $this->objectManager->get($this->defaultViewObjectName);
+        if ($this->viewResolver instanceof GenericViewResolver) {
+            /*
+             * This setter is not part of the ViewResolverInterface as it's only necessary to set
+             * the default view class from this point when using the generic view resolver which
+             * must respect the possibly overridden property defaultViewObjectName.
+             */
+            $this->viewResolver->setDefaultViewClass($this->defaultViewObjectName);
+        }
+
+        $view = $this->viewResolver->resolve(
+            $this->request->getControllerObjectName(),
+            $this->request->getControllerActionName(),
+            $this->request->getFormat()
+        );
+
+        if ($view instanceof ViewInterface) {
             $this->setViewConfiguration($view);
             if ($view->canRender($this->controllerContext) === false) {
                 $view = null;
diff --git a/typo3/sysext/extbase/Classes/Mvc/View/GenericViewResolver.php b/typo3/sysext/extbase/Classes/Mvc/View/GenericViewResolver.php
new file mode 100644
index 000000000000..766c85c4393b
--- /dev/null
+++ b/typo3/sysext/extbase/Classes/Mvc/View/GenericViewResolver.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Extbase\Mvc\View;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+
+/**
+ * @internal only to be used within Extbase, not part of TYPO3 Core API.
+ */
+class GenericViewResolver implements ViewResolverInterface
+{
+    /**
+     * @var ObjectManager
+     */
+    private $objectManager;
+
+    /**
+     * @var string
+     */
+    private $defaultViewClass;
+
+    public function __construct(ObjectManager $objectManager)
+    {
+        $this->objectManager = $objectManager;
+    }
+
+    /**
+     * @param string $defaultViewClass
+     * @internal
+     */
+    public function setDefaultViewClass(string $defaultViewClass): void
+    {
+        $this->defaultViewClass = $defaultViewClass;
+    }
+
+    public function resolve(string $controllerObjectName, string $actionName, string $format): ViewInterface
+    {
+        return $this->objectManager->get($this->defaultViewClass);
+    }
+}
diff --git a/typo3/sysext/extbase/Classes/Mvc/View/ViewResolverInterface.php b/typo3/sysext/extbase/Classes/Mvc/View/ViewResolverInterface.php
new file mode 100644
index 000000000000..27ee2f9c3349
--- /dev/null
+++ b/typo3/sysext/extbase/Classes/Mvc/View/ViewResolverInterface.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Extbase\Mvc\View;
+
+/**
+ * @internal only to be used within Extbase, not part of TYPO3 Core API.
+ *
+ * It's safe to use this interface in TYPO3 10 LTS as it will not be changed or removed in that
+ * version but this interface is likely to be removed and/or changed in version 11.
+ */
+interface ViewResolverInterface
+{
+    public function resolve(string $controllerObjectName, string $actionName, string $format): ViewInterface;
+}
diff --git a/typo3/sysext/extbase/Configuration/Services.yaml b/typo3/sysext/extbase/Configuration/Services.yaml
index e393761fbb7b..d0c5dfda66bc 100644
--- a/typo3/sysext/extbase/Configuration/Services.yaml
+++ b/typo3/sysext/extbase/Configuration/Services.yaml
@@ -83,3 +83,6 @@ services:
         identifier: 'legacy-slot'
         method: 'emitAfterPersistObjectSignal'
         event: TYPO3\CMS\Extbase\Event\Persistence\EntityPersistedEvent
+
+  TYPO3\CMS\Extbase\Mvc\View\GenericViewResolver: ~
+  TYPO3\CMS\Extbase\Mvc\View\ViewResolverInterface: '@TYPO3\CMS\Extbase\Mvc\View\GenericViewResolver'
diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerTest.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerTest.php
index e8fabffe2505..e68796e94346 100644
--- a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerTest.php
+++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerTest.php
@@ -18,6 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Extbase\Tests\Functional\Mvc\Controller;
 
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Mvc\View\JsonView;
 use TYPO3\CMS\Extbase\Mvc\Web\Request;
 use TYPO3\CMS\Extbase\Mvc\Web\Response;
 use TYPO3\CMS\Extbase\Object\ObjectManager;
@@ -118,4 +119,29 @@ class ActionControllerTest extends \TYPO3\TestingFramework\Core\Functional\Funct
         $validators->rewind();
         self::assertInstanceOf(NotEmptyValidator::class, $validators->current());
     }
+
+    /**
+     * @test
+     */
+    public function resolveViewRespectsDefaultViewObjectName()
+    {
+        // Test setup
+        $reflectionClass = new \ReflectionClass($this->controller);
+        $reflectionMethod = $reflectionClass->getProperty('defaultViewObjectName');
+        $reflectionMethod->setAccessible(true);
+        $reflectionMethod->setValue($this->controller, JsonView::class);
+
+        $this->request->setControllerActionName('qux');
+
+        // Test run
+        $this->controller->processRequest($this->request, $this->response);
+
+        // Assertions
+        $reflectionMethod = $reflectionClass->getProperty('view');
+        $reflectionMethod->setAccessible(true);
+        $reflectionMethod->getValue($this->controller);
+
+        $view = $reflectionMethod->getValue($this->controller);
+        self::assertInstanceOf(JsonView::class, $view);
+    }
 }
diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Controller/TestController.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Controller/TestController.php
index 32f9ec608e86..6f2b22aead7c 100644
--- a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Controller/TestController.php
+++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Controller/TestController.php
@@ -71,4 +71,9 @@ class TestController extends ActionController
         // return string so we don't need to mock a view
         return '';
     }
+
+    public function quxAction()
+    {
+        return '';
+    }
 }
-- 
GitLab