Commit ab3729bb authored by Benni Mack's avatar Benni Mack Committed by Christian Kuhn
Browse files

[FEATURE] Introduce Request/Response based on PSR-7

The PSR-7 standard is adapted into the TYPO3 Bootstrap with a
backwards-compatible layer.

The PSR-7 implementation brings several new classes:
 * Message (the base for Requests and Responses)
 * Request (for Requests made within PHP)
 * ServerRequest and a factory based on the current system environment
 * Response
 * Uri (a unified API for fetching several parts of an URI)

At any TYPO3 request a new ServerRequest object is created inside the
Bootstrap and handed over to the RequestHandler which can then use this
object for checking certain GET and POST variables instead of using
GeneralUtility.

The proper call (usually a Controller) creates a Response object that
is handed back to the RequestHandler + Bootstrap. The TYPO3 Bootstrap
will output anything related in the shutdown() method.

An example is shown with the LoginController and currently hard-wired
as no proper routing/dispatching is there yet.

Currently this is an internal API as the rest (Dispatch/Router and
Controller API) will follow once the base is in.

Please note that the PSR-7 standard works with Value Objects meaning
that it is not possible to modify any object but instead new objects
will be created for Message, ServerRequest and Response if modified.

The next steps are:
* Integrate proper Routing + Dispatching for Backend Routes to register
  new BE requests
* Migrate all AJAX Calls to use the new API and request / response
  handling
* Introduce a common Base Controller for all regular BE requests which
  is based on Request/Response and works as a replacement for sc_base
* Then: proper documentation for the whole bootstrap /
  dispatch + routing / controller logic
* Integrate symfony console app into the CLI Bootstrap as alternative
  for Request/Response
* Refactor TSFE to use Response / Request objects properly
* Refactor redirects logic to use Response objects

Resolves: #67558
Releases: master
Change-Id: I5b528284ecca790f784c7780b008356158343ee8
Reviewed-on: http://review.typo3.org/40355


