[FEATURE] Add support for PSR-15 HTTP middlewares 28/55528/15
authorBenni Mack <benni@typo3.org>
Sat, 3 Feb 2018 20:03:58 +0000 (21:03 +0100)
committerBenni Mack <benni@typo3.org>
Sat, 3 Feb 2018 20:56:55 +0000 (21:56 +0100)
PSR-15 middlewares are hooks on steroids for HTTP requests.
Due to chaining middlewares and the core request handler(s),
middlewares can execute code before and after the application.

Middlewares may return early, that means they can prevent
consecutive middleware and the core application from being executed
at all.

A full middleware coverage for *all* requests will need
a lot of changes to the core and extensions.
We'll eventually need to stop using header()/exit()/die()
or HttpUtility::{redirect,sendRespose}(). Those method calls
need to be replaced by manipulation of PSR-7 message objects.

Change-Id: I075639835115f7cf28f18c3814ef6dd190fdf29b
Releases: master
Resolves: #83725
Reviewed-on: https://review.typo3.org/55528
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
13 files changed:
composer.json
composer.lock
typo3/sysext/backend/Classes/Http/AjaxRequestHandler.php
typo3/sysext/backend/Classes/Http/Application.php
typo3/sysext/backend/Classes/Http/RequestHandler.php
typo3/sysext/core/Classes/Core/Bootstrap.php
typo3/sysext/core/Classes/Http/MiddlewareDispatcher.php [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-83725-SupportForPSR-15HTTPMiddlewares.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/Fixtures/MiddlewareFixture.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/MiddlewareDispatcherTest.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Http/Application.php
typo3/sysext/frontend/Classes/Http/EidRequestHandler.php
typo3/sysext/frontend/Classes/Http/RequestHandler.php

index eb2049f..ad5312a 100644 (file)
@@ -47,6 +47,7 @@
                "typo3/class-alias-loader": "^1.0",
                "typo3/cms-composer-installers": "^2.0",
                "psr/http-message": "~1.0",
+               "psr/http-server-middleware": "^1.0",
                "cogpowered/finediff": "~0.3.1",
                "mso/idna-convert": "^1.1.0",
                "typo3fluid/fluid": "^2.4",
index 5a28e37..9c19a06 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "21d955b30e53e0936f50899a42da5a33",
+    "content-hash": "181268b1042e97fef196d891794e14f5",
     "packages": [
         {
             "name": "cogpowered/finediff",
             "time": "2016-08-06T14:39:51+00:00"
         },
         {
+            "name": "psr/http-server-handler",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-server-handler.git",
+                "reference": "439d92054dc06097f2406ec074a2627839955a02"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/439d92054dc06097f2406ec074a2627839955a02",
+                "reference": "439d92054dc06097f2406ec074a2627839955a02",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0",
+                "psr/http-message": "^1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Server\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP server-side request handler",
+            "keywords": [
+                "handler",
+                "http",
+                "http-interop",
+                "psr",
+                "psr-15",
+                "psr-7",
+                "request",
+                "response",
+                "server"
+            ],
+            "time": "2018-01-22T17:04:15+00:00"
+        },
+        {
+            "name": "psr/http-server-middleware",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-server-middleware.git",
+                "reference": "ea17eb1fb2c8df6db919cc578451a8013c6a0ae5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/ea17eb1fb2c8df6db919cc578451a8013c6a0ae5",
+                "reference": "ea17eb1fb2c8df6db919cc578451a8013c6a0ae5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0",
+                "psr/http-message": "^1.0",
+                "psr/http-server-handler": "^1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Server\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP server-side middleware",
+            "keywords": [
+                "http",
+                "http-interop",
+                "middleware",
+                "psr",
+                "psr-15",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "time": "2018-01-22T17:08:31+00:00"
+        },
+        {
             "name": "psr/log",
             "version": "1.0.2",
             "source": {
index 27dfd1b..075e4f4 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Backend\Http;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface as PsrRequestHandlerInterface;
 use TYPO3\CMS\Backend\Routing\Exception\InvalidRequestTokenException;
 use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
 use TYPO3\CMS\Core\Core\Bootstrap;
@@ -33,7 +34,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  *
  * AJAX Requests are typically registered within EXT:myext/Configuration/Backend/AjaxRoutes.php
  */
-class AjaxRequestHandler implements RequestHandlerInterface
+class AjaxRequestHandler implements RequestHandlerInterface, PsrRequestHandlerInterface
 {
     /**
      * Instance of the current TYPO3 bootstrap
@@ -71,6 +72,17 @@ class AjaxRequestHandler implements RequestHandlerInterface
      */
     public function handleRequest(ServerRequestInterface $request): ResponseInterface
     {
+        return $this->handle($request);
+    }
+
+    /**
+     * Handles any AJAX request in the TYPO3 Backend, after finishing running middlewares
+     *
+     * @param ServerRequestInterface $request
+     * @return ResponseInterface
+     */
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
         // First get the name of the route
         $routePath = $request->getParsedBody()['route'] ?? $request->getQueryParams()['route'] ?? '';
         $request = $request->withAttribute('routePath', $routePath);
index fe59180..5ef0ad1 100644 (file)
@@ -75,7 +75,7 @@ class Application implements ApplicationInterface
      */
     public function run(callable $execute = null)
     {
-        $this->bootstrap->handleRequest(\TYPO3\CMS\Core\Http\ServerRequestFactory::fromGlobals());
+        $this->bootstrap->handleRequest(\TYPO3\CMS\Core\Http\ServerRequestFactory::fromGlobals(), 'backend');
 
         if ($execute !== null) {
             call_user_func($execute);
index 0b7daf6..3c4c2f6 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Backend\Http;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface as PsrRequestHandlerInterface;
 use TYPO3\CMS\Backend\Routing\Exception\InvalidRequestTokenException;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Http\RedirectResponse;
@@ -35,7 +36,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  *   - route
  *   - token
  */
-class RequestHandler implements RequestHandlerInterface
+class RequestHandler implements RequestHandlerInterface, PsrRequestHandlerInterface
 {
     /**
      * Instance of the current TYPO3 bootstrap
@@ -61,6 +62,17 @@ class RequestHandler implements RequestHandlerInterface
      */
     public function handleRequest(ServerRequestInterface $request): ResponseInterface
     {
+        return $this->handle($request);
+    }
+
+    /**
+     * Handles a backend request, after finishing running middlewares
+     *
+     * @param ServerRequestInterface $request
+     * @return ResponseInterface
+     */
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
         // Check if a module URL is requested and deprecate this call
         $moduleName = $request->getQueryParams()['M'] ?? $request->getParsedBody()['M'] ?? null;
         // Allow the login page to be displayed if routing is not used and on index.php
index dc6e949..a42ed91 100644 (file)
@@ -16,6 +16,10 @@ namespace TYPO3\CMS\Core\Core;
 
 use Doctrine\Common\Annotations\AnnotationReader;
 use Doctrine\Common\Annotations\AnnotationRegistry;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use TYPO3\CMS\Core\Http\MiddlewareDispatcher;
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
@@ -330,21 +334,89 @@ class Bootstrap
      * through the request handlers depending on Frontend, Backend, CLI etc.
      *
      * @param \Psr\Http\Message\RequestInterface|\Symfony\Component\Console\Input\InputInterface $request
+     * @param string $middlewareStackName the name of the middleware, usually "frontend" or "backend" for TYPO3 applications
      * @return Bootstrap
      * @throws \TYPO3\CMS\Core\Exception
      * @internal This is not a public API method, do not use in own extensions
      */
-    public function handleRequest($request)
+    public function handleRequest($request, string $middlewareStackName = null)
     {
         // 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);
+        // The application requested a middleware stack, and the request handler is PSR-15 capable.
+        // Fill the middleware dispatcher; enqueue the request handler as kernel for the middleware stack.
+        if ($request instanceof ServerRequestInterface && $requestHandler instanceof RequestHandlerInterface && $middlewareStackName !== null) {
+            $middlewares = $this->resolveMiddlewareStack($middlewareStackName);
+            $dispatcher = new MiddlewareDispatcher($requestHandler, $middlewares);
+            $this->response = $dispatcher->handle($request);
+        } else {
+            // Execute the command which returns a Response object or NULL
+            $this->response = $requestHandler->handleRequest($request);
+        }
+
         return $this;
     }
 
     /**
+     * Returns the middleware stack registered in all packages within Configuration/RequestMiddlewares.php
+     * which are sorted by dependency
+     *
+     * @todo: think if we should move this to a "MiddlewareResolver" class
+     *
+     * @param string $stackName
+     * @return array
+     * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException
+     * @throws \TYPO3\CMS\Core\Exception
+     */
+    protected function resolveMiddlewareStack(string $stackName): array
+    {
+        $middlewares = [];
+        // Check if the registered middlewares from all active packages have already been cached
+        $cacheIdentifier = 'middlewares_' . $stackName . '_' . sha1((TYPO3_version . PATH_site));
+
+        /** @var $cache \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend */
+        $cache = $this->getEarlyInstance(\TYPO3\CMS\Core\Cache\CacheManager::class)->getCache('cache_core');
+        if ($cache->has($cacheIdentifier)) {
+            $middlewares = $cache->requireOnce($cacheIdentifier);
+        } else {
+            // Loop over all packages and check for a Configuration/RequestMiddlewares.php file
+            $packageManager = $this->getEarlyInstance(\TYPO3\CMS\Core\Package\PackageManager::class);
+            $packages = $packageManager->getActivePackages();
+            $allMiddlewares = [];
+            foreach ($packages as $package) {
+                $packageConfiguration = $package->getPackagePath() . 'Configuration/RequestMiddlewares.php';
+                if (file_exists($packageConfiguration)) {
+                    $middlewaresInPackage = require $packageConfiguration;
+                    if (is_array($middlewaresInPackage)) {
+                        $allMiddlewares = array_merge_recursive($allMiddlewares, $middlewaresInPackage);
+                    }
+                }
+            }
+
+            // Order each stack
+            $dependencyOrdering = GeneralUtility::makeInstance(DependencyOrderingService::class);
+            foreach ($allMiddlewares as $stack => $middlewaresOfStack) {
+                $middlewaresOfStack = $dependencyOrdering->orderByDependencies($middlewaresOfStack);
+                $sanitizedMiddlewares = [];
+                foreach ($middlewaresOfStack as $name => $middleware) {
+                    $sanitizedMiddlewares[$name] = $middleware['target'];
+                }
+                // Order reverse, the last middleware in the array is executed first (last in, first out).
+                $sanitizedMiddlewares = array_reverse($sanitizedMiddlewares);
+                $stackCacheIdentifier = 'middlewares_' . $stack . '_' . sha1((TYPO3_version . PATH_site));
+                $cache->set($stackCacheIdentifier, 'return ' . var_export($sanitizedMiddlewares, true) . ';');
+
+                // Save the stack which we need later-on for returning them
+                if ($stack === $stackName) {
+                    $middlewares = $sanitizedMiddlewares;
+                }
+            }
+        }
+        return $middlewares;
+    }
+
+    /**
      * Outputs content if there is a proper Response object.
      *
      * @return Bootstrap
diff --git a/typo3/sysext/core/Classes/Http/MiddlewareDispatcher.php b/typo3/sysext/core/Classes/Http/MiddlewareDispatcher.php
new file mode 100644 (file)
index 0000000..69974d4
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+declare(strict_types = 1);
+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\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * MiddlewareDispatcher
+ *
+ * This class manages and dispatches a PSR-15 middleware stack.
+ */
+class MiddlewareDispatcher implements RequestHandlerInterface
+{
+    /**
+     * Tip of the middleware call stack
+     *
+     * @var RequestHandlerInterface
+     */
+    protected $tip = null;
+
+    /**
+     * @param RequestHandlerInterface $kernel
+     * @param array $middlewares
+     */
+    public function __construct(
+        RequestHandlerInterface $kernel,
+        array $middlewares = []
+    ) {
+        $this->seedMiddlewareStack($kernel);
+
+        foreach ($middlewares as $middleware) {
+            if (is_string($middleware)) {
+                $this->lazy($middleware);
+            } else {
+                $this->add($middleware);
+            }
+        }
+    }
+
+    /**
+     * Invoke the middleware stack
+     *
+     * @param ServerRequestInterface $request
+     * @return ResponseInterface
+     */
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
+        return $this->tip->handle($request);
+    }
+
+    /**
+     * Seed the middleware stack with the inner request handler
+     *
+     * @param RequestHandlerInterface $kernel
+     */
+    protected function seedMiddlewareStack(RequestHandlerInterface $kernel)
+    {
+        $this->tip = $kernel;
+    }
+
+    /**
+     * Add a new middleware to the stack
+     *
+     * Middlewares are organized as a stack. That means middlewares
+     * that have been added before will be executed after the newly
+     * added one (last in, first out).
+     *
+     * @param MiddlewareInterface $middleware
+     */
+    public function add(MiddlewareInterface $middleware)
+    {
+        $next = $this->tip;
+        $this->tip = new class($middleware, $next) implements RequestHandlerInterface {
+            private $middleware;
+            private $next;
+
+            public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $next)
+            {
+                $this->middleware = $middleware;
+                $this->next = $next;
+            }
+
+            public function handle(ServerRequestInterface $request): ResponseInterface
+            {
+                return $this->middleware->process($request, $this->next);
+            }
+        };
+    }
+
+    /**
+     * Add a new middleware by class name
+     *
+     * Middlewares are organized as a stack. That means middlewares
+     * that have been added before will be executed after the newly
+     * added one (last in, first out).
+     *
+     * @param string $middleware
+     */
+    public function lazy(string $middleware)
+    {
+        $next = $this->tip;
+        $this->tip = new class($middleware, $next) implements RequestHandlerInterface {
+            private $middleware;
+            private $next;
+
+            public function __construct(string $middleware, RequestHandlerInterface $next)
+            {
+                $this->middleware = $middleware;
+                $this->next = $next;
+            }
+
+            public function handle(ServerRequestInterface $request): ResponseInterface
+            {
+                $middleware = GeneralUtility::makeInstance($this->middleware);
+
+                if (!$middleware instanceof MiddlewareInterface) {
+                    throw new \InvalidArgumentException(get_class($middleware) . ' does not implement ' . MiddlewareInterface::class, 1516821342);
+                }
+                return $middleware->process($request, $this->next);
+            }
+        };
+    }
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-83725-SupportForPSR-15HTTPMiddlewares.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-83725-SupportForPSR-15HTTPMiddlewares.rst
new file mode 100644 (file)
index 0000000..06f6600
--- /dev/null
@@ -0,0 +1,56 @@
+.. include:: ../../Includes.txt
+
+=====================================================
+Feature: #83725 - Support for PSR-15 HTTP middlewares
+=====================================================
+
+See :issue:`83725`
+
+Description
+===========
+
+Support for PSR-15 style HTTP middlewares has been added for frontend and backend requests.
+
+PSR-15 implements middlewares (any kind of PHP functionality) which act as layers before the actual
+request handlers do their work. Any layer can be added at any point of the request to enrich / exchange a
+HTTP request or response object (PSR-7), to add functionality or do early returns of a different Response object
+(Access denied to a specific request).
+
+Basic examples of middleware are layers for authentication or security, or handling of Exceptions (like
+TYPO3's `PageNotFoundException`) to produce HTTP response objects.
+
+Adding PSR-15 to TYPO3 allows to restructure TYPO3's existing PHP classes into smaller chunks, while giving
+Site developers the possibility to add a layer at a specific place (via TYPO3's dependency ordering).
+
+Middlewares in TYPO3 are added into Middleware stacks as not every middleware needs to be called for every HTTP request.
+Currently TYPO3 supports a generic "frontend" stack and a "backend" stack, for any TYPO3 Frontend requests or
+TYPO3 Backend requests. These stacks are then processed before the actual Request Handler
+(implementing PSR-15 RequestHandlerInterface) handles the actual logic. The Request Handler produces a PSR-7 Response
+object which is then sent back through all middlewares of a stack.
+
+Impact
+======
+
+To add a middleware to the "frontend" or backend middleware stack, create the
+:file:`Configuration/RequestMiddlewares.php` in the respective extension:
+
+.. code-block:: php
+
+       return [
+               // stack name: currently 'frontend' or 'backend'
+               'frontend' => [
+                       'middleware-identifier' => [
+                               'target' => \ACME\Ext\Middleware::class,
+                               'description' => '',
+                               'before' => [
+                                       'another-middleware-identifier',
+                               ],
+                               'after' => [
+                                       'yet-another-middleware-identifier',
+                               ],
+                       ]
+               ]
+       ];
+
+
+.. index:: Backend, Frontend, PHP-API, NotScanned
diff --git a/typo3/sysext/core/Tests/Unit/Http/Fixtures/MiddlewareFixture.php b/typo3/sysext/core/Tests/Unit/Http/Fixtures/MiddlewareFixture.php
new file mode 100644 (file)
index 0000000..7b75c44
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Http\Fixtures;
+
+/*
+ * 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\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+/**
+ * MiddlewareFixture
+ */
+class MiddlewareFixture implements MiddlewareInterface
+{
+    /**
+     * @var string
+     */
+    public static $id = '0';
+
+    /**
+     * @var bool
+     */
+    public static $hasBeenInstantiated = false;
+
+    public function __construct()
+    {
+        static::$hasBeenInstantiated = true;
+    }
+
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        $request = $request->withAddedHeader('X-SEQ-PRE-REQ-HANDLER', static::$id);
+        $response = $handler->handle($request);
+
+        return $response->withAddedHeader('X-SEQ-POST-REQ-HANDLER', static::$id);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/MiddlewareDispatcherTest.php b/typo3/sysext/core/Tests/Unit/Http/MiddlewareDispatcherTest.php
new file mode 100644 (file)
index 0000000..abc8376
--- /dev/null
@@ -0,0 +1,209 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\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\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use TYPO3\CMS\Core\Http\MiddlewareDispatcher;
+use TYPO3\CMS\Core\Http\Response;
+use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Tests\Unit\Http\Fixtures\MiddlewareFixture;
+
+/**
+ * Test case
+ */
+class MiddlewareDispatcherTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function executesKernelWithEmptyMiddlewareStack()
+    {
+        $kernel = new class implements RequestHandlerInterface {
+            public function handle(ServerRequestInterface $request): ResponseInterface
+            {
+                return (new Response)->withStatus(204);
+            }
+        };
+
+        $dispatcher = new MiddlewareDispatcher($kernel);
+        $response = $dispatcher->handle(new ServerRequest);
+
+        $this->assertSame(204, $response->getStatusCode());
+    }
+
+    /**
+     * @test
+     */
+    public function executesMiddlewaresLastInFirstOut()
+    {
+        $kernel = new class implements RequestHandlerInterface {
+            public function handle(ServerRequestInterface $request): ResponseInterface
+            {
+                return (new Response)
+                    ->withStatus(204)
+                    ->withHeader('X-SEQ-PRE-REQ-HANDLER', $request->getHeader('X-SEQ-PRE-REQ-HANDLER'));
+            }
+        };
+
+        $middleware1 = new class implements MiddlewareInterface {
+            public $id = '0';
+
+            public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+            {
+                $request = $request->withAddedHeader('X-SEQ-PRE-REQ-HANDLER', $this->id);
+                $response = $handler->handle($request);
+
+                return $response->withAddedHeader('X-SEQ-POST-REQ-HANDLER', $this->id);
+            }
+        };
+
+        $middleware2 = clone $middleware1;
+        $middleware2->id = '1';
+
+        MiddlewareFixture::$id = '2';
+
+        $middleware4 = clone $middleware1;
+        $middleware4->id = '3';
+
+        $dispatcher = new MiddlewareDispatcher($kernel, [$middleware1, $middleware2]);
+        $dispatcher->lazy(MiddlewareFixture::class);
+        $dispatcher->add($middleware4);
+
+        $response = $dispatcher->handle(new ServerRequest);
+
+        $this->assertSame(['3', '2', '1', '0'], $response->getHeader('X-SEQ-PRE-REQ-HANDLER'));
+        $this->assertSame(['0', '1', '2', '3'], $response->getHeader('X-SEQ-POST-REQ-HANDLER'));
+        $this->assertSame(204, $response->getStatusCode());
+    }
+
+    /**
+     * @test
+     */
+    public function doesNotInstantiateLazyMiddlewareInCaseOfAnEarlyReturningOuterMiddleware()
+    {
+        $kernel = new class implements RequestHandlerInterface {
+            public function handle(ServerRequestInterface $request): ResponseInterface
+            {
+                return new Response;
+            }
+        };
+        $middleware = new class implements MiddlewareInterface {
+            public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+            {
+                return (new Response)->withStatus(404);
+            }
+        };
+
+        MiddlewareFixture::$hasBeenInstantiated = false;
+        $dispatcher = new MiddlewareDispatcher($kernel, [MiddlewareFixture::class, $middleware]);
+        $response = $dispatcher->handle(new ServerRequest);
+
+        $this->assertFalse(MiddlewareFixture::$hasBeenInstantiated);
+        $this->assertSame(404, $response->getStatusCode());
+    }
+
+    /**
+     * @test
+     */
+    public function throwsExceptionForLazyNonMiddlewareInterfaceClasses()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1516821342);
+
+        $kernel = new class implements RequestHandlerInterface {
+            public function handle(ServerRequestInterface $request): ResponseInterface
+            {
+                return new Response;
+            }
+        };
+
+        MiddlewareFixture::$hasBeenInstantiated = false;
+        $dispatcher = new MiddlewareDispatcher($kernel);
+        $dispatcher->lazy(\stdClass::class);
+        $response = $dispatcher->handle(new ServerRequest);
+    }
+
+    /**
+     * @test
+     */
+    public function canBeExcutedMultipleTimes()
+    {
+        $kernel = new class implements RequestHandlerInterface {
+            public function handle(ServerRequestInterface $request): ResponseInterface
+            {
+                return new Response;
+            }
+        };
+        $middleware = new class implements MiddlewareInterface {
+            public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+            {
+                return (new Response)->withStatus(204);
+            }
+        };
+
+        $dispatcher = new MiddlewareDispatcher($kernel);
+        $dispatcher->add($middleware);
+
+        $response1 = $dispatcher->handle(new ServerRequest);
+        $response2 = $dispatcher->handle(new ServerRequest);
+
+        $this->assertSame(204, $response1->getStatusCode());
+        $this->assertSame(204, $response2->getStatusCode());
+    }
+
+    /**
+     * @test
+     */
+    public function canBeReExecutedRecursivelyDuringDispatch()
+    {
+        $kernel = new class implements RequestHandlerInterface {
+            public function handle(ServerRequestInterface $request): ResponseInterface
+            {
+                return new Response;
+            }
+        };
+
+        $dispatcher = new MiddlewareDispatcher($kernel);
+
+        $dispatcher->add(new class($dispatcher) implements MiddlewareInterface {
+            private $dispatcher = null;
+
+            public function __construct(RequestHandlerInterface $dispatcher)
+            {
+                $this->dispatcher = $dispatcher;
+            }
+
+            public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+            {
+                if ($request->hasHeader('X-NESTED')) {
+                    return (new Response)->withStatus(204)->withAddedHeader('X-TRACE', 'nested');
+                }
+
+                $response = $this->dispatcher->handle($request->withAddedHeader('X-NESTED', '1'));
+
+                return $response->withAddedHeader('X-TRACE', 'outer');
+            }
+        });
+
+        $response = $dispatcher->handle(new ServerRequest);
+
+        $this->assertSame(204, $response->getStatusCode());
+        $this->assertSame(['nested', 'outer'], $response->getHeader('X-TRACE'));
+    }
+}
index d9b33fa..115a396 100644 (file)
@@ -76,7 +76,7 @@ class Application implements ApplicationInterface
      */
     public function run(callable $execute = null)
     {
-        $this->bootstrap->handleRequest(\TYPO3\CMS\Core\Http\ServerRequestFactory::fromGlobals());
+        $this->bootstrap->handleRequest(\TYPO3\CMS\Core\Http\ServerRequestFactory::fromGlobals(), 'frontend');
 
         if ($execute !== null) {
             call_user_func($execute);
index 9c5498a..34e3b47 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Frontend\Http;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface as PsrRequestHandlerInterface;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Exception;
 use TYPO3\CMS\Core\Http\Dispatcher;
@@ -30,7 +31,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  * Lightweight alternative to the regular RequestHandler used when $_GET[eID] is set.
  * In the future, logic from the EidUtility will be moved to this class.
  */
-class EidRequestHandler implements RequestHandlerInterface
+class EidRequestHandler implements RequestHandlerInterface, PsrRequestHandlerInterface
 {
     /**
      * Instance of the current TYPO3 bootstrap
@@ -56,22 +57,7 @@ class EidRequestHandler implements RequestHandlerInterface
      */
     public function handleRequest(ServerRequestInterface $request): ResponseInterface
     {
-        // Starting time tracking
-        $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']) ?: 'be_typo_user';
-
-        /** @var TimeTracker $timeTracker */
-        $timeTracker = GeneralUtility::makeInstance(TimeTracker::class, ($request->getCookieParams()[$configuredCookieName] ? true : false));
-        $timeTracker->start();
-
-        // Hook to preprocess the current request
-        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['preprocessRequest'] ?? [] as $hookFunction) {
-            $hookParameters = [];
-            GeneralUtility::callUserFunction($hookFunction, $hookParameters, $hookParameters);
-        }
-
-        // Remove any output produced until now
-        $this->bootstrap->endOutputBufferingAndCleanPreviousOutput();
-        return $this->dispatch($request);
+        return $this->handle($request);
     }
 
     /**
@@ -103,8 +89,24 @@ class EidRequestHandler implements RequestHandlerInterface
      * @return ResponseInterface
      * @throws Exception
      */
-    protected function dispatch(ServerRequestInterface $request): ResponseInterface
+    public function handle(ServerRequestInterface $request): ResponseInterface
     {
+        // Starting time tracking
+        $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']) ?: 'be_typo_user';
+
+        /** @var TimeTracker $timeTracker */
+        $timeTracker = GeneralUtility::makeInstance(TimeTracker::class, ($request->getCookieParams()[$configuredCookieName] ? true : false));
+        $timeTracker->start();
+
+        // Hook to preprocess the current request
+        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['preprocessRequest'] ?? [] as $hookFunction) {
+            $hookParameters = [];
+            GeneralUtility::callUserFunction($hookFunction, $hookParameters, $hookParameters);
+        }
+
+        // Remove any output produced until now
+        $this->bootstrap->endOutputBufferingAndCleanPreviousOutput();
+
         /** @var Response $response */
         $response = GeneralUtility::makeInstance(Response::class);
 
index 62d04fe..5390816 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Frontend\Http;
 
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface as PsrRequestHandlerInterface;
 use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\FrontendEditing\FrontendEditingController;
@@ -40,7 +41,7 @@ use TYPO3\CMS\Frontend\View\AdminPanelView;
  * Previously, this was called index_ts.php and also included the logic for the lightweight "eID" concept,
  * which is now handled in a separate request handler (EidRequestHandler).
  */
-class RequestHandler implements RequestHandlerInterface
+class RequestHandler implements RequestHandlerInterface, PsrRequestHandlerInterface
 {
     /**
      * Instance of the current TYPO3 bootstrap
@@ -84,6 +85,17 @@ class RequestHandler implements RequestHandlerInterface
      */
     public function handleRequest(ServerRequestInterface $request): ResponseInterface
     {
+        return $this->handle($request);
+    }
+
+    /**
+     * Handles a frontend request, after finishing running middlewares
+     *
+     * @param ServerRequestInterface $request
+     * @return ResponseInterface|null
+     */
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
         $response = null;
         $this->request = $request;
         $this->initializeTimeTracker();