[FEATURE] Add support for PSR-15 HTTP middlewares 44/55544/7
authorBenjamin Franzke <bfr@qbus.de>
Sat, 3 Feb 2018 22:56:37 +0000 (23:56 +0100)
committerBenni Mack <benni@typo3.org>
Tue, 6 Feb 2018 20:09:08 +0000 (21:09 +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.

PSR-15 interfaces are provided by psr/http-server-middleware:

composer require psr/http-server-middleware:^1.0

Change-Id: Ia906d51da2e4309a37fb47a7966b52f873782d8f
Releases: master
Resolves: #83725
Reviewed-on: https://review.typo3.org/55544
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>
15 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/Classes/Http/MiddlewareStackResolver.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/core/composer.json
typo3/sysext/frontend/Classes/Http/Application.php
typo3/sysext/frontend/Classes/Http/EidRequestHandler.php
typo3/sysext/frontend/Classes/Http/RequestHandler.php

index eb2049f..b82b14e 100644 (file)
@@ -53,7 +53,8 @@
                "guzzlehttp/guzzle": "^6.3.0",
                "doctrine/dbal": "^2.6",
                "nikic/php-parser": "^3.1",
-               "symfony/polyfill-intl-icu": "^1.6"
+               "symfony/polyfill-intl-icu": "^1.6",
+               "psr/http-server-middleware": "^1.0"
        },
        "require-dev": {
                "codeception/codeception": "^2.3",
index dc5c592..43eb5c1 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": "404f780d81028713a93bcb93bdfc3d69",
     "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..868b732 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\Http\MiddlewareStackResolver;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
@@ -330,17 +334,33 @@ 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) {
+            $resolver = new MiddlewareStackResolver(
+                $this->getEarlyInstance(\TYPO3\CMS\Core\Package\PackageManager::class),
+                GeneralUtility::makeInstance(\TYPO3\CMS\Core\Service\DependencyOrderingService::class),
+                $this->getEarlyInstance(\TYPO3\CMS\Core\Cache\CacheManager::class)->getCache('cache_core')
+            );
+            $middlewares = $resolver->resolve($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;
     }
 
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/Classes/Http/MiddlewareStackResolver.php b/typo3/sysext/core/Classes/Http/MiddlewareStackResolver.php
new file mode 100644 (file)
index 0000000..68856be
--- /dev/null
@@ -0,0 +1,139 @@
+<?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 TYPO3\CMS\Core\Cache\Frontend\PhpFrontend as PhpFrontendCache;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
+
+/**
+ * This class resolves middleware stacks from defined configuration in all active packages.
+ */
+class MiddlewareStackResolver
+{
+    /**
+     * @var PackageManager
+     */
+    protected $packageManager;
+
+    /**
+     * @var DependencyOrderingService
+     */
+    protected $dependencyOrderingService;
+
+    /**
+     * @var PhpFrontendCache
+     */
+    protected $cache;
+
+    public function __construct(
+        PackageManager $packageManager,
+        DependencyOrderingService $dependencyOrderingService,
+        PhpFrontendCache $cache
+    ) {
+        $this->packageManager = $packageManager;
+        $this->dependencyOrderingService = $dependencyOrderingService;
+        $this->cache = $cache;
+    }
+
+    /**
+     * Returns the middleware stack registered in all packages within Configuration/RequestMiddlewares.php
+     * which are sorted by given dependency requirements
+     *
+     * @param string $stackName
+     * @return array
+     * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException
+     * @throws \TYPO3\CMS\Core\Exception
+     */
+    public function resolve(string $stackName): array
+    {
+        // Check if the registered middlewares from all active packages have already been cached
+        $cacheIdentifier = $this->getCacheIdentifier($stackName);
+        if ($this->cache->has($cacheIdentifier)) {
+            return $this->cache->requireOnce($cacheIdentifier);
+        }
+
+        $allMiddlewares = $this->loadConfiguration();
+        $middlewares = $this->sanitizeMiddlewares($allMiddlewares);
+
+        // Ensure that we create a cache for $stackName, even if the stack is empty
+        if (!isset($middlewares[$stackName])) {
+            $middlewares[$stackName] = [];
+        }
+
+        foreach ($middlewares as $stack => $middlewaresOfStack) {
+            $this->cache->set($this->getCacheIdentifier($stack), 'return ' . var_export($middlewaresOfStack, true) . ';');
+        }
+
+        return $middlewares[$stackName];
+    }
+
+    /**
+     * Loop over all packages and check for a Configuration/RequestMiddlewares.php file
+     *
+     * @return array
+     */
+    protected function loadConfiguration(): array
+    {
+        $packages = $this->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);
+                }
+            }
+        }
+
+        return $allMiddlewares;
+    }
+
+    /**
+     * Order each stack and sanitize to a plain array
+     *
+     * @param array
+     * @return array
+     */
+    protected function sanitizeMiddlewares(array $allMiddlewares): array
+    {
+        $middlewares = [];
+
+        foreach ($allMiddlewares as $stack => $middlewaresOfStack) {
+            $middlewaresOfStack = $this->dependencyOrderingService->orderByDependencies($middlewaresOfStack);
+
+            $sanitizedMiddlewares = [];
+            foreach ($middlewaresOfStack as $name => $middleware) {
+                $sanitizedMiddlewares[$name] = $middleware['target'];
+            }
+
+            // Order reverse, MiddlewareDispatcher executes the last middleware in the array first (last in, first out).
+            $middlewares[$stack] = array_reverse($sanitizedMiddlewares);
+        }
+
+        return $middlewares;
+    }
+
+    /**
+     * @param string $stackName
+     * @return string
+     */
+    protected function getCacheIdentifier(string $stackName): string
+    {
+        return 'middlewares_' . $stackName . '_' . sha1((TYPO3_version . PATH_site));
+    }
+}
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..207178d
--- /dev/null
@@ -0,0 +1,57 @@
+.. 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 style middlewares are intended to be used to move common request and response processing away from
+the application layer into (possibly reusable) components.
+Middlewares are concentric layers surrounding other middlewares (so called inner middlewares) or request handlers;
+that means they can perform pre- and postprocessing of request and response objects (PSR-7). They allow to enrich or
+exchange PSR-7 objects in order to add functionality or to perform early returns (without invoking the core application).
+
+Common middleware usecases are layers for authentication, authorization, security enforcement, or the conversion of
+exceptions (like TYPO3's `PageNotFoundException`) into HTTP response objects.
+
+Adding PSR-15 to TYPO3 allows to restructure TYPO3's existing PHP classes into smaller chunks, while giving developers
+the possibility to add own middlewares at a specific position in the middleware chain (via TYPO3's dependency ordering).
+
+Middlewares in TYPO3 are added into middleware stacks; not every middleware needs to be called for every HTTP request.
+Currently TYPO3 supports a generic "frontend" and a "backend" stack; they're run for any TYPO3 Frontend or TYPO3 Backend
+request respectively. These stacks are processed before the actual Request Handler (which implements the PSR-15
+RequestHandlerInterface) handles the application logic. The Request Handler produces a PSR-7 Response object which is
+propagated back through all middlewares of the 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 ab1b786..bf0751a 100644 (file)
@@ -28,6 +28,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 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();