Reviewed-by: default avatarHelmut Hummel <helmut.hummel@typo3.org>
Tested-by: default avatarHelmut Hummel <helmut.hummel@typo3.org>
Reviewed-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Markus Klein's avatarMarkus Klein <markus.klein@typo3.org>
Tested-by: Markus Klein's avatarMarkus Klein <markus.klein@typo3.org>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent ccf7dae5
......@@ -19,9 +19,5 @@
*/
call_user_func(function() {
$classLoader = require __DIR__ . '/contrib/vendor/autoload.php';
(new \TYPO3\CMS\Backend\Http\Application($classLoader))->run(function() {
// currently implemented as a closure as there is no Request/Response implementation or routing in the backend
$loginController = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Controller\LoginController::class);
$loginController->main();
});
(new \TYPO3\CMS\Backend\Http\Application($classLoader))->run();
});
......@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Backend\Console;
* The TYPO3 project - inspiring people to share!
*/
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\RequestHandlerInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -44,9 +45,10 @@ class CliRequestHandler implements RequestHandlerInterface {
/**
* Handles any commandline request
*
* @param ServerRequestInterface $request
* @return void
*/
public function handleRequest() {
public function handleRequest(ServerRequestInterface $request) {
$commandLineKey = $this->getCommandLineKeyOrDie();
$commandLineScript = $this->getIncludeScriptByCommandLineKey($commandLineKey);
......@@ -137,9 +139,10 @@ class CliRequestHandler implements RequestHandlerInterface {
/**
* This request handler can handle any CLI request .
*
* @param ServerRequestInterface $request
* @return bool If the request is a CLI request, TRUE otherwise FALSE
*/
public function canHandleRequest() {
public function canHandleRequest(ServerRequestInterface $request) {
return defined('TYPO3_cliMode') && (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE) && (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI);
}
......
......@@ -31,7 +31,7 @@ use TYPO3\CMS\Fluid\View\StandaloneView;
* @author Kasper Skårhøj <kasperYYYY@typo3.com>
* @author Frank Nägler <typo3@naegler.net>
*/
class LoginController {
class LoginController implements \TYPO3\CMS\Core\Http\ControllerInterface {
/**
* The URL to redirect to after login.
......@@ -121,11 +121,27 @@ class LoginController {
$this->view = $this->getFluidTemplateObject();
}
/**
* Injects the request object for the current request or subrequest
* As this controller goes only through the main() method, it is rather simple for now
* This will be split up in an abstract controller once proper routing/dispatcher is in place.
*
* @param \Psr\Http\Message\RequestInterface $request
* @return \Psr\Http\Message\ResponseInterface $response
*/
public function processRequest(\Psr\Http\Message\RequestInterface $request) {
$content = $this->main();
/** @var \TYPO3\CMS\Core\Http\Response $response */
$response = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\Response::class);
$response->getBody()->write($content);
return $response;
}
/**
* Main function - creating the login/logout form
*
* @throws Exception
* @return void
* @return string The content to output
*/
public function main() {
/** @var $pageRenderer \TYPO3\CMS\Core\Page\PageRenderer */
......@@ -221,7 +237,7 @@ class LoginController {
$content .= $this->view->render();
$content .= $this->getDocumentTemplate()->endPage();
echo $content;
return $content;
}
/**
......
......@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Backend\Http;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\RequestHandlerInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Psr\Http\Message\ServerRequestInterface;
/**
* Base class for all AJAX-related calls for the TYPO3 Backend run through typo3/ajax.php.
......@@ -49,7 +50,7 @@ class AjaxRequestHandler implements RequestHandlerInterface {
);
/**
* Constructor handing over the bootstrap
* Constructor handing over the bootstrap and the original request
*
* @param Bootstrap $bootstrap
*/
......@@ -60,15 +61,14 @@ class AjaxRequestHandler implements RequestHandlerInterface {
/**
* Handles any AJAX request in the TYPO3 Backend
*
* @return void
* @param ServerRequestInterface $request
* @return NULL|\Psr\Http\Message\ResponseInterface
*/
public function handleRequest() {
public function handleRequest(ServerRequestInterface $request) {
// First get the ajaxID
$ajaxID = isset($_POST['ajaxID']) ? $_POST['ajaxID'] : $_GET['ajaxID'];
if (isset($ajaxID)) {
$ajaxID = (string)stripslashes($ajaxID);
}
$ajaxID = isset($request->getParsedBody()['ajaxID']) ? $request->getParsedBody()['ajaxID'] : $request->getQueryParams()['ajaxID'];
// used for backwards-compatibility
$GLOBALS['ajaxID'] = $ajaxID;
$this->boot($ajaxID);
......@@ -94,7 +94,8 @@ class AjaxRequestHandler implements RequestHandlerInterface {
$success = TRUE;
$tokenIsValid = TRUE;
if ($csrfTokenCheck) {
$tokenIsValid = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->validateToken(GeneralUtility::_GP('ajaxToken'), 'ajaxCall', $ajaxID);
$ajaxToken = $request->getParsedBody()['ajaxToken'] ?: $request->getQueryParams()['ajaxToken'];
$tokenIsValid = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->validateToken($ajaxToken, 'ajaxCall', $ajaxID);
}
if ($tokenIsValid) {
// Cleanup global variable space
......@@ -110,14 +111,17 @@ class AjaxRequestHandler implements RequestHandlerInterface {
// Outputting the content (and setting the X-JSON-Header)
$ajaxObj->render();
return NULL;
}
/**
* This request handler can handle any backend request coming from ajax.php
*
* @param ServerRequestInterface $request
* @return bool If the request is an AJAX backend request, TRUE otherwise FALSE
*/
public function canHandleRequest() {
public function canHandleRequest(ServerRequestInterface $request) {
return TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX;
}
......
......@@ -20,6 +20,7 @@ use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Exception;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use Psr\Http\Message\ServerRequestInterface;
/**
* Handles the request for backend modules and wizards
......@@ -42,7 +43,15 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
protected $backendUserAuthentication;
/**
* @param Bootstrap $bootstrap The TYPO3 core bootstrap
* Instance of the current Http Request
* @var ServerRequestInterface
*/
protected $request;
/**
* Constructor handing over the bootstrap and the original request
*
* @param Bootstrap $bootstrap
*/
public function __construct(Bootstrap $bootstrap) {
$this->bootstrap = $bootstrap;
......@@ -51,9 +60,12 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
/**
* Handles the request, evaluating the configuration and executes the module accordingly
*
* @param ServerRequestInterface $request
* @return NULL|\Psr\Http\Message\ResponseInterface
* @throws Exception
*/
public function handleRequest() {
public function handleRequest(ServerRequestInterface $request) {
$this->request = $request;
$this->boot();
$this->moduleRegistry = $GLOBALS['TBE_MODULES'];
......@@ -67,7 +79,7 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
$this->backendUserAuthentication = $GLOBALS['BE_USER'];
$moduleName = (string)GeneralUtility::_GET('M');
$moduleName = (string)$this->request->getQueryParams()['M'];
if ($this->isDispatchedModule($moduleName)) {
$isDispatched = $this->dispatchModule($moduleName);
} else {
......@@ -107,10 +119,11 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
/**
* This request handler can handle any backend request coming from mod.php
*
* @param ServerRequestInterface $request
* @return bool
*/
public function canHandleRequest() {
return (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE) && !empty((string)GeneralUtility::_GET('M'));
public function canHandleRequest(ServerRequestInterface $request) {
return (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE) && !empty((string)$request->getQueryParams()['M']);
}
/**
......@@ -119,7 +132,7 @@ class BackendModuleRequestHandler implements \TYPO3\CMS\Core\Core\RequestHandler
* @return bool
*/
protected function isValidModuleRequest() {
return $this->getFormProtection()->validateToken((string)GeneralUtility::_GP('moduleToken'), 'moduleCall', (string)GeneralUtility::_GET('M'));
return $this->getFormProtection()->validateToken((string)$this->request->getQueryParams()['moduleToken'], 'moduleCall', (string)$this->request->getQueryParams()['M']);
}
/**
......
......@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Backend\Http;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Core\RequestHandlerInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* General RequestHandler for the TYPO3 Backend. This is used for all Backend requests except for CLI
......@@ -33,7 +34,7 @@ class RequestHandler implements RequestHandlerInterface {
protected $bootstrap;
/**
* Constructor handing over the bootstrap
* Constructor handing over the bootstrap and the original request
*
* @param Bootstrap $bootstrap
*/
......@@ -44,9 +45,14 @@ class RequestHandler implements RequestHandlerInterface {
/**
* Handles any backend request
*
* @return void
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return NULL|\Psr\Http\Message\ResponseInterface
*/
public function handleRequest() {
public function handleRequest(\Psr\Http\Message\ServerRequestInterface $request) {
// enable dispatching via Request/Response logic only for typo3/index.php currently
$path = substr($request->getUri()->getPath(), strlen(GeneralUtility::getIndpEnv('TYPO3_SITE_PATH')));
$routingEnabled = ($path === TYPO3_mainDir . 'index.php' || $path === TYPO3_mainDir);
// Evaluate the constant for skipping the BE user check for the bootstrap
if (defined('TYPO3_PROCEED_IF_NO_USER') && TYPO3_PROCEED_IF_NO_USER) {
$proceedIfNoUserIsLoggedIn = TRUE;
......@@ -68,14 +74,20 @@ class RequestHandler implements RequestHandlerInterface {
->endOutputBufferingAndCleanPreviousOutput()
->initializeOutputCompression()
->sendHttpHeaders();
if ($routingEnabled) {
return $this->dispatch($request);
}
return NULL;
}
/**
* This request handler can handle any backend request (but not CLI).
*
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return bool If the request is not a CLI script, TRUE otherwise FALSE
*/
public function canHandleRequest() {
public function canHandleRequest(\Psr\Http\Message\ServerRequestInterface $request) {
return (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE && !(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI));
}
......@@ -88,4 +100,19 @@ class RequestHandler implements RequestHandlerInterface {
public function getPriority() {
return 50;
}
/**
* Dispatch the request to the appropriate controller, will go to a proper dispatcher/router class in the future
*
* @internal
* @param \Psr\Http\Message\RequestInterface $request
* @return NULL|\Psr\Http\Message\ResponseInterface
*/
protected function dispatch($request) {
$controller = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Controller\LoginController::class);
if ($controller instanceof \TYPO3\CMS\Core\Http\ControllerInterface) {
return $controller->processRequest($request);
}
return NULL;
}
}
<?php
namespace TYPO3\CMS\Backend\Tests\Unit;
namespace TYPO3\CMS\Backend\Tests\Unit\Http;
/*
* This file is part of the TYPO3 CMS project.
......@@ -35,9 +35,15 @@ class BackendModuleRequestHandlerTest extends UnitTestCase {
*/
protected $formProtectionMock;
/**
* @var \TYPO3\CMS\Core\Http\ServerRequest|PHPUnit_Framework_MockObject_MockObject
*/
protected $requestMock;
public function setUp() {
$this->requestMock = $this->getAccessibleMock(\TYPO3\CMS\Core\Http\ServerRequest::class, array(), array(), '', FALSE);
$this->formProtectionMock = $this->getMockForAbstractClass(AbstractFormProtection::class, array(), '', TRUE, TRUE, TRUE, array('validateToken'));
$this->subject = $this->getAccessibleMock(BackendModuleRequestHandler::class, array('boot', 'getFormProtection'), array(), '', FALSE);
$this->subject = $this->getAccessibleMock(BackendModuleRequestHandler::class, array('boot', 'getFormProtection'), array(\TYPO3\CMS\Core\Core\Bootstrap::getInstance()), '', TRUE);
}
/**
......@@ -48,16 +54,16 @@ class BackendModuleRequestHandlerTest extends UnitTestCase {
public function moduleIndexIsCalled() {
$GLOBALS['TBE_MODULES'] = array(
'_PATHS' => array(
'module_fixture' => __DIR__ . '/Fixtures/ModuleFixture/'
'module_fixture' => __DIR__ . '/../Fixtures/ModuleFixture/'
)
);
$_GET['M'] = 'module_fixture';
$this->requestMock->expects($this->any())->method('getQueryParams')->will($this->returnValue(array('M' => 'module_fixture')));
$this->formProtectionMock->expects($this->once())->method('validateToken')->will($this->returnValue(TRUE));
$this->subject->expects($this->once())->method('boot');
$this->subject->expects($this->once())->method('getFormProtection')->will($this->returnValue($this->formProtectionMock));
$this->subject->handleRequest();
$this->subject->handleRequest($this->requestMock);
}
/**
......@@ -70,7 +76,7 @@ class BackendModuleRequestHandlerTest extends UnitTestCase {
$this->subject->expects($this->once())->method('boot');
$this->subject->expects($this->once())->method('getFormProtection')->will($this->returnValue($this->formProtectionMock));
$this->subject->handleRequest();
$this->subject->handleRequest($this->requestMock);
}
/**
......@@ -82,16 +88,15 @@ class BackendModuleRequestHandlerTest extends UnitTestCase {
$GLOBALS['TBE_MODULES'] = array(
'_PATHS' => array(
'_dispatcher' => array(),
'module_fixture' => __DIR__ . '/Fixtures/ModuleFixture/'
'module_fixture' => __DIR__ . '/../Fixtures/ModuleFixture/'
)
);
$_GET['M'] = 'module_fixture';
$this->requestMock->expects($this->any())->method('getQueryParams')->will($this->returnValue(array('M' => 'module_fixture')));
$this->formProtectionMock->expects($this->once())->method('validateToken')->will($this->returnValue(TRUE));
$this->subject->expects($this->once())->method('boot');
$this->subject->expects($this->once())->method('getFormProtection')->will($this->returnValue($this->formProtectionMock));
$this->subject->handleRequest();
$this->subject->handleRequest($this->requestMock);
}
}
......@@ -73,11 +73,18 @@ class Bootstrap {
protected $activeErrorHandlerClassName;
/**
* registered request handlers
* A list of all registered request handlers, see the Application class / entry points for the registration
* @var RequestHandlerInterface[]
*/
protected $availableRequestHandlers = array();
/**
* The Response object when using Request/Response logic
* @var \Psr\Http\Message\ResponseInterface
* @see shutdown()
*/
protected $response;
/**
* @var bool
*/
......@@ -178,19 +185,6 @@ class Bootstrap {
return $this;
}
/**
* Resolve the request handler that were registered based on the application
* and execute the request
*
* @return Bootstrap
* @throws \TYPO3\CMS\Core\Exception
*/
public function handleRequest() {
$requestHandler = $this->resolveRequestHandler();
$requestHandler->handleRequest();
return $this;
}
/**
* Run the base setup that checks server environment, determines pathes,
* populates base files and sets common configuration.
......@@ -265,15 +259,16 @@ class Bootstrap {
* Be sure to always have the constants that are defined in $this->defineTypo3RequestTypes() are set,
* so most RequestHandlers can check if they can handle the request.
*
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return RequestHandlerInterface
* @throws \TYPO3\CMS\Core\Exception
* @internal This is not a public API method, do not use in own extensions
*/
public function resolveRequestHandler() {
protected function resolveRequestHandler(\Psr\Http\Message\ServerRequestInterface $request) {
$suitableRequestHandlers = array();
foreach ($this->availableRequestHandlers as $requestHandlerClassName) {
$requestHandler = GeneralUtility::makeInstance($requestHandlerClassName, $this);
if ($requestHandler->canHandleRequest()) {
if ($requestHandler->canHandleRequest($request)) {
$priority = $requestHandler->getPriority();
if (isset($suitableRequestHandlers[$priority])) {
throw new \TYPO3\CMS\Core\Exception('More than one request handler with the same priority can handle the request, but only one handler may be active at a time!', 1176471352);
......@@ -288,6 +283,42 @@ class Bootstrap {
return array_pop($suitableRequestHandlers);
}
/**
* Builds a Request instance from the current process, and then resolves the request
* through the request handlers depending on Frontend, Backend, CLI etc.
*
* @return Bootstrap
* @throws \TYPO3\CMS\Core\Exception
*/
protected function handleRequest() {
// Build the Request object
$request = \TYPO3\CMS\Core\Http\ServerRequestFactory::fromGlobals();
// Resolve request handler that were registered based on the Application
$requestHandler = $this->resolveRequestHandler($request);
// Execute the command which returns a Response object or NULL
$this->response = $requestHandler->handleRequest($request);
return $this;
}
/**
* Outputs content if there is a proper Response object.
*
* @return Bootstrap
*/
protected function sendResponse() {
if ($this->response instanceof \Psr\Http\Message\ResponseInterface) {
if (!headers_sent()) {
foreach ($this->response->getHeaders() as $name => $values) {
header($name . ': ' . implode(', ', $values), FALSE);
}
}
echo $this->response->getBody()->__toString();
}
return $this;
}
/**
* Registers the instance of the specified object for an early boot stage.
* On finalizing the Object Manager initialization, all those instances will
......@@ -1140,6 +1171,7 @@ class Bootstrap {
* @internal This is not a public API method, do not use in own extensions
*/
public function shutdown() {
$this->sendResponse();
return $this;
}
......
......@@ -16,7 +16,7 @@ namespace TYPO3\CMS\Core\Core;
/**
* The interface for a request handler
* see FrontendRequestHandler
* see RequestHandler in EXT:backend/Classes/Http/ and EXT:frontend/Classes/Http
*
* @api
*/
......@@ -25,18 +25,20 @@ interface RequestHandlerInterface {
/**
* Handles a raw request
*
* @return void
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return NULL|\Psr\Http\Message\ResponseInterface
* @api
*/
public function handleRequest();
public function handleRequest(\Psr\Http\Message\ServerRequestInterface $request);
/**
* Checks if the request handler can handle the current request.
* Checks if the request handler can handle the given request.
*
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return bool TRUE if it can handle the request, otherwise FALSE
* @api
*/
public function canHandleRequest();
public function canHandleRequest(\Psr\Http\Message\ServerRequestInterface $request);
/**
* Returns the priority - how eager the handler is to actually handle the
......@@ -47,4 +49,5 @@ interface RequestHandlerInterface {
* @api
*/
public function getPriority();
}
<?php
namespace TYPO3\CMS\Core\Http;
/*
* 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 Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\RequestInterface;
/**
* An interface every controller should implement
* in order to deal with PSR-7 standard.
*
* @internal please note that this API will be extended until TYPO3 CMS 7 LTS and is not public yet.
*/
interface ControllerInterface {
/**
* Processes a typical request.
*
* @param RequestInterface $request The request object
* @return ResponseInterface $response The response, created by the controller
* @api
*/
public function processRequest(RequestInterface $request);
}
<?php
namespace TYPO3\CMS\Core\Http;
/*
* 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 Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
/**
* Default implementation for the MessageInterface of the PSR-7 standard
* It is the base for any request or response for PSR-7.
*
* Highly inspired by https://github.com/phly/http/
*
* @internal Note that this is not public API yet.