Commit 785ff682 authored by Alexander Schnitzler's avatar Alexander Schnitzler Committed by Benni Mack
Browse files

[FEATURE] Introduce ForwardResponse for extbase

This patch introduces a PSR-7 compatible response class
which allows users to initiate forwarding to another
extbase controller action.

Returning a ForwardResponse replaces the helper function
forward() in the ActionController.

Releases: master
Resolves: #92815
Change-Id: I37b40d9e3de1125c0173d2115e0224cb1b13dc2f
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/66564


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Torben Hansen's avatarTorben Hansen <derhansen@gmail.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Torben Hansen's avatarTorben Hansen <derhansen@gmail.com>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 9e283414
......@@ -35,6 +35,7 @@ use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
use TYPO3\CMS\Core\Session\SessionManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
......@@ -247,7 +248,7 @@ class BackendUserController extends ActionController
*
* @param int $user
*/
public function initiatePasswordResetAction(int $user): void
public function initiatePasswordResetAction(int $user): ResponseInterface
{
$context = GeneralUtility::makeInstance(Context::class);
/** @var BackendUser $user */
......@@ -271,7 +272,7 @@ class BackendUserController extends ActionController
FlashMessage::OK
);
}
$this->forward('index');
return new ForwardResponse('index');
}
/**
......@@ -283,7 +284,7 @@ class BackendUserController extends ActionController
{
$this->moduleData->attachUidCompareUser($uid);
$this->moduleDataStorageService->persistModuleData($this->moduleData);
$this->forward('index');
return new ForwardResponse('index');
}
/**
......@@ -331,7 +332,7 @@ class BackendUserController extends ActionController
if ($success) {
$this->addFlashMessage(LocalizationUtility::translate('LLL:EXT:beuser/Resources/Private/Language/locallang.xlf:terminateSessionSuccess', 'beuser') ?? '');
}
$this->forward('online');
return new ForwardResponse('online');
}
/**
......
.. include:: ../../Includes.txt
===============================================================
Deprecation: #92815 - ActionController::forward() is deprecated
===============================================================
See :issue:`92815`
Description
===========
Method :php:`TYPO3\\CMS\\Extbase\\Mvc\\Controller\\ActionController::forward()` is deprecated in favor of returning a :php:`TYPO3\\CMS\\Extbase\\Http\\ForwardResponse` in a controller action instead.
Impact
======
Calling :php:`TYPO3\\CMS\\Extbase\\Mvc\\Controller\\ActionController::forward()`, which itself throws a `TYPO3\\CMS\\Extbase\\Mvc\\Exception\\StopActionException` to initiate the abortion of the current request and to initiate a new request, will trigger a deprecation warning.
Affected Installations
======================
All installations that make use of the helper method :php:`TYPO3\\CMS\\Extbase\\Mvc\\Controller\\ActionController::forward()`.
Migration
=========
Instead of calling this helper method, a controller action must return a :php:`TYPO3\\CMS\\Extbase\\Http\\ForwardResponse`.
Example:
.. code-block:: php
<?php
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class FooController extends ActionController
{
public function listAction()
{
// do something
$this->forward('show');
}
// more actions here
}
.. code-block:: php
<?php
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
class FooController extends ActionController
{
public function listAction(): ResponseInterface
{
// do something
return new ForwardResponse('show');
}
// more actions here
}
.. index:: PHP-API, NotScanned, ext:extbase
.. include:: ../../Includes.txt
=======================================================
Feature: #92815 - Introduce ForwardResponse for extbase
=======================================================
See :issue:`92815`
Description
===========
Since TYPO3 11.0, extbase controller actions can and should return PSR-7 compatible response objects. To allow the initiation of forwarding to another controller action class :php:`TYPO3\\CMS\\Extbase\\Http\\ForwardResponse` has been introduced.
Minimal example:
.. code-block:: php
<?php
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
class FooController extends ActionController
{
public function listAction(): ResponseInterface
{
// do something
return new ForwardResponse('show');
}
public function showAction(): ResponseInterface
{
// do something
}
}
Example that shows the full api:
.. code-block:: php
<?php
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
class FooController extends ActionController
{
public function listAction(): ResponseInterface
{
// do something
return (new ForwardResponse('show'))
->withControllerName('Bar')
->withExtensionName('Baz')
->withArguments(['foo' => 'bar'])
;
}
}
Impact
======
Class :php:`TYPO3\\CMS\\Extbase\\Http\\ForwardResponse` allows users to initiate forwarding to other controller actions with a PSR-7 compatible response object.
.. index:: PHP-API, ext:extbase
......@@ -18,6 +18,7 @@ namespace OliverHader\IrreTutorial\Controller;
use OliverHader\IrreTutorial\Service\QueueService;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
......@@ -95,9 +96,10 @@ abstract class AbstractController extends ActionController
{
if ($this->getQueueService()->isActive()) {
$this->getQueueService()->addValue($this->getRuntimeIdentifier(), $value);
$this->forward('process', 'Queue');
return (new ForwardResponse('process'))->withControllerName('Queue');
}
$this->view->assign('value', $value);
return $this->view->render();
}
/**
......
......@@ -39,7 +39,7 @@ class ContentController extends AbstractController
{
$contents = $this->contentRepository->findAll();
$value = $this->getStructure($contents);
$this->process($value);
return $this->process($value);
}
/**
......@@ -48,7 +48,7 @@ class ContentController extends AbstractController
public function showAction(Content $content)
{
$value = $this->getStructure($content);
$this->process($value);
return $this->process($value);
}
/**
......
......@@ -16,6 +16,7 @@
namespace OliverHader\IrreTutorial\Controller;
use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
use TYPO3\CMS\Extbase\Mvc\Controller\ControllerInterface;
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerException;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
......@@ -47,18 +48,27 @@ class QueueController extends AbstractController
$calls[] = ['Content', 'show', ['content' => (string)$uid]];
}
$this->getQueueService()->set($calls);
$this->forward('process');
return new ForwardResponse('process');
}
public function processAction()
{
$call = $this->getQueueService()->shift();
if ($call === null) {
$this->forward('finish');
return new ForwardResponse('finish');
}
// Clear these states and fetch fresh entities!
$this->getPersistenceManager()->clearState();
$this->forward($call[1], $call[0], null, $call[2] ?? null);
$response = (new ForwardResponse($call[1]))
->withControllerName($call[0]);
$arguments = $call[2] ?? null;
if (is_array($arguments)) {
$response = $response->withArguments($arguments);
}
return $response;
}
public function finishAction()
......
<?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\Extbase\Http;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Extbase\Error\Result;
class ForwardResponse extends Response
{
private string $actionName;
private ?string $controllerName = null;
private ?string $extensionName = null;
private array $arguments = [];
private Result $argumentsValidationResult;
public function __construct(string $actionName)
{
$this->actionName = $actionName;
$this->argumentsValidationResult = new Result();
parent::__construct('php://temp', 204);
}
public function withControllerName(string $controllerName): self
{
$clone = clone $this;
$clone->controllerName = $controllerName;
return $clone;
}
public function withoutControllerName(): self
{
$clone = clone $this;
$clone->controllerName = null;
return $clone;
}
public function withExtensionName(string $extensionName): self
{
$clone = clone $this;
$clone->extensionName = $extensionName;
return $clone;
}
public function withoutExtensionName(): self
{
$clone = clone $this;
$this->extensionName = null;
return $clone;
}
public function withArguments(array $arguments): self
{
$clone = clone $this;
$clone->arguments = $arguments;
return $clone;
}
public function withoutArguments(): self
{
$clone = clone $this;
$this->arguments = [];
return $clone;
}
public function withArgumentsValidationResult(Result $argumentsValidationResult): self
{
$clone = clone $this;
$clone->argumentsValidationResult = $argumentsValidationResult;
return $clone;
}
public function getActionName(): string
{
return $this->actionName;
}
public function getControllerName(): ?string
{
return $this->controllerName;
}
public function getExtensionName(): ?string
{
return $this->extensionName;
}
public function getArguments(): array
{
return $this->arguments;
}
public function getArgumentsValidationResult(): Result
{
return $this->argumentsValidationResult;
}
}
......@@ -18,6 +18,7 @@ namespace TYPO3\CMS\Extbase\Mvc\Controller;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Http\ResponseFactoryInterface;
use TYPO3\CMS\Core\Http\Stream;
use TYPO3\CMS\Core\Messaging\AbstractMessage;
......@@ -27,6 +28,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionCallEvent;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
use TYPO3\CMS\Extbase\Mvc\Controller\Exception\RequiredArgumentMissingException;
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentTypeException;
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchActionException;
......@@ -675,15 +677,17 @@ abstract class ActionController implements ControllerInterface
* We clear the page cache by default on an error as well, as we need to make sure the
* data is re-evaluated when the user changes something.
*
* @return string
* @return ResponseInterface
*/
protected function errorAction()
{
$this->clearCacheOnError();
$this->addErrorFlashMessage();
$this->forwardToReferringRequest();
if (($response = $this->forwardToReferringRequest()) !== null) {
return $response;
}
return $this->getFlattenedValidationErrorMessage();
return $this->htmlResponse($this->getFlattenedValidationErrorMessage());
}
/**
......@@ -730,9 +734,9 @@ abstract class ActionController implements ControllerInterface
* to the originating request. This effectively ends processing of the current request, so do not
* call this method before you have finished the necessary business logic!
*
* @throws StopActionException
* @return ResponseInterface|null
*/
protected function forwardToReferringRequest()
protected function forwardToReferringRequest(): ?ResponseInterface
{
$referringRequest = null;
$referringRequestArguments = $this->request->getInternalArguments()['__referrer'] ?? null;
......@@ -747,21 +751,23 @@ abstract class ActionController implements ControllerInterface
base64_decode($this->hashService->validateAndStripHmac($referringRequestArguments['arguments']))
);
}
// todo: Remove ReferringRequest. It's only used here in this context to trigger the logic of
// \TYPO3\CMS\Extbase\Mvc\Web\ReferringRequest::setArgument() and its parent method which should then
// be extracted from the request class.
$referringRequest = new ReferringRequest();
$referringRequest->setArguments(array_replace_recursive($arguments, $referrerArray));
}
if ($referringRequest !== null) {
$originalRequest = clone $this->request;
$this->request->setOriginalRequest($originalRequest);
$this->request->setOriginalRequestMappingResults($this->arguments->validate());
$this->forward(
$referringRequest->getControllerActionName(),
$referringRequest->getControllerName(),
$referringRequest->getControllerExtensionName(),
$referringRequest->getArguments()
);
return (new ForwardResponse((string)$referringRequest->getControllerActionName()))
->withControllerName((string)$referringRequest->getControllerName())
->withExtensionName((string)$referringRequest->getControllerExtensionName())
->withArguments($referringRequest->getArguments())
->withArgumentsValidationResult($this->arguments->validate())
;
}
return null;
}
/**
......@@ -841,9 +847,15 @@ abstract class ActionController implements ControllerInterface
* @param array|null $arguments Arguments to pass to the target action
* @throws StopActionException
* @see redirect()
* @deprecated since TYPO3 11.0, will be removed in 12.0
*/
public function forward($actionName, $controllerName = null, $extensionName = null, array $arguments = null)
{
trigger_error(
sprintf('Method %s is deprecated. To forward to another action, return a %s instead.', __METHOD__, ForwardResponse::class),
E_USER_DEPRECATED
);
$this->request->setDispatched(false);
$this->request->setControllerActionName($actionName);
......@@ -877,7 +889,6 @@ abstract class ActionController implements ControllerInterface
* @param int $delay (optional) The delay in seconds. Default is no delay.
* @param int $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other
* @throws StopActionException
* @see forward()
*/
protected function redirect($actionName, $controllerName = null, $extensionName = null, array $arguments = null, $pageUid = null, $delay = 0, $statusCode = 303)
{
......
......@@ -21,6 +21,7 @@ use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Extbase\Annotation\IgnoreValidation;
use TYPO3\CMS\Extbase\Event\Mvc\AfterRequestDispatchedEvent;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
use TYPO3\CMS\Extbase\Mvc\Controller\ControllerInterface;
use TYPO3\CMS\Extbase\Mvc\Exception\InfiniteLoopException;
use TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerException;
......@@ -88,6 +89,9 @@ class Dispatcher implements SingletonInterface
$controller = $this->resolveController($request);
try {
$response = $controller->processRequest($request);
if ($response instanceof ForwardResponse) {
$request = static::buildRequestFromCurrentRequestAndForwardResponse($request, $response);
}
} catch (StopActionException $ignoredException) {
$response = $ignoredException->getResponse();
}
......@@ -127,4 +131,32 @@ class Dispatcher implements SingletonInterface
}
return $controller;
}
/**
* @internal only to be used within Extbase, not part of TYPO3 Core API.
* @todo: make this a private method again as soon as the tests, that fake the dispatching of requests, are refactored.
*/
public static function buildRequestFromCurrentRequestAndForwardResponse(Request $currentRequest, ForwardResponse $forwardResponse): Request
{
$request = clone $currentRequest;
$request->setDispatched(false);
$request->setControllerActionName($forwardResponse->getActionName());
if ($forwardResponse->getControllerName() !== null) {
$request->setControllerName($forwardResponse->getControllerName());
}
if ($forwardResponse->getExtensionName() !== null) {
$request->setControllerExtensionName($forwardResponse->getExtensionName());
}
if ($forwardResponse->getArguments() !== []) {
$request->setArguments($forwardResponse->getArguments());
}
$request->setOriginalRequest($currentRequest);
$request->setOriginalRequestMappingResults($forwardResponse->getArgumentsValidationResult());
return $request;
}
}
......@@ -91,14 +91,14 @@ class Request implements RequestInterface
/**
* If this request is a forward because of an error, the original request gets filled.
*
* @var \TYPO3\CMS\Extbase\Mvc\Request
* @var \TYPO3\CMS\Extbase\Mvc\Request|null
*/
protected $originalRequest;
/**
* If the request is a forward because of an error, these mapping results get filled here.
*
* @var \TYPO3\CMS\Extbase\Error\Result
* @var \TYPO3\CMS\Extbase\Error\Result|null
*/
protected $originalRequestMappingResults;
......@@ -449,10 +449,10 @@ class Request implements RequestInterface
/**
* Returns the original request. Filled only if a property mapping error occurred.
*
* @return \TYPO3\CMS\Extbase\Mvc\Request the original request.
* @return \TYPO3\CMS\Extbase\Mvc\Request|null the original request.
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getOriginalRequest()
public function getOriginalRequest(): ?Request
{
return $this->originalRequest;
}
......@@ -472,7 +472,7 @@ class Request implements RequestInterface
* @return \TYPO3\CMS\Extbase\Error\Result
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getOriginalRequestMappingResults()
public function getOriginalRequestMappingResults(): Result
{
if ($this->originalRequestMappingResults === null) {
return new Result();
......
......@@ -19,6 +19,7 @@ use ExtbaseTeam\BlogExample\Domain\Model\Blog;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\Annotation\IgnoreValidation;
use TYPO3\CMS\Extbase\Http\ForwardResponse;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
......@@ -75,7 +76,7 @@ class BlogController extends ActionController
*/
public function testForwardAction($blogPost)
{
$this->forward('testForwardTarget', null, null, ['blogPost' => $blogPost]);
return (new ForwardResponse('testForwardTarget'))->withArguments(['blogPost' => $blogPost]);
}
/**
......