[FEATURE] Introduce Request/Response based on PSR-7 55/40355/20
authorBenjamin Mack <benni@typo3.org>
Wed, 20 May 2015 04:28:41 +0000 (12:28 +0800)
committerChristian Kuhn <lolli@schwarzbu.ch>
Mon, 13 Jul 2015 12:11:02 +0000 (14:11 +0200)
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: Helmut Hummel <helmut.hummel@typo3.org>
Tested-by: Helmut Hummel <helmut.hummel@typo3.org>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Markus Klein <markus.klein@typo3.org>
Tested-by: Markus Klein <markus.klein@typo3.org>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
30 files changed:
composer.json
typo3/index.php
typo3/sysext/backend/Classes/Console/CliRequestHandler.php
typo3/sysext/backend/Classes/Controller/LoginController.php
typo3/sysext/backend/Classes/Http/AjaxRequestHandler.php
typo3/sysext/backend/Classes/Http/BackendModuleRequestHandler.php
typo3/sysext/backend/Classes/Http/RequestHandler.php
typo3/sysext/backend/Tests/Unit/BackendModuleRequestHandlerTest.php [deleted file]
typo3/sysext/backend/Tests/Unit/Http/BackendModuleRequestHandlerTest.php [new file with mode: 0644]
typo3/sysext/core/Classes/Core/Bootstrap.php
typo3/sysext/core/Classes/Core/RequestHandlerInterface.php
typo3/sysext/core/Classes/Http/ControllerInterface.php [new file with mode: 0644]
typo3/sysext/core/Classes/Http/Message.php [new file with mode: 0644]
typo3/sysext/core/Classes/Http/Request.php [new file with mode: 0644]
typo3/sysext/core/Classes/Http/Response.php [new file with mode: 0644]
typo3/sysext/core/Classes/Http/ServerRequest.php [new file with mode: 0644]
typo3/sysext/core/Classes/Http/ServerRequestFactory.php [new file with mode: 0644]
typo3/sysext/core/Classes/Http/Stream.php [new file with mode: 0644]
typo3/sysext/core/Classes/Http/UploadedFile.php [new file with mode: 0644]
typo3/sysext/core/Classes/Http/Uri.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/MessageTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/RequestTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/ResponseTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/ServerRequestFactoryTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/ServerRequestTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/StreamTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/UploadedFileTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Http/UriTest.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Http/EidRequestHandler.php
typo3/sysext/frontend/Classes/Http/RequestHandler.php

index 9bc716f..fa55dde 100644 (file)
@@ -41,7 +41,8 @@
                "symfony/finder": "2.6.9",
                "doctrine/instantiator": "1.0.4",
                "helhum/class-alias-loader": "1.1.9",
-               "typo3/cms-composer-installers": "1.2.1"
+               "typo3/cms-composer-installers": "1.2.1",
+               "psr/http-message": "1.0"
        },
        "require-dev": {
                "mikey179/vfsStream": "1.4.*@dev",
index 3871198..69514c7 100644 (file)
@@ -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();
 });
index 62d4a39..3e9c24f 100644 (file)
@@ -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);
        }
 
index 460ab45..ee522e9 100644 (file)
@@ -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.
@@ -122,10 +122,26 @@ class LoginController {
        }
 
        /**
+        * 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;
        }
 
        /**
index ac11642..306d7d2 100644 (file)
@@ -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;
        }
 
index c06b092..e31d852 100644 (file)
@@ -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']);
        }
 
        /**
index bf4ed83..489616e 100644 (file)
@@ -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;
+       }
 }
diff --git a/typo3/sysext/backend/Tests/Unit/BackendModuleRequestHandlerTest.php b/typo3/sysext/backend/Tests/Unit/BackendModuleRequestHandlerTest.php
deleted file mode 100644 (file)
index 654ef29..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php
-namespace TYPO3\CMS\Backend\Tests\Unit;
-
-/*
- * 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 PHPUnit_Framework_MockObject_MockObject;
-use TYPO3\CMS\Backend\Http\BackendModuleRequestHandler;
-use TYPO3\CMS\Core\FormProtection\AbstractFormProtection;
-use TYPO3\CMS\Core\Tests\AccessibleObjectInterface;
-use TYPO3\CMS\Core\Tests\UnitTestCase;
-
-/**
- * Class BackendModuleRequestHandlerTest
- */
-class BackendModuleRequestHandlerTest extends UnitTestCase {
-
-       /**
-        * @var BackendModuleRequestHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface
-        */
-       protected $subject;
-
-       /**
-        * @var \TYPO3\CMS\Core\FormProtection\AbstractFormProtection|PHPUnit_Framework_MockObject_MockObject
-        */
-       protected $formProtectionMock;
-
-       public function setUp() {
-               $this->formProtectionMock = $this->getMockForAbstractClass(AbstractFormProtection::class, array(), '', TRUE, TRUE, TRUE, array('validateToken'));
-               $this->subject = $this->getAccessibleMock(BackendModuleRequestHandler::class, array('boot', 'getFormProtection'), array(), '', FALSE);
-       }
-
-       /**
-        * @test
-        * @expectedException \InvalidArgumentException
-        * @expectedExceptionCode 1425236663
-        */
-       public function moduleIndexIsCalled() {
-               $GLOBALS['TBE_MODULES'] = array(
-                       '_PATHS' => array(
-                               'module_fixture' => __DIR__ . '/Fixtures/ModuleFixture/'
-                       )
-               );
-               $_GET['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();
-       }
-
-       /**
-        * @test
-        * @expectedException \TYPO3\CMS\Core\Exception
-        * @expectedExceptionCode 1417988921
-        */
-       public function throwsExceptionIfTokenIsInvalid() {
-               $this->formProtectionMock->expects($this->once())->method('validateToken')->will($this->returnValue(FALSE));
-               $this->subject->expects($this->once())->method('boot');
-               $this->subject->expects($this->once())->method('getFormProtection')->will($this->returnValue($this->formProtectionMock));
-
-               $this->subject->handleRequest();
-       }
-
-       /**
-        * @test
-        * @expectedException \InvalidArgumentException
-        * @expectedExceptionCode 1425236663
-        */
-       public function moduleDispatcherIsCalled() {
-               $GLOBALS['TBE_MODULES'] = array(
-                       '_PATHS' => array(
-                               '_dispatcher' => array(),
-                               'module_fixture' => __DIR__ . '/Fixtures/ModuleFixture/'
-                       )
-               );
-               $_GET['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();
-       }
-
-}
diff --git a/typo3/sysext/backend/Tests/Unit/Http/BackendModuleRequestHandlerTest.php b/typo3/sysext/backend/Tests/Unit/Http/BackendModuleRequestHandlerTest.php
new file mode 100644 (file)
index 0000000..9c7d054
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+namespace TYPO3\CMS\Backend\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 PHPUnit_Framework_MockObject_MockObject;
+use TYPO3\CMS\Backend\Http\BackendModuleRequestHandler;
+use TYPO3\CMS\Core\FormProtection\AbstractFormProtection;
+use TYPO3\CMS\Core\Tests\AccessibleObjectInterface;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+
+/**
+ * Class BackendModuleRequestHandlerTest
+ */
+class BackendModuleRequestHandlerTest extends UnitTestCase {
+
+       /**
+        * @var BackendModuleRequestHandler|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface
+        */
+       protected $subject;
+
+       /**
+        * @var \TYPO3\CMS\Core\FormProtection\AbstractFormProtection|PHPUnit_Framework_MockObject_MockObject
+        */
+       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(\TYPO3\CMS\Core\Core\Bootstrap::getInstance()), '', TRUE);
+       }
+
+       /**
+        * @test
+        * @expectedException \InvalidArgumentException
+        * @expectedExceptionCode 1425236663
+        */
+       public function moduleIndexIsCalled() {
+               $GLOBALS['TBE_MODULES'] = array(
+                       '_PATHS' => array(
+                               'module_fixture' => __DIR__ . '/../Fixtures/ModuleFixture/'
+                       )
+               );
+
+               $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->requestMock);
+       }
+
+       /**
+        * @test
+        * @expectedException \TYPO3\CMS\Core\Exception
+        * @expectedExceptionCode 1417988921
+        */
+       public function throwsExceptionIfTokenIsInvalid() {
+               $this->formProtectionMock->expects($this->once())->method('validateToken')->will($this->returnValue(FALSE));
+               $this->subject->expects($this->once())->method('boot');
+               $this->subject->expects($this->once())->method('getFormProtection')->will($this->returnValue($this->formProtectionMock));
+
+               $this->subject->handleRequest($this->requestMock);
+       }
+
+       /**
+        * @test
+        * @expectedException \InvalidArgumentException
+        * @expectedExceptionCode 1425236663
+        */
+       public function moduleDispatcherIsCalled() {
+               $GLOBALS['TBE_MODULES'] = array(
+                       '_PATHS' => array(
+                               '_dispatcher' => array(),
+                               'module_fixture' => __DIR__ . '/../Fixtures/ModuleFixture/'
+                       )
+               );
+               $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->requestMock);
+       }
+
+}
index 0e4ec6d..43c98e4 100644 (file)
@@ -73,12 +73,19 @@ 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
         */
        static protected $usesComposerClassLoading = FALSE;
@@ -179,19 +186,6 @@ class Bootstrap {
        }
 
        /**
-        * 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);
@@ -289,6 +284,42 @@ class Bootstrap {
        }
 
        /**
+        * 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
         * be transferred to the Object Manager's registry.
@@ -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;
        }
 
index cdddae3..7af6f42 100644 (file)
@@ -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();
+
 }
diff --git a/typo3/sysext/core/Classes/Http/ControllerInterface.php b/typo3/sysext/core/Classes/Http/ControllerInterface.php
new file mode 100644 (file)
index 0000000..e2d275e
--- /dev/null
@@ -0,0 +1,37 @@
+<?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);
+
+}
diff --git a/typo3/sysext/core/Classes/Http/Message.php b/typo3/sysext/core/Classes/Http/Message.php
new file mode 100644 (file)
index 0000000..e5938b8
--- /dev/null
@@ -0,0 +1,472 @@
+<?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.
+ */
+class Message implements MessageInterface {
+
+       /**
+        * The HTTP Protocol version, defaults to 1.1
+        * @var string
+        */
+       protected $protocolVersion = '1.1';
+
+       /**
+        * Associative array containing all headers of this Message
+        * This is a mixed-case list of the headers (as due to the specification)
+        * @var array
+        */
+       protected $headers = array();
+
+       /**
+        * Lowercased version of all headers, in order to check if a header is set or not
+        * this way a lot of checks are easier to be set
+        * @var array
+        */
+       protected $lowercasedHeaderNames = array();
+
+       /**
+        * The body as a Stream object
+        * @var StreamInterface
+        */
+       protected $body;
+
+       /**
+        * Retrieves the HTTP protocol version as a string.
+        *
+        * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
+        *
+        * @return string HTTP protocol version.
+        */
+       public function getProtocolVersion() {
+               return $this->protocolVersion;
+       }
+
+       /**
+        * Return an instance with the specified HTTP protocol version.
+        *
+        * The version string MUST contain only the HTTP version number (e.g.,
+        * "1.1", "1.0").
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * new protocol version.
+        *
+        * @param string $version HTTP protocol version
+        * @return Message
+        */
+       public function withProtocolVersion($version) {
+               $clonedObject = clone $this;
+               $clonedObject->protocolVersion = $version;
+               return $clonedObject;
+       }
+
+       /**
+        * Retrieves all message header values.
+        *
+        * The keys represent the header name as it will be sent over the wire, and
+        * each value is an array of strings associated with the header.
+        *
+        *     // Represent the headers as a string
+        *     foreach ($message->getHeaders() as $name => $values) {
+        *         echo $name . ": " . implode(", ", $values);
+        *     }
+        *
+        *     // Emit headers iteratively:
+        *     foreach ($message->getHeaders() as $name => $values) {
+        *         foreach ($values as $value) {
+        *             header(sprintf('%s: %s', $name, $value), false);
+        *         }
+        *     }
+        *
+        * While header names are not case-sensitive, getHeaders() will preserve the
+        * exact case in which headers were originally specified.
+        *
+        * @return array Returns an associative array of the message's headers. Each
+        *     key MUST be a header name, and each value MUST be an array of strings
+        *     for that header.
+        */
+       public function getHeaders() {
+               return $this->headers;
+       }
+
+       /**
+        * Checks if a header exists by the given case-insensitive name.
+        *
+        * @param string $name Case-insensitive header field name.
+        * @return bool Returns true if any header names match the given header
+        *     name using a case-insensitive string comparison. Returns false if
+        *     no matching header name is found in the message.
+        */
+       public function hasHeader($name) {
+               return isset($this->lowercasedHeaderNames[strtolower($name)]);
+       }
+
+       /**
+        * Retrieves a message header value by the given case-insensitive name.
+        *
+        * This method returns an array of all the header values of the given
+        * case-insensitive header name.
+        *
+        * If the header does not appear in the message, this method MUST return an
+        * empty array.
+        *
+        * @param string $name Case-insensitive header field name.
+        * @return string[] An array of string values as provided for the given
+        *    header. If the header does not appear in the message, this method MUST
+        *    return an empty array.
+        */
+       public function getHeader($name) {
+               if (!$this->hasHeader($name)) {
+                       return array();
+               }
+               $header = $this->lowercasedHeaderNames[strtolower($name)];
+               $headerValue = $this->headers[$header];
+               if (is_array($headerValue)) {
+                       return $headerValue;
+               } else {
+                       return array($headerValue);
+               }
+       }
+
+       /**
+        * Retrieves a comma-separated string of the values for a single header.
+        *
+        * This method returns all of the header values of the given
+        * case-insensitive header name as a string concatenated together using
+        * a comma.
+        *
+        * NOTE: Not all header values may be appropriately represented using
+        * comma concatenation. For such headers, use getHeader() instead
+        * and supply your own delimiter when concatenating.
+        *
+        * If the header does not appear in the message, this method MUST return
+        * an empty string.
+        *
+        * @param string $name Case-insensitive header field name.
+        * @return string A string of values as provided for the given header
+        *    concatenated together using a comma. If the header does not appear in
+        *    the message, this method MUST return an empty string.
+        */
+       public function getHeaderLine($name) {
+               $headerValue = $this->getHeader($name);
+               if (empty($headerValue)) {
+                       return '';
+               }
+               return implode(',', $headerValue);
+       }
+
+       /**
+        * Return an instance with the provided value replacing the specified header.
+        *
+        * While header names are case-insensitive, the casing of the header will
+        * be preserved by this function, and returned from getHeaders().
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * new and/or updated header and value.
+        *
+        * @param string $name Case-insensitive header field name.
+        * @param string|string[] $value Header value(s).
+        * @return Message
+        * @throws \InvalidArgumentException for invalid header names or values.
+        */
+       public function withHeader($name, $value) {
+               if (is_string($value)) {
+                       $value = array($value);
+               }
+
+               if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
+                       throw new \InvalidArgumentException('Invalid header value for header "' . $name . '"". The value must be a string or an array of strings.', 1436717266);
+               }
+
+               $this->validateHeaderName($name);
+               $this->validateHeaderValues($value);
+               $lowercasedHeaderName = strtolower($name);
+
+               $clonedObject = clone $this;
+               $clonedObject->headers[$name] = $value;
+               $clonedObject->lowercasedHeaderNames[$lowercasedHeaderName] = $name;
+               return $clonedObject;
+       }
+
+       /**
+        * Return an instance with the specified header appended with the given value.
+        *
+        * Existing values for the specified header will be maintained. The new
+        * value(s) will be appended to the existing list. If the header did not
+        * exist previously, it will be added.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * new header and/or value.
+        *
+        * @param string $name Case-insensitive header field name to add.
+        * @param string|string[] $value Header value(s).
+        * @return Message
+        * @throws \InvalidArgumentException for invalid header names or values.
+        */
+       public function withAddedHeader($name, $value) {
+               if (is_string($value)) {
+                       $value = array($value);
+               }
+               if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
+                       throw new \InvalidArgumentException('Invalid header value for header "' . $name . '". The header value must be a string or array of strings', 1436717267);
+               }
+               $this->validateHeaderName($name);
+               $this->validateHeaderValues($value);
+               if (!$this->hasHeader($name)) {
+                       return $this->withHeader($name, $value);
+               }
+               $name = $this->lowercasedHeaderNames[strtolower($name)];
+               $clonedObject = clone $this;
+               $clonedObject->headers[$name] = array_merge($this->headers[$name], $value);
+               return $clonedObject;
+       }
+
+       /**
+        * Return an instance without the specified header.
+        *
+        * Header resolution MUST be done without case-sensitivity.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that removes
+        * the named header.
+        *
+        * @param string $name Case-insensitive header field name to remove.
+        * @return Message
+        */
+       public function withoutHeader($name) {
+               if (!$this->hasHeader($name)) {
+                       return clone $this;
+               }
+               // fetch the original header from the lowercased version
+               $lowercasedHeader = strtolower($name);
+               $name = $this->lowercasedHeaderNames[$lowercasedHeader];
+               $clonedObject = clone $this;
+               unset($clonedObject->headers[$name], $clonedObject->lowercasedHeaderNames[$lowercasedHeader]);
+               return $clonedObject;
+       }
+
+       /**
+        * Gets the body of the message.
+        *
+        * @return \Psr\Http\Message\StreamInterface Returns the body as a stream.
+        */
+       public function getBody() {
+               return $this->body;
+       }
+
+       /**
+        * Return an instance with the specified message body.
+        *
+        * The body MUST be a StreamInterface object.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return a new instance that has the
+        * new body stream.
+        *
+        * @param \Psr\Http\Message\StreamInterface $body Body.
+        * @return Message
+        * @throws \InvalidArgumentException When the body is not valid.
+        */
+       public function withBody(StreamInterface $body) {
+               $clonedObject = clone $this;
+               $clonedObject->body = $body;
+               return $clonedObject;
+       }
+
+       /**
+        * Ensure header names and values are valid.
+        *
+        * @param array $headers
+        * @throws \InvalidArgumentException
+        */
+       protected function assertHeaders(array $headers) {
+               foreach ($headers as $name => $headerValues) {
+                       $this->validateHeaderName($name);
+                       // check if all values are correct
+                       array_walk($headerValues, function($value, $key, Message $messageObject) {
+                               if (!$messageObject->isValidHeaderValue($value)) {
+                                       throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717268);
+                               }
+                       }, $this);
+               }
+       }
+
+       /**
+        * Filter a set of headers to ensure they are in the correct internal format.
+        *
+        * Used by message constructors to allow setting all initial headers at once.
+        *
+        * @param array $originalHeaders Headers to filter.
+        * @return array Filtered headers and names.
+        */
+       protected function filterHeaders(array $originalHeaders) {
+               $headerNames = $headers = array();
+               foreach ($originalHeaders as $header => $value) {
+                       if (!is_string($header) || (!is_array($value) && !is_string($value))) {
+                               continue;
+                       }
+                       if (!is_array($value)) {
+                               $value = array($value);
+                       }
+                       $headerNames[strtolower($header)] = $header;
+                       $headers[$header] = $value;
+               }
+               return array($headerNames, $headers);
+       }
+
+       /**
+        * Helper function to test if an array contains only strings
+        *
+        * @param array $data
+        * @return bool
+        */
+       protected function arrayContainsOnlyStrings(array $data) {
+               return array_reduce($data, function($original, $item) {
+                       return is_string($item) ? $original : FALSE;
+               }, TRUE);
+       }
+
+       /**
+        * Assert that the provided header values are valid.
+        *
+        * @see http://tools.ietf.org/html/rfc7230#section-3.2
+        * @param string[] $values
+        * @throws \InvalidArgumentException
+        */
+       protected function validateHeaderValues(array $values) {
+               array_walk($values, function($value, $key, Message $messageObject) {
+                       if (!$messageObject->isValidHeaderValue($value)) {
+                               throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717269);
+                       }
+               }, $this);
+       }
+
+       /**
+        * Filter a header value
+        *
+        * Ensures CRLF header injection vectors are filtered.
+        *
+        * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
+        * tabs are allowed in values; header continuations MUST consist of
+        * a single CRLF sequence followed by a space or horizontal tab.
+        *
+        * This method filters any values not allowed from the string, and is
+        * lossy.
+        *
+        * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
+        * @param string $value
+        * @return string
+        */
+       public function filter($value) {
+               $value  = (string)$value;
+               $length = strlen($value);
+               $string = '';
+               for ($i = 0; $i < $length; $i += 1) {
+                       $ascii = ord($value[$i]);
+
+                       // Detect continuation sequences
+                       if ($ascii === 13) {
+                               $lf = ord($value[$i + 1]);
+                               $ws = ord($value[$i + 2]);
+                               if ($lf === 10 && in_array($ws, [9, 32], TRUE)) {
+                                       $string .= $value[$i] . $value[$i + 1];
+                                       $i += 1;
+                               }
+                               continue;
+                       }
+
+                       // Non-visible, non-whitespace characters
+                       // 9 === horizontal tab
+                       // 32-126, 128-254 === visible
+                       // 127 === DEL
+                       // 255 === null byte
+                       if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) {
+                               continue;
+                       }
+
+                       $string .= $value[$i];
+               }
+
+               return $string;
+       }
+
+       /**
+        * Check whether or not a header name is valid and throw an exception.
+        *
+        * @see http://tools.ietf.org/html/rfc7230#section-3.2
+        * @param string $name
+        * @throws \InvalidArgumentException
+        */
+       public function validateHeaderName($name) {
+               if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
+                       throw new \InvalidArgumentException('Invalid header name, given "' . $name . '"', 1436717270);
+               }
+       }
+
+       /**
+        * Checks if a a HTTP header value is valid.
+        *
+        * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
+        * tabs are allowed in values; header continuations MUST consist of
+        * a single CRLF sequence followed by a space or horizontal tab.
+        *
+        * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
+        * @param string $value
+        * @return bool
+        */
+       public function isValidHeaderValue($value) {
+               $value = (string)$value;
+
+               // Look for:
+               // \n not preceded by \r, OR
+               // \r not followed by \n, OR
+               // \r\n not followed by space or horizontal tab; these are all CRLF attacks
+               if (preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value)) {
+                       return FALSE;
+               }
+
+               $length = strlen($value);
+               for ($i = 0; $i < $length; $i += 1) {
+                       $ascii = ord($value[$i]);
+
+                       // Non-visible, non-whitespace characters
+                       // 9 === horizontal tab
+                       // 10 === line feed
+                       // 13 === carriage return
+                       // 32-126, 128-254 === visible
+                       // 127 === DEL
+                       // 255 === null byte
+                       if (($ascii < 32 && ! in_array($ascii, [9, 10, 13], TRUE)) || $ascii === 127 || $ascii > 254) {
+                               return FALSE;
+                       }
+               }
+
+               return TRUE;
+       }
+
+}
diff --git a/typo3/sysext/core/Classes/Http/Request.php b/typo3/sysext/core/Classes/Http/Request.php
new file mode 100644 (file)
index 0000000..9d7d05d
--- /dev/null
@@ -0,0 +1,349 @@
+<?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\RequestInterface;
+use Psr\Http\Message\UriInterface;
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * Default implementation for the RequestInterface of the PSR-7 standard
+ * It is the base for any request sent BY PHP.
+ *
+ * Please see ServerRequest for the typical use cases in the framework.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class Request extends Message implements RequestInterface {
+
+       /**
+        * The request-target, if it has been provided or calculated.
+        * @var NULL|string
+        */
+       protected $requestTarget;
+
+       /**
+        * The HTTP method, defaults to GET
+        *
+        * @var string
+        */
+       protected $method;
+
+       /**
+        * Supported HTTP methods
+        *
+        * @var array
+        */
+       protected $supportedMethods = array(
+               'CONNECT',
+               'DELETE',
+               'GET',
+               'HEAD',
+               'OPTIONS',
+               'PATCH',
+               'POST',
+               'PUT',
+               'TRACE'
+       );
+
+       /**
+        * An instance of the Uri object
+        * @var UriInterface
+        */
+       protected $uri;
+
+       /**
+        * Constructor, the only place to set all parameters of this Request
+        *
+        * @param NULL|string $uri URI for the request, if any.
+        * @param NULL|string $method HTTP method for the request, if any.
+        * @param string|resource|StreamInterface $body Message body, if any.
+        * @param array $headers Headers for the message, if any.
+        * @throws \InvalidArgumentException for any invalid value.
+        */
+       public function __construct($uri = NULL, $method = NULL, $body = 'php://input', array $headers = array()) {
+
+               // Build a streamable object for the body
+               if (!is_string($body) && !is_resource($body) && !$body instanceof StreamInterface) {
+                       throw new \InvalidArgumentException('Body must be a string stream resource identifier, a stream resource, or a StreamInterface instance', 1436717271);
+               }
+
+               if (!$body instanceof StreamInterface) {
+                       $body = new Stream($body);
+               }
+
+               if (is_string($uri)) {
+                       $uri = new Uri($uri);
+               }
+
+               if (!$uri instanceof UriInterface && $uri !== NULL) {
+                       throw new \InvalidArgumentException('Invalid URI provided; must be null, a string, or a UriInterface instance', 1436717272);
+               }
+
+               $this->validateMethod($method);
+
+               $this->method = $method;
+               $this->uri    = $uri;
+               $this->body   = $body;
+               list($this->headerNames, $headers) = $this->filterHeaders($headers);
+               $this->assertHeaders($headers);
+               $this->headers = $headers;
+       }
+
+       /**
+        * Retrieves all message header values.
+        *
+        * The keys represent the header name as it will be sent over the wire, and
+        * each value is an array of strings associated with the header.
+        *
+        *     // Represent the headers as a string
+        *     foreach ($message->getHeaders() as $name => $values) {
+        *         echo $name . ": " . implode(", ", $values);
+        *     }
+        *
+        *     // Emit headers iteratively:
+        *     foreach ($message->getHeaders() as $name => $values) {
+        *         foreach ($values as $value) {
+        *             header(sprintf('%s: %s', $name, $value), false);
+        *         }
+        *     }
+        *
+        * While header names are not case-sensitive, getHeaders() will preserve the
+        * exact case in which headers were originally specified.
+        *
+        * @return array Returns an associative array of the message's headers. Each
+        *     key MUST be a header name, and each value MUST be an array of strings
+        *     for that header.
+        */
+       public function getHeaders() {
+               $headers = parent::getHeaders();
+               if (!$this->hasHeader('host') && ($this->uri && $this->uri->getHost())) {
+                       $headers['host'] = [$this->getHostFromUri()];
+               }
+               return $headers;
+       }
+
+       /**
+        * Retrieves a message header value by the given case-insensitive name.
+        *
+        * This method returns an array of all the header values of the given
+        * case-insensitive header name.
+        *
+        * If the header does not appear in the message, this method MUST return an
+        * empty array.
+        *
+        * @param string $name Case-insensitive header field name.
+        * @return string[] An array of string values as provided for the given
+        *    header. If the header does not appear in the message, this method MUST
+        *    return an empty array.
+        */
+       public function getHeader($header) {
+               if (!$this->hasHeader($header) && strtolower($header) === 'host' && ($this->uri && $this->uri->getHost())) {
+                       return array($this->getHostFromUri());
+               }
+               return parent::getHeader($header);
+       }
+
+       /**
+        * Retrieve the host from the URI instance
+        *
+        * @return string
+        */
+       protected function getHostFromUri() {
+               $host  = $this->uri->getHost();
+               $host .= $this->uri->getPort() ? ':' . $this->uri->getPort() : '';
+               return $host;
+       }
+
+       /**
+        * Retrieves the message's request target.
+        *
+        * Retrieves the message's request-target either as it will appear (for
+        * clients), as it appeared at request (for servers), or as it was
+        * specified for the instance (see withRequestTarget()).
+        *
+        * In most cases, this will be the origin-form of the composed URI,
+        * unless a value was provided to the concrete implementation (see
+        * withRequestTarget() below).
+        *
+        * If no URI is available, and no request-target has been specifically
+        * provided, this method MUST return the string "/".
+        *
+        * @return string
+        */
+       public function getRequestTarget() {
+               if ($this->requestTarget !== NULL) {
+                       return $this->requestTarget;
+               }
+               if (!$this->uri) {
+                       return '/';
+               }
+               $target = $this->uri->getPath();
+
+               if ($this->uri->getQuery()) {
+                       $target .= '?' . $this->uri->getQuery();
+               }
+
+               if (empty($target)) {
+                       $target = '/';
+               }
+               return $target;
+       }
+
+       /**
+        * Return an instance with the specific request-target.
+        *
+        * If the request needs a non-origin-form request-target — e.g., for
+        * specifying an absolute-form, authority-form, or asterisk-form —
+        * this method may be used to create an instance with the specified
+        * request-target, verbatim.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * changed request target.
+        *
+        * @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
+        *     request-target forms allowed in request messages)
+        *
+        * @param mixed $requestTarget
+        * @return Request
+        */
+       public function withRequestTarget($requestTarget) {
+               if (preg_match('#\s#', $requestTarget)) {
+                       throw new \InvalidArgumentException('Invalid request target provided which contains whitespaces.', 1436717273);
+               }
+               $clonedObject = clone $this;
+               $clonedObject->requestTarget = $requestTarget;
+               return $clonedObject;
+       }
+
+       /**
+        * Retrieves the HTTP method of the request, defaults to GET
+        *
+        * @return string Returns the request method.
+        */
+       public function getMethod() {
+               return !empty($this->method) ? $this->method : 'GET';
+       }
+
+       /**
+        * Return an instance with the provided HTTP method.
+        *
+        * While HTTP method names are typically all uppercase characters, HTTP
+        * method names are case-sensitive and thus implementations SHOULD NOT
+        * modify the given string.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * changed request method.
+        *
+        * @param string $method Case-sensitive method.
+        * @return Request
+        * @throws \InvalidArgumentException for invalid HTTP methods.
+        */
+       public function withMethod($method) {
+               $clonedObject = clone $this;
+               $clonedObject->method = $method;
+               return $clonedObject;
+       }
+
+       /**
+        * Retrieves the URI instance.
+        *
+        * This method MUST return a UriInterface instance.
+        *
+        * @link http://tools.ietf.org/html/rfc3986#section-4.3
+        * @return \Psr\Http\Message\UriInterface Returns a UriInterface instance
+        *     representing the URI of the request.
+        */
+       public function getUri() {
+               return $this->uri;
+       }
+
+       /**
+        * Returns an instance with the provided URI.
+        *
+        * This method MUST update the Host header of the returned request by
+        * default if the URI contains a host component. If the URI does not
+        * contain a host component, any pre-existing Host header MUST be carried
+        * over to the returned request.
+        *
+        * You can opt-in to preserving the original state of the Host header by
+        * setting `$preserveHost` to `true`. When `$preserveHost` is set to
+        * `true`, this method interacts with the Host header in the following ways:
+        *
+        * - If the the Host header is missing or empty, and the new URI contains
+        *   a host component, this method MUST update the Host header in the returned
+        *   request.
+        * - If the Host header is missing or empty, and the new URI does not contain a
+        *   host component, this method MUST NOT update the Host header in the returned
+        *   request.
+        * - If a Host header is present and non-empty, this method MUST NOT update
+        *   the Host header in the returned request.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * new UriInterface instance.
+        *
+        * @link http://tools.ietf.org/html/rfc3986#section-4.3
+        *
+        * @param \Psr\Http\Message\UriInterface $uri New request URI to use.
+        * @param bool $preserveHost Preserve the original state of the Host header.
+        * @return Request
+        */
+       public function withUri(UriInterface $uri, $preserveHost = FALSE) {
+               $clonedObject = clone $this;
+               $clonedObject->uri = $uri;
+
+               if ($preserveHost) {
+                       return $clonedObject;
+               }
+
+               if (!$uri->getHost()) {
+                       return $clonedObject;
+               }
+
+               $host = $uri->getHost();
+
+               if ($uri->getPort()) {
+                       $host .= ':' . $uri->getPort();
+               }
+
+               $clonedObject->headerNames['host'] = 'Host';
+               $clonedObject->headers['Host'] = array($host);
+               return $clonedObject;
+       }
+
+       /**
+        * Validate the HTTP method, helper function.
+        *
+        * @param NULL|string $method
+        * @throws \InvalidArgumentException on invalid HTTP method.
+        */
+       protected function validateMethod($method) {
+               if ($method !== NULL) {
+                       if (!is_string($method)) {
+                               $methodAsString = is_object($method) ? get_class($method) : gettype($method);
+                               throw new \InvalidArgumentException('Unsupported HTTP method "' . $methodAsString . '".', 1436717274);
+                       }
+                       $method = strtoupper($method);
+                       if (!in_array($method, $this->supportedMethods, TRUE)) {
+                               throw new \InvalidArgumentException('Unsupported HTTP method "' . $method. '".', 1436717275);
+                       }
+               }
+       }
+}
diff --git a/typo3/sysext/core/Classes/Http/Response.php b/typo3/sysext/core/Classes/Http/Response.php
new file mode 100644 (file)
index 0000000..14b4fb5
--- /dev/null
@@ -0,0 +1,201 @@
+<?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\StreamInterface;
+
+/**
+ * Default implementation for the ResponseInterface of the PSR-7 standard.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class Response extends Message implements ResponseInterface {
+
+       /**
+        * The HTTP status code of the response
+        * @var int $statusCode
+        */
+       protected $statusCode;
+
+       /**
+        * The reason phrase of the response
+        * @var string $reasonPhrase
+        */
+       protected $reasonPhrase = '';
+
+       /**
+        * The standardized and other important HTTP Status Codes
+        * @var array
+        */
+       protected $availableStatusCodes = array(
+               // INFORMATIONAL CODES
+               100 => 'Continue',
+               101 => 'Switching Protocols',
+               102 => 'Processing',
+               // SUCCESS CODES
+               200 => 'OK',
+               201 => 'Created',
+               202 => 'Accepted',
+               203 => 'Non-Authoritative Information',
+               204 => 'No Content',
+               205 => 'Reset Content',
+               206 => 'Partial Content',
+               207 => 'Multi-status',
+               208 => 'Already Reported',
+               // REDIRECTION CODES
+               300 => 'Multiple Choices',
+               301 => 'Moved Permanently',
+               302 => 'Found',
+               303 => 'See Other',
+               304 => 'Not Modified',
+               305 => 'Use Proxy',
+               306 => 'Switch Proxy', // Deprecated
+               307 => 'Temporary Redirect',
+               // CLIENT ERROR
+               400 => 'Bad Request',
+               401 => 'Unauthorized',
+               402 => 'Payment Required',
+               403 => 'Forbidden',
+               404 => 'Not Found',
+               405 => 'Method Not Allowed',
+               406 => 'Not Acceptable',
+               407 => 'Proxy Authentication Required',
+               408 => 'Request Time-out',
+               409 => 'Conflict',
+               410 => 'Gone',
+               411 => 'Length Required',
+               412 => 'Precondition Failed',
+               413 => 'Request Entity Too Large',
+               414 => 'Request-URI Too Large',
+               415 => 'Unsupported Media Type',
+               416 => 'Requested range not satisfiable',
+               417 => 'Expectation Failed',
+               418 => 'I\'m a teapot',
+               422 => 'Unprocessable Entity',
+               423 => 'Locked',
+               424 => 'Failed Dependency',
+               425 => 'Unordered Collection',
+               426 => 'Upgrade Required',
+               428 => 'Precondition Required',
+               429 => 'Too Many Requests',
+               431 => 'Request Header Fields Too Large',
+               // SERVER ERROR
+               500 => 'Internal Server Error',
+               501 => 'Not Implemented',
+               502 => 'Bad Gateway',
+               503 => 'Service Unavailable',
+               504 => 'Gateway Time-out',
+               505 => 'HTTP Version not supported',
+               506 => 'Variant Also Negotiates',
+               507 => 'Insufficient Storage',
+               508 => 'Loop Detected',
+               509 => 'Bandwidth Limit Exceeded',
+               511 => 'Network Authentication Required'
+       );
+
+       /**
+        * Constructor for generating new responses
+        *
+        * @param StreamInterface|string $body
+        * @param int $statusCode
+        * @param array $headers
+        * @throws \InvalidArgumentException if any of the given arguments are given
+        */
+       public function __construct($body = 'php://temp', $statusCode = 200, $headers = array()) {
+               // Build a streamable object for the body
+               if (!is_string($body) && !is_resource($body) && !$body instanceof StreamInterface) {
+                       throw new \InvalidArgumentException('Body must be a string stream resource identifier, a stream resource, or a StreamInterface instance', 1436717277);
+               }
+
+               if (!$body instanceof StreamInterface) {
+                       $body = new Stream($body, 'rw');
+               }
+               $this->body = $body;
+
+               if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($statusCode) === FALSE || !array_key_exists((int)$statusCode, $this->availableStatusCodes)) {
+                       throw new \InvalidArgumentException('The given status code is not a valid HTTP status code.', 1436717278);
+               }
+               $this->statusCode = (int)$statusCode;
+
+               $this->reasonPhrase = $this->availableStatusCodes[$this->statusCode];
+               list($this->headerNames, $headers) = $this->filterHeaders($headers);
+               $this->assertHeaders($headers);
+               $this->headers = $headers;
+       }
+
+       /**
+        * Gets the response status code.
+        *
+        * The status code is a 3-digit integer result code of the server's attempt
+        * to understand and satisfy the request.
+        *
+        * @return int Status code.
+        */
+       public function getStatusCode() {
+               return $this->statusCode;
+       }
+
+       /**
+        * Return an instance with the specified status code and, optionally, reason phrase.
+        *
+        * If no reason phrase is specified, implementations MAY choose to default
+        * to the RFC 7231 or IANA recommended reason phrase for the response's
+        * status code.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * updated status and reason phrase.
+        *
+        * @link http://tools.ietf.org/html/rfc7231#section-6
+        * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+        *
+        * @param int $code The 3-digit integer result code to set.
+        * @param string $reasonPhrase The reason phrase to use with the
+        *     provided status code; if none is provided, implementations MAY
+        *     use the defaults as suggested in the HTTP specification.
+        * @return Response
+        * @throws \InvalidArgumentException For invalid status code arguments.
+        */
+       public function withStatus($code, $reasonPhrase = '') {
+               if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($code) === FALSE || !array_key_exists((int)$code, $this->availableStatusCodes)) {
+                       throw new \InvalidArgumentException('The given status code is not a valid HTTP status code', 1436717279);
+               }
+               $clonedObject = clone $this;
+               $clonedObject->statusCode = $code;
+               $clonedObject->reasonPhrase = $reasonPhrase !== '' ? $reasonPhrase : $this->availableStatusCodes[$code];
+               return $clonedObject;
+       }
+
+       /**
+        * Gets the response reason phrase associated with the status code.
+        *
+        * Because a reason phrase is not a required element in a response
+        * status line, the reason phrase value MAY be null. Implementations MAY
+        * choose to return the default RFC 7231 recommended reason phrase (or those
+        * listed in the IANA HTTP Status Code Registry) for the response's
+        * status code.
+        *
+        * @link http://tools.ietf.org/html/rfc7231#section-6
+        * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+        * @return string Reason phrase; must return an empty string if none present.
+        */
+       public function getReasonPhrase() {
+               return $this->reasonPhrase;
+       }
+
+}
diff --git a/typo3/sysext/core/Classes/Http/ServerRequest.php b/typo3/sysext/core/Classes/Http/ServerRequest.php
new file mode 100644 (file)
index 0000000..019eebf
--- /dev/null
@@ -0,0 +1,369 @@
+<?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\ServerRequestInterface;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UploadedFileInterface;
+
+/**
+ * Represents a typical request incoming from the server to be processed
+ * by the TYPO3 Core. The original request is built from the ServerRequestFactory
+ * inside TYPO3's Bootstrap.
+ *
+ * Note that the PSR-7 standard works with immutable value objects, meaning that
+ * any modification to a Request object using the "with" methods will result
+ * in a new Request object.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class ServerRequest extends Request implements ServerRequestInterface {
+
+       /**
+        * @var array
+        */
+       protected $attributes;
+
+       /**
+        * @var array
+        */
+       protected $cookieParams;
+
+       /**
+        * @var array
+        */
+       protected $parsedBody;
+
+       /**
+        * @var array
+        */
+       protected $queryParams;
+
+       /**
+        * @var array
+        */
+       protected $serverParams;
+
+       /**
+        * @var array
+        */
+       protected $uploadedFiles;
+
+       /**
+        * Constructor, the only place to set all parameters of this Message/Request
+        *
+        * @param NULL|string $uri URI for the request, if any.
+        * @param NULL|string $method HTTP method for the request, if any.
+        * @param string|resource|StreamInterface $body Message body, if any.
+        * @param array $headers Headers for the message, if any.
+        * @param array $serverParams Server parameters, typically from $_SERVER
+        * @param array $uploadedFiles Upload file information, a tree of UploadedFiles
+        * @throws \InvalidArgumentException for any invalid value.
+        */
+       public function __construct($uri = NULL, $method = NULL, $body = 'php://input', array $headers = array(), array $serverParams = array(), array $uploadedFiles = NULL) {
+               if ($uploadedFiles !== NULL) {
+                       $this->validateUploadedFiles($uploadedFiles);
+               }
+
+               parent::__construct($uri, $method, $body, $headers);
+
+               $this->serverParams  = $serverParams;
+               $this->uploadedFiles = $uploadedFiles;
+       }
+
+       /**
+        * Retrieve server parameters.
+        *
+        * Retrieves data related to the incoming request environment,
+        * typically derived from PHP's $_SERVER superglobal. The data IS NOT
+        * REQUIRED to originate from $_SERVER.
+        *
+        * @return array
+        */
+       public function getServerParams() {
+               return $this->serverParams;
+       }
+
+       /**
+        * Retrieve cookies.
+        *
+        * Retrieves cookies sent by the client to the server.
+        *
+        * The data MUST be compatible with the structure of the $_COOKIE
+        * superglobal.
+        *
+        * @return array
+        */
+       public function getCookieParams() {
+               return $this->cookieParams;
+       }
+
+       /**
+        * Return an instance with the specified cookies.
+        *
+        * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
+        * be compatible with the structure of $_COOKIE. Typically, this data will
+        * be injected at instantiation.
+        *
+        * This method MUST NOT update the related Cookie header of the request
+        * instance, nor related values in the server params.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * updated cookie values.
+        *
+        * @param array $cookies Array of key/value pairs representing cookies.
+        * @return ServerRequest
+        */
+       public function withCookieParams(array $cookies) {
+               $clonedObject = clone $this;
+               $clonedObject->cookieParams = $cookies;
+               return $clonedObject;
+       }
+
+       /**
+        * Retrieve query string arguments.
+        *
+        * Retrieves the deserialized query string arguments, if any.
+        *
+        * Note: the query params might not be in sync with the URI or server
+        * params. If you need to ensure you are only getting the original
+        * values, you may need to parse the query string from `getUri()->getQuery()`
+        * or from the `QUERY_STRING` server param.
+        *
+        * @return array
+        */
+       public function getQueryParams() {
+               return $this->queryParams;
+       }
+
+       /**
+        * Return an instance with the specified query string arguments.
+        *
+        * These values SHOULD remain immutable over the course of the incoming
+        * request. They MAY be injected during instantiation, such as from PHP's
+        * $_GET superglobal, or MAY be derived from some other value such as the
+        * URI. In cases where the arguments are parsed from the URI, the data
+        * MUST be compatible with what PHP's parse_str() would return for
+        * purposes of how duplicate query parameters are handled, and how nested
+        * sets are handled.
+        *
+        * Setting query string arguments MUST NOT change the URI stored by the
+        * request, nor the values in the server params.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * updated query string arguments.
+        *
+        * @param array $query Array of query string arguments, typically from
+        *     $_GET.
+        * @return ServerRequest
+        */
+       public function withQueryParams(array $query) {
+               $clonedObject = clone $this;
+               $clonedObject->queryParams = $query;
+               return $clonedObject;
+       }
+
+       /**
+        * Retrieve normalized file upload data.
+        *
+        * This method returns upload metadata in a normalized tree, with each leaf
+        * an instance of Psr\Http\Message\UploadedFileInterface.
+        *
+        * These values MAY be prepared from $_FILES or the message body during
+        * instantiation, or MAY be injected via withUploadedFiles().
+        *
+        * @return array An array tree of UploadedFileInterface instances; an empty
+        *     array MUST be returned if no data is present.
+        */
+       public function getUploadedFiles() {
+               return $this->uploadedFiles;
+       }
+
+       /**
+        * Create a new instance with the specified uploaded files.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * updated body parameters.
+        *
+        * @param array $uploadedFiles An array tree of UploadedFileInterface instances.
+        * @return ServerRequest
+        * @throws \InvalidArgumentException if an invalid structure is provided.
+        */
+       public function withUploadedFiles(array $uploadedFiles) {
+               $this->validateUploadedFiles($uploadedFiles);
+               $clonedObject = clone $this;
+               $clonedObject->uploadedFiles = $uploadedFiles;
+               return $clonedObject;
+       }
+
+       /**
+        * Retrieve any parameters provided in the request body.
+        *
+        * If the request Content-Type is either application/x-www-form-urlencoded
+        * or multipart/form-data, and the request method is POST, this method MUST
+        * return the contents of $_POST.
+        *
+        * Otherwise, this method may return any results of deserializing
+        * the request body content; as parsing returns structured content, the
+        * potential types MUST be arrays or objects only. A null value indicates
+        * the absence of body content.
+        *
+        * @return null|array|object The deserialized body parameters, if any.
+        *     These will typically be an array or object.
+        */
+       public function getParsedBody() {
+               return $this->parsedBody;
+       }
+
+       /**
+        * Return an instance with the specified body parameters.
+        *
+        * These MAY be injected during instantiation.
+        *
+        * If the request Content-Type is either application/x-www-form-urlencoded
+        * or multipart/form-data, and the request method is POST, use this method
+        * ONLY to inject the contents of $_POST.
+        *
+        * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
+        * deserializing the request body content. Deserialization/parsing returns
+        * structured data, and, as such, this method ONLY accepts arrays or objects,
+        * or a null value if nothing was available to parse.
+        *
+        * As an example, if content negotiation determines that the request data
+        * is a JSON payload, this method could be used to create a request
+        * instance with the deserialized parameters.
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * updated body parameters.
+        *
+        * @param null|array|object $data The deserialized body data. This will
+        *     typically be in an array or object.
+        * @return ServerRequest
+        * @throws \InvalidArgumentException if an unsupported argument type is
+        *     provided.
+        */
+       public function withParsedBody($data) {
+               $clonedObject = clone $this;
+               $clonedObject->parsedBody = $data;
+               return $clonedObject;
+       }
+
+       /**
+        * Retrieve attributes derived from the request.
+        *
+        * The request "attributes" may be used to allow injection of any
+        * parameters derived from the request: e.g., the results of path
+        * match operations; the results of decrypting cookies; the results of
+        * deserializing non-form-encoded message bodies; etc. Attributes
+        * will be application and request specific, and CAN be mutable.
+        *
+        * @return array Attributes derived from the request.
+        */
+       public function getAttributes() {
+               return $this->attributes;
+       }
+
+       /**
+        * Retrieve a single derived request attribute.
+        *
+        * Retrieves a single derived request attribute as described in
+        * getAttributes(). If the attribute has not been previously set, returns
+        * the default value as provided.
+        *
+        * This method obviates the need for a hasAttribute() method, as it allows
+        * specifying a default value to return if the attribute is not found.
+        *
+        * @see getAttributes()
+        *
+        * @param string $name The attribute name.
+        * @param mixed $default Default value to return if the attribute does not exist.
+        * @return mixed
+        */
+       public function getAttribute($name, $default = NULL) {
+               return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
+       }
+
+       /**
+        * Return an instance with the specified derived request attribute.
+        *
+        * This method allows setting a single derived request attribute as
+        * described in getAttributes().
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that has the
+        * updated attribute.
+        *
+        * @see getAttributes()
+        *
+        * @param string $name The attribute name.
+        * @param mixed $value The value of the attribute.
+        * @return ServerRequest
+        */
+       public function withAttribute($name, $value) {
+               $clonedObject = clone $this;
+               $clonedObject->attributes[$name] = $value;
+               return $clonedObject;
+       }
+
+       /**
+        * Return an instance that removes the specified derived request attribute.
+        *
+        * This method allows removing a single derived request attribute as
+        * described in getAttributes().
+        *
+        * This method MUST be implemented in such a way as to retain the
+        * immutability of the message, and MUST return an instance that removes
+        * the attribute.
+        *
+        * @see getAttributes()
+        *
+        * @param string $name The attribute name.
+        * @return ServerRequest
+        */
+       public function withoutAttribute($name) {
+               $clonedObject = clone $this;
+               if (!isset($clonedObject->attributes[$name])) {
+                       return $clonedObject;
+               } else {
+                       unset($clonedObject->attributes[$name]);
+                       return $clonedObject;
+               }
+       }
+
+       /**
+        * Recursively validate the structure in an uploaded files array.
+        *
+        * @param array $uploadedFiles
+        * @throws \InvalidArgumentException if any leaf is not an UploadedFileInterface instance.
+        */
+       protected function validateUploadedFiles(array $uploadedFiles) {
+               foreach ($uploadedFiles as $file) {
+                       if (is_array($file)) {
+                               $this->validateUploadedFiles($file);
+                               continue;
+                       }
+                       if (!$file instanceof UploadedFileInterface) {
+                               throw new \InvalidArgumentException('Invalid file in uploaded files structure.', 1436717281);
+                       }
+               }
+       }
+
+}
diff --git a/typo3/sysext/core/Classes/Http/ServerRequestFactory.php b/typo3/sysext/core/Classes/Http/ServerRequestFactory.php
new file mode 100644 (file)
index 0000000..73014f1
--- /dev/null
@@ -0,0 +1,152 @@
+<?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\UploadedFileInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Class ServerRequestFactory to create ServerRequest objects
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class ServerRequestFactory {
+
+       /**
+        * Create a request from the original superglobal variables.
+        *
+        * @return ServerRequest
+        * @throws \InvalidArgumentException when invalid file values given
+        * @internal Note that this is not public API yet.
+        */
+       static public function fromGlobals() {
+               $serverParameters = $_SERVER;
+               $headers = static::prepareHeaders($serverParameters);
+
+               $method = isset($serverParameters['REQUEST_METHOD']) ? $serverParameters['REQUEST_METHOD'] : 'GET';
+               $uri = new Uri(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'));
+
+               $request = new ServerRequest(
+                       $uri,
+                       $method,
+                       'php://input',
+                       $headers,
+                       $serverParameters,
+                       static::normalizeUploadedFiles($_FILES)
+               );
+
+               if (!empty($_COOKIE)) {
+                       $request = $request->withCookieParams($_COOKIE);
+               }
+               $queryParameters = GeneralUtility::_GET();
+               if (!empty($queryParameters)) {
+                       $request = $request->withQueryParams($queryParameters);
+               }
+               $parsedBody = GeneralUtility::_POST();
+               if (!empty($parsedBody)) {
+                       $request = $request->withParsedBody($parsedBody);
+               }
+               return $request;
+       }
+
+       /**
+        * Fetch headers from $_SERVER variables
+        * which are only the ones starting with HTTP_* and CONTENT_*
+        *
+        * @param array $server
+        * @return array
+        */
+       protected static function prepareHeaders(array $server) {
+               $headers = array();
+               foreach ($server as $key => $value) {
+                       if (strpos($key, 'HTTP_COOKIE') === 0) {
+                               // Cookies are handled using the $_COOKIE superglobal
+                               continue;
+                       }
+                       if (!empty($value)) {
+                               if (strpos($key, 'HTTP_') === 0) {
+                                       $name = strtr(substr($key, 5), '_', ' ');
+                                       $name = strtr(ucwords(strtolower($name)), ' ', '-');
+                                       $name = strtolower($name);
+                                       $headers[$name] = $value;
+                               } elseif (strpos($key, 'CONTENT_') === 0) {
+                                       $name = substr($key, 8); // Content-
+                                       $name = 'Content-' . (($name == 'MD5') ? $name : ucfirst(strtolower($name)));
+                                       $name = strtolower($name);
+                                       $headers[$name] = $value;
+                               }
+                       }
+               }
+               return $headers;
+       }
+
+       /**
+        * Normalize uploaded files
+        *
+        * Transforms each value into an UploadedFileInterface instance, and ensures that nested arrays are normalized.
+        *
+        * @param array $files
+        * @return array
+        * @throws \InvalidArgumentException for unrecognized values
+        */
+       protected static function normalizeUploadedFiles(array $files) {
+               $normalizedFileUploads = array();
+               foreach ($files as $key => $value) {
+                       if ($value instanceof UploadedFileInterface) {
+                               $normalizedFileUploads[$key] = $value;
+                       } elseif (is_array($value)) {
+                               if (isset($value['tmp_name'])) {
+                                       $normalizedFileUploads[$key] = self::createUploadedFile($value);
+                               } else {
+                                       $normalizedFileUploads[$key] = self::normalizeUploadedFiles($value);
+                               }
+                       } else {
+                               throw new \InvalidArgumentException('Invalid value in files specification.', 1436717282);
+                       }
+               }
+               return $normalizedFileUploads;
+       }
+
+       /**
+        * Create and return an UploadedFile instance from a $_FILES specification.
+        *
+        * If the specification represents an array of values, this method will
+        * delegate to normalizeNestedFileSpec() and return that return value.
+        *
+        * @param array $value $_FILES structure
+        * @return UploadedFileInterface[]|UploadedFileInterface
+        */
+       protected static function createUploadedFile(array $value) {
+               if (is_array($value['tmp_name'])) {
+                       $files = array();
+                       foreach (array_keys($value['tmp_name']) as $key) {
+                               $data = array(
+                                       'tmp_name' => $value['tmp_name'][$key],
+                                       'size'     => $value['size'][$key],
+                                       'error'    => $value['error'][$key],
+                                       'name'     => $value['name'][$key],
+                                       'type'     => $value['type'][$key]
+                               );
+                               $files[$key] = self::createUploadedFile($data);
+                       }
+                       return $files;
+               } else {
+                       return new UploadedFile($value['tmp_name'], $value['size'], $value['error'], $value['name'], $value['type']);
+               }
+       }
+
+}
diff --git a/typo3/sysext/core/Classes/Http/Stream.php b/typo3/sysext/core/Classes/Http/Stream.php
new file mode 100644 (file)
index 0000000..10df091
--- /dev/null
@@ -0,0 +1,343 @@
+<?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\StreamInterface;
+
+/**
+ * Default implementation for the StreamInterface of the PSR-7 standard
+ * Acts mainly as a decorator class for streams/resources.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class Stream implements StreamInterface {
+
+       /**
+        * The actual PHP resource
+        * @var resource
+        */
+       protected $resource;
+
+       /**
+        * @var string|resource
+        */
+       protected $stream;
+
+       /**
+        * Constructor setting up the PHP resource
+        *
+        * @param string|resource $stream
+        * @param string $mode Mode with which to open stream
+        * @throws \InvalidArgumentException
+        */
+       public function __construct($stream, $mode = 'r') {
+               $this->stream = $stream;
+               if (is_resource($stream)) {
+                       $this->resource = $stream;
+               } elseif (is_string($stream)) {
+                       $this->resource = fopen($stream, $mode);
+               } else {
+                       throw new \InvalidArgumentException('Invalid stream provided; must be a string stream identifier or resource', 1436717284);
+               }
+       }
+
+       /**
+        * Reads all data from the stream into a string, from the beginning to end.
+        *
+        * This method MUST attempt to seek to the beginning of the stream before
+        * reading data and read the stream until the end is reached.
+        *
+        * Warning: This could attempt to load a large amount of data into memory.
+        *
+        * This method MUST NOT raise an exception in order to conform with PHP's
+        * string casting operations.
+        *
+        * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
+        * @return string
+        */
+       public function __toString() {
+               if (!$this->isReadable()) {
+                       return '';
+               }
+               try {
+                       $this->rewind();
+                       return $this->getContents();
+               } catch (\RuntimeException $e) {
+                       return '';
+               }
+       }
+
+       /**
+        * Closes the stream and any underlying resources.
+        *
+        * @return void
+        */
+       public function close() {
+               if (!$this->resource) {
+                       return;
+               }
+               $resource = $this->detach();
+               fclose($resource);
+       }
+
+       /**
+        * Separates any underlying resources from the stream.
+        *
+        * After the stream has been detached, the stream is in an unusable state.
+        *
+        * @return resource|null Underlying PHP stream, if any
+        */
+       public function detach() {
+               $resource = $this->resource;
+               $this->resource = NULL;
+               return $resource;
+       }
+
+       /**
+        * Get the size of the stream if known.
+        *
+        * @return int|null Returns the size in bytes if known, or null if unknown.
+        */
+       public function getSize() {
+               if ($this->resource === NULL) {
+                       return NULL;
+               }
+               $stats = fstat($this->resource);
+               return $stats['size'];
+       }
+
+       /**
+        * Returns the current position of the file read/write pointer
+        *
+        * @return int Position of the file pointer
+        * @throws \RuntimeException on error.
+        */
+       public function tell() {
+               if (!$this->resource) {
+                       throw new \RuntimeException('No resource available; cannot tell position', 1436717285);
+               }
+               $result = ftell($this->resource);
+               if (!is_int($result)) {
+                       throw new \RuntimeException('Error occurred during tell operation', 1436717286);
+               }
+               return $result;
+       }
+
+       /**
+        * Returns true if the stream is at the end of the stream.
+        *
+        * @return bool
+        */
+       public function eof() {
+               if (!$this->resource) {
+                       return TRUE;
+               }
+               return feof($this->resource);
+       }
+
+       /**
+        * Returns whether or not the stream is seekable.
+        *
+        * @return bool
+        */
+       public function isSeekable() {
+               if (!$this->resource) {
+                       return FALSE;
+               }
+               return (bool)$this->getMetadata('seekable');
+       }
+
+       /**
+        * Seek to a position in the stream.
+        *
+        * @link http://www.php.net/manual/en/function.fseek.php
+        *
+        * @param int $offset Stream offset
+        * @param int $whence Specifies how the cursor position will be calculated
+        *     based on the seek offset. Valid values are identical to the built-in
+        *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
+        *     offset bytes SEEK_CUR: Set position to current location plus offset
+        *     SEEK_END: Set position to end-of-stream plus offset.
+        *
+        * @throws \RuntimeException on failure.
+        */
+       public function seek($offset, $whence = SEEK_SET) {
+               if (!$this->resource) {
+                       throw new \RuntimeException('No resource available; cannot seek position', 1436717287);
+               }
+
+               if (!$this->isSeekable()) {
+                       throw new \RuntimeException('Stream is not seekable', 1436717288);
+               }
+               $result = fseek($this->resource, $offset, $whence);
+               if ($result !== 0) {
+                       throw new \RuntimeException('Error seeking within stream', 1436717289);
+               }
+       }
+
+       /**
+        * Seek to the beginning of the stream.
+        *
+        * If the stream is not seekable, this method will raise an exception;
+        * otherwise, it will perform a seek(0).
+        *
+        * @see seek()
+        * @link http://www.php.net/manual/en/function.fseek.php
+        * @throws \RuntimeException on failure.
+        */
+       public function rewind() {
+               $this->seek(0);
+       }
+
+       /**
+        * Returns whether or not the stream is writable.
+        *
+        * @return bool
+        */
+       public function isWritable() {
+               if (!$this->resource) {
+                       return FALSE;
+               }
+               $uri = $this->getMetadata('uri');
+               return is_writable($uri);
+       }
+
+       /**
+        * Write data to the stream.
+        *
+        * @param string $string The string that is to be written.
+        * @return int Returns the number of bytes written to the stream.
+        * @throws \RuntimeException on failure.
+        */
+       public function write($string) {
+               if (!$this->resource) {
+                       throw new \RuntimeException('No resource available; cannot write', 1436717290);
+               }
+               $result = fwrite($this->resource, $string);
+               if ($result === FALSE) {
+                       throw new \RuntimeException('Error writing to stream', 1436717291);
+               }
+               return $result;
+       }
+
+       /**
+        * Returns whether or not the stream is readable.
+        *
+        * @return bool
+        */
+       public function isReadable() {
+               if (!$this->resource) {
+                       return FALSE;
+               }
+               $mode = $this->getMetadata('mode');
+               return (strpos($mode, 'r') !== FALSE || strpos($mode, '+') !== FALSE);
+       }
+
+       /**
+        * Read data from the stream.
+        *
+        * @param int $length Read up to $length bytes from the object and return
+        *     them. Fewer than $length bytes may be returned if underlying stream
+        *     call returns fewer bytes.
+        * @return string Returns the data read from the stream, or an empty string
+        *     if no bytes are available.
+        * @throws \RuntimeException if an error occurs.
+        */
+       public function read($length) {
+               if (!$this->resource) {
+                       throw new \RuntimeException('No resource available; cannot read', 1436717292);
+               }
+               if (!$this->isReadable()) {
+                       throw new \RuntimeException('Stream is not readable', 1436717293);
+               }
+               $result = fread($this->resource, $length);
+               if ($result === FALSE) {
+                       throw new \RuntimeException('Error reading stream', 1436717294);
+               }
+               return $result;
+       }
+
+       /**
+        * Returns the remaining contents in a string
+        *
+        * @return string
+        * @throws \RuntimeException if unable to read or an error occurs while
+        *     reading.
+        */
+       public function getContents() {
+               if (!$this->isReadable()) {
+                       return '';
+               }
+               $result = stream_get_contents($this->resource);
+               if ($result === FALSE) {
+                       throw new \RuntimeException('Error reading from stream', 1436717295);
+               }
+               return $result;
+       }
+
+       /**
+        * Get stream metadata as an associative array or retrieve a specific key.
+        *
+        * The keys returned are identical to the keys returned from PHP's
+        * stream_get_meta_data() function.
+        *
+        * @link http://php.net/manual/en/function.stream-get-meta-data.php
+        *
+        * @param string $key Specific metadata to retrieve.
+        *
+        * @return array|mixed|null Returns an associative array if no key is
+        *     provided. Returns a specific key value if a key is provided and the
+        *     value is found, or null if the key is not found.
+        */
+       public function getMetadata($key = NULL) {
+               $metadata = stream_get_meta_data($this->resource);
+               if ($key === NULL) {
+                       return $metadata;
+               }
+               if (!isset($metadata[$key])) {
+                       return NULL;
+               }
+               return $metadata[$key];
+       }
+
+       /**
+        * Attach a new stream/resource to the instance.
+        *
+        * @param string|resource $resource
+        * @param string $mode
+        * @throws \InvalidArgumentException for stream identifier that cannot be cast to a resource
+        * @throws \InvalidArgumentException for non-resource stream
+        */
+       public function attach($resource, $mode = 'r') {
+               $error = NULL;
+               if (!is_resource($resource) && is_string($resource)) {
+                       set_error_handler(function ($e) use (&$error) {
+                               $error = $e;
+                       }, E_WARNING);
+                       $resource = fopen($resource, $mode);
+                       restore_error_handler();
+               }
+               if ($error) {
+                       throw new \InvalidArgumentException('Invalid stream reference provided', 1436717296);
+               }
+               if (!is_resource($resource)) {
+                       throw new \InvalidArgumentException('Invalid stream provided; must be a string stream identifier or resource', 1436717297);
+               }
+               $this->resource = $resource;
+       }
+
+}
diff --git a/typo3/sysext/core/Classes/Http/UploadedFile.php b/typo3/sysext/core/Classes/Http/UploadedFile.php
new file mode 100644 (file)
index 0000000..efeaf4e
--- /dev/null
@@ -0,0 +1,265 @@
+<?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\StreamInterface;
+use Psr\Http\Message\UploadedFileInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Class UploadedFile which represents one uploaded file, usually coming
+ * from $_FILES, according to PSR-7 standard.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class UploadedFile implements UploadedFileInterface {
+
+       /**
+        * @var NULL|string
+        */
+       protected $file;
+
+       /**
+        * @var NULL|StreamInterface
+        */
+       protected $stream;
+
+       /**
+        * @var string
+        */
+       protected $clientFilename;
+
+       /**
+        * @var string
+        */
+       protected $clientMediaType;
+
+       /**
+        * @var int
+        */
+       protected $error;
+
+       /**
+        * @var bool
+        */
+       protected $moved = false;
+
+       /**
+        * @var int
+        */
+       protected $size;
+
+       /**
+        * Constructor method
+        *
+        * @param string|resource $input is either a stream or a filename
+        * @param int $size see $_FILES['size'] from PHP
+        * @param int $errorStatus see $_FILES['error']
+        * @param string $clientFilename the original filename handed over from the client
+        * @param string $clientMediaType the media type (optional)
+        *
+        * @throws \InvalidArgumentException
+        */
+       public function __construct($input, $size, $errorStatus, $clientFilename = NULL, $clientMediaType = NULL) {
+
+               if (is_string($input)) {
+                       $this->file = $input;
+               }
+
+               if (is_resource($input)) {
+                       $this->stream = new Stream($input);
+               } elseif ($input instanceof StreamInterface) {
+                       $this->stream = $input;
+               }
+
+               if (!$this->file && !$this->stream) {
+                       throw new \InvalidArgumentException('The input given was not a valid stream or file.', 1436717301);
+               }
+
+               if (!is_int($size)) {
+                       throw new \InvalidArgumentException('The size provided for an uploaded file must be an integer.', 1436717302);
+               }
+               $this->size = $size;
+
+               if (!is_int($errorStatus) || 0 > $errorStatus || 8 < $errorStatus) {
+                       throw new \InvalidArgumentException('Invalid error status for an uploaded file. See UPLOAD_ERR_* constant in PHP.', 1436717303);
+               }
+               $this->error = $errorStatus;
+
+               if ($clientFilename !== NULL && !is_string($clientFilename)) {
+                       throw new \InvalidArgumentException('Invalid client filename provided for an uploaded file.', 1436717304);
+               }
+               $this->clientFilename = $clientFilename;
+
+               if ($clientMediaType !== NULL && !is_string($clientMediaType)) {
+                       throw new \InvalidArgumentException('Invalid client media type provided for an uploaded file.', 1436717305);
+               }
+               $this->clientMediaType = $clientMediaType;
+       }
+
+       /**
+        * Retrieve a stream representing the uploaded file.
+        * Returns a StreamInterface instance, representing the uploaded file. The purpose of this method
+        * is to allow utilizing native PHP stream functionality to manipulate the file upload, such as
+        * stream_copy_to_stream() (though the result will need to be decorated in a native PHP stream wrapper
+        * to work with such functions).
+        *
+        * If the moveTo() method has been called previously, this method raises an exception.
+        *
+        * @return StreamInterface Stream representation of the uploaded file.
+        * @throws \RuntimeException in cases when no stream is available or can be created.
+        */
+       public function getStream() {
+               if ($this->moved) {
+                       throw new \RuntimeException('Cannot retrieve stream as it was moved.', 1436717306);
+               }
+
+               if ($this->stream instanceof StreamInterface) {
+                       return $this->stream;
+               }
+
+               $this->stream = new Stream($this->file);
+               return $this->stream;
+       }
+
+        /**
+        * Move the uploaded file to a new location.
+        *
+        * Use this method as an alternative to move_uploaded_file(). This method is
+        * guaranteed to work in both SAPI and non-SAPI environments.
+        * Implementations must determine which environment they are in, and use the
+        * appropriate method (move_uploaded_file(), rename(), or a stream
+        * operation) to perform the operation.
+        *
+        * $targetPath may be an absolute path, or a relative path. If it is a
+        * relative path, resolution should be the same as used by PHP's rename()
+        * function.
+        *
+        * The original file or stream MUST be removed on completion.
+        *
+        * If this method is called more than once, any subsequent calls MUST raise
+        * an exception.
+        *
+        * When used in an SAPI environment where $_FILES is populated, when writing
+        * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
+        * used to ensure permissions and upload status are verified correctly.
+        *
+        * If you wish to move to a stream, use getStream(), as SAPI operations
+        * cannot guarantee writing to stream destinations.
+        *
+        * @see http://php.net/is_uploaded_file
+        * @see http://php.net/move_uploaded_file
+        * @param string $targetPath Path to which to move the uploaded file.
+        * @throws \InvalidArgumentException if the $path specified is invalid.
+        * @throws \RuntimeException on any error during the move operation, or on the second or subsequent call to the method.
+        */
+       public function moveTo($targetPath) {
+               if (!is_string($targetPath) || empty($targetPath)) {
+                       throw new \InvalidArgumentException('Invalid path while moving an uploaded file.', 1436717307);
+               }
+
+               if ($this->moved) {
+                       throw new \RuntimeException('Cannot move uploaded file, as it was already moved.', 1436717308);
+               }
+
+               // Check if the target path is inside the allowed paths of TYPO3, and make it absolute.
+               $targetPath = GeneralUtility::getFileAbsFileName($targetPath);
+               if (empty($targetPath)) {
+                       throw new \RuntimeException('Cannot move uploaded file, as it was already moved.', 1436717309);
+               }
+
+               if (!empty($this->file) && is_uploaded_file($this->file)) {
+                       if (GeneralUtility::upload_copy_move($this->file, $targetPath . basename($this->file)) === FALSE) {
+                               throw new \RuntimeException('An error occurred while moving uploaded file', 1436717310);
+                       }
+               } elseif ($this->stream) {
+                       $handle = fopen($targetPath, 'wb+');
+                       if ($handle === FALSE) {
+                               throw new \RuntimeException('Unable to write to target path.', 1436717311);
+                       }
+
+                       $this->stream->rewind();
+                       while (!$this->stream->eof()) {
+                               fwrite($handle, $this->stream->read(4096));
+                       }
+
+                       fclose($handle);
+               }
+
+               $this->moved = TRUE;
+       }
+
+       /**
+        * Retrieve the file size.
+        * Usually returns the value stored in the "size" key of
+        * the file in the $_FILES array if available, as PHP calculates this based
+        * on the actual size transmitted.
+        *
+        * @return int|NULL The file size in bytes or null if unknown.
+        */
+       public function getSize() {
+               return $this->size;
+       }
+
+       /**
+        * Retrieve the error associated with the uploaded file.
+        * Usually returns the value stored in the "error" key of
+        * the file in the $_FILES array.
+        *
+        * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
+        *
+        * If the file was uploaded successfully, this method MUST return
+        * UPLOAD_ERR_OK.
+        *
+        * @see http://php.net/manual/en/features.file-upload.errors.php
+        * @return int One of PHP's UPLOAD_ERR_XXX constants.
+        */
+       public function getError() {
+               return $this->error;
+       }
+
+       /**
+        * Retrieve the filename sent by the client.
+        * Usually returns the value stored in the "name" key of
+        * the file in the $_FILES array.
+        *
+        * Do not trust the value returned by this method. A client could send
+        * a malicious filename with the intention to corrupt or hack your
+        * application.
+        *
+        * @return string|NULL The filename sent by the client or null if none was provided.
+        */
+       public function getClientFilename() {
+               return $this->clientFilename;
+       }
+
+       /**
+        * Retrieve the media type sent by the client.
+        * Usually returns the value stored in the "type" key of
+        * the file in the $_FILES array.
+        *
+        * Do not trust the value returned by this method. A client could send
+        * a malicious media type with the intention to corrupt or hack your
+        * application.
+        *
+        * @return string|NULL The media type sent by the client or null if none was provided.
+        */
+       public function getClientMediaType() {
+               return $this->clientMediaType;
+       }
+
+}
diff --git a/typo3/sysext/core/Classes/Http/Uri.php b/typo3/sysext/core/Classes/Http/Uri.php
new file mode 100644 (file)
index 0000000..98cd63d
--- /dev/null
@@ -0,0 +1,716 @@
+<?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\UriInterface;
+
+/**
+ * Represents a URI based on the PSR-7 Standard.
+ *
+ * Highly inspired by https://github.com/phly/http/
+ *
+ * @internal Note that this is not public API yet.
+ */
+class Uri implements UriInterface {
+
+       /**
+        * Sub-delimiters used in query strings and fragments.
+        *
+        * @const string
+        */
+       const SUBDELIMITER_CHARLIST = '!\$&\'\(\)\*\+,;=';
+
+       /**
+        * Unreserved characters used in paths, query strings, and fragments.
+        *
+        * @const string
+        */
+       const UNRESERVED_CHARLIST = 'a-zA-Z0-9_\-\.~';
+
+       /**
+        * The default scheme for the URI
+        * @var string
+        */
+       protected $scheme;
+
+       /**
+        * @var int[] Associative array containing schemes and their default ports.
+        */
+       protected $supportedSchemes = array(
+               'http'  => 80,
+               'https' => 443
+       );
+
+       /**
+        * The authority part of the URI
+        * @var string
+        */
+       protected $authority = '';
+
+       /**
+        * The userInfo part of the URI
+        * @var string
+        */
+       protected $userInfo = '';
+
+       /**
+        * The host part of the URI
+        * @var string
+        */
+       protected $host = '';
+
+       /**
+        * The port of the URI (empty if it is the standard port for the scheme)
+        * @var int|NULL
+        */
+       protected $port = NULL;
+
+       /**
+        * The path part of the URI (can be empty or /)
+        * @var string
+        */
+       protected $path = '';
+
+       /**
+        * The query part of the URI without the ?
+        * @var string
+        */
+       protected $query;
+
+       /**
+        * The fragment part of the URI without the # before
+        * @var string
+        */
+       protected $fragment;
+
+       /**
+        * @param string|null $uri The full URI including query string and fragment
+        * @throws \InvalidArgumentException when the URI is not a string
+        */
+       public function __construct($uri = '') {
+          if (!is_string($uri)) {
+                  $argumentType = is_object($uri) ? get_class($uri) : gettype($uri);
+                  throw new \InvalidArgumentException('URI passed must be a string, but is of type "' . $argumentType . '"', 1436717320);
+               }
+               if (!empty($uri)) {
+                       $this->parseUri($uri);
+               }
+       }
+
+       /**
+        * helper function for parsing the full URI string
+        * @param string $uri
+        * @throws \InvalidArgumentException if the URI is malformed.
+        */
+       protected function parseUri($uri) {
+               $uriParts = parse_url($uri);
+
+               if ($uriParts === FALSE) {
+                       throw new \InvalidArgumentException('The parsedUri string appears to be malformed', 1436717322);
+               }
+
+               if (isset($uriParts['scheme'])) {
+                       $this->scheme = $this->sanitizeScheme($uriParts['scheme']);
+               }
+
+               if (isset($uriParts['user'])) {
+                       $this->userInfo = $uriParts['user'];
+                       if (isset($uriParts['pass'])) {
+                               $this->userInfo .= ':' . $uriParts['pass'];
+                       }
+               }
+
+               if (isset($uriParts['host'])) {
+                       $this->host = $uriParts['host'];
+               }
+
+               if (isset($uriParts['port'])) {
+                       $this->port = (int)$uriParts['port'];
+               }
+
+               if (isset($uriParts['path'])) {
+                       $this->path = $this->sanitizePath($uriParts['path']);
+               }
+
+               if (isset($uriParts['query'])) {
+                       $this->query = $this->sanitizeQuery($uriParts['query']);
+               }
+
+               if (isset($uriParts['fragment'])) {
+                       $this->fragment = $this->sanitizeFragment($uriParts['fragment']);
+               }
+       }
+
+       /**
+        * Retrieve the scheme component of the URI.
+        *
+        * If no scheme is present, this method MUST return an empty string.
+        *
+        * The value returned MUST be normalized to lowercase, per RFC 3986
+        * Section 3.1.
+        *
+        * The trailing ":" character is not part of the scheme and MUST NOT be
+        * added.
+        *
+        * @see https://tools.ietf.org/html/rfc3986#section-3.1
+        * @return string The URI scheme.
+        */
+       public function getScheme() {
+               return $this->scheme;
+       }
+
+       /**
+        * Retrieve the authority component of the URI.
+        *
+        * If no authority information is present, this method MUST return an empty
+        * string.
+        *
+        * The authority syntax of the URI is:
+        *
+        * <pre>
+        * [user-info@]host[:port]
+        * </pre>
+        *
+        * If the port component is not set or is the standard port for the current
+        * scheme, it SHOULD NOT be included.
+        *
+        * @see https://tools.ietf.org/html/rfc3986#section-3.2
+        * @return string The URI authority, in "[user-info@]host[:port]" format.
+        */
+       public function getAuthority() {
+               if (empty($this->host)) {
+                       return '';
+               }
+
+               $authority = $this->host;
+               if (!empty($this->userInfo)) {
+                       $authority = $this->userInfo . '@' . $authority;
+               }
+
+               if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) {
+                       $authority .= ':' . $this->port;
+               }
+
+               return $authority;
+       }
+
+       /**
+        * Retrieve the user information component of the URI.
+        *
+        * If no user information is present, this method MUST return an empty
+        * string.
+        *
+        * If a user is present in the URI, this will return that value;
+        * additionally, if the password is also present, it will be appended to the
+        * user value, with a colon (":") separating the values.
+        *
+        * The trailing "@" character is not part of the user information and MUST
+        * NOT be added.
+        *
+        * @return string The URI user information, in "username[:password]" format.
+        */
+       public function getUserInfo() {
+               return $this->userInfo;
+       }
+
+       /**
+        * Retrieve the host component of the URI.
+        *
+        * If no host is present, this method MUST return an empty string.
+        *
+        * The value returned MUST be normalized to lowercase, per RFC 3986
+        * Section 3.2.2.
+        *
+        * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
+        * @return string The URI host.
+        */
+       public function getHost() {
+               return $this->host;
+       }
+
+       /**
+        * Retrieve the port component of the URI.
+        *
+        * If a port is present, and it is non-standard for the current scheme,
+        * this method MUST return it as an integer. If the port is the standard port
+        * used with the current scheme, this method SHOULD return null.
+        *
+        * If no port is present, and no scheme is present, this method MUST return
+        * a null value.
+        *
+        * If no port is present, but a scheme is present, this method MAY return
+        * the standard port for that scheme, but SHOULD return null.
+        *
+        * @return null|int The URI port.
+        */
+       public function getPort() {
+               return $this->isNonStandardPort($this->scheme, $this->host, $this->port) ? $this->port : NULL;
+       }
+
+       /**
+        * Retrieve the path component of the URI.
+        *
+        * The path can either be empty or absolute (starting with a slash) or
+        * rootless (not starting with a slash). Implementations MUST support all
+        * three syntaxes.
+        *
+        * Normally, the empty path "" and absolute path "/" are considered equal as
+        * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
+        * do this normalization because in contexts with a trimmed base path, e.g.
+        * the front controller, this difference becomes significant. It's the task
+        * of the user to handle both "" and "/".
+        *
+        * The value returned MUST be percent-encoded, but MUST NOT double-encode
+        * any characters. To determine what characters to encode, please refer to
+        * RFC 3986, Sections 2 and 3.3.
+        *
+        * As an example, if the value should include a slash ("/") not intended as
+        * delimiter between path segments, that value MUST be passed in encoded
+        * form (e.g., "%2F") to the instance.
+        *
+        * @see https://tools.ietf.org/html/rfc3986#section-2
+        * @see https://tools.ietf.org/html/rfc3986#section-3.3
+        * @return string The URI path.
+        */
+       public function getPath() {
+               return $this->path;
+       }
+
+       /**
+        * Retrieve the query string of the URI.
+        *
+        * If no query string is present, this method MUST return an empty string.
+        *
+        * The leading "?" character is not part of the query and MUST NOT be
+        * added.
+        *
+        * The value returned MUST be percent-encoded, but MUST NOT double-encode
+        * any characters. To determine what characters to encode, please refer to
+        * RFC 3986, Sections 2 and 3.4.
+        *
+        * As an example, if a value in a key/value pair of the query string should
+        * include an ampersand ("&") not intended as a delimiter between values,
+        * that value MUST be passed in encoded form (e.g., "%26") to the instance.
+        *
+        * @see https://tools.ietf.org/html/rfc3986#section-2
+        * @see https://tools.ietf.org/html/rfc3986#section-3.4
+        * @return string The URI query string.
+        */
+       public function getQuery() {
+               return $this->query;
+       }
+
+       /**
+        * Retrieve the fragment component of the URI.
+        *
+        * If no fragment is present, this method MUST return an empty string.
+        *
+        * The leading "#" character is not part of the fragment and MUST NOT be
+        * added.
+        *
+        * The value returned MUST be percent-encoded, but MUST NOT double-encode
+        * any characters. To determine what characters to encode, please refer to
+        * RFC 3986, Sections 2 and 3.5.
+        *
+        * @see https://tools.ietf.org/html/rfc3986#section-2
+        * @see https://tools.ietf.org/html/rfc3986#section-3.5
+        * @return string The URI fragment.
+        */
+       public function getFragment() {
+               return $this->fragment;
+       }
+
+       /**
+        * Return an instance with the specified scheme.
+        *
+        * This method MUST retain the state of the current instance, and return
+        * an instance that contains the specified scheme.
+        *
+        * Implementations MUST support the schemes "http" and "https" case
+        * insensitively, and MAY accommodate other schemes if required.
+        *
+        * An empty scheme is equivalent to removing the scheme.
+        *
+        * @param string $scheme The scheme to use with the new instance.
+        *
+        * @return self A new instance with the specified scheme.
+        * @throws \InvalidArgumentException for invalid or unsupported schemes.
+        */
+       public function withScheme($scheme) {
+               $scheme = $this->sanitizeScheme($scheme);
+
+               $clonedObject = clone $this;
+               $clonedObject->scheme = $scheme;
+               return $clonedObject;
+       }
+
+       /**
+        * Return an instance with the specified user information.
+        *
+        * This method MUST retain the state of the current instance, and return
+        * an instance that contains the specified user information.
+        *
+        * Password is optional, but the user information MUST include the
+        * user; an empty string for the user is equivalent to removing user
+        * information.
+        *
+        * @param string $user The user name to use for authority.
+        * @param null|string $password The password associated with $user.
+        *
+        * @return self A new instance with the specified user information.
+        */
+       public function withUserInfo($user, $password = NULL) {
+
+               $userInfo = $user;
+               if (!empty($password)) {
+                       $userInfo .= ':' . $password;
+               }
+
+               $clonedObject = clone $this;
+               $clonedObject->userInfo = $userInfo;
+               return $clonedObject;
+       }
+
+       /**
+        * Return an instance with the specified host.
+        *
+        * This method MUST retain the state of the current instance, and return
+        * an instance that contains the specified host.
+        *
+        * An empty host value is equivalent to removing the host.
+        *
+        * @param string $host The hostname to use with the new instance.
+        *
+        * @return self A new instance with the specified host.
+        * @throws \InvalidArgumentException for invalid hostnames.
+        */
+       public function withHost($host) {
+               $clonedObject = clone $this;
+               $clonedObject->host = $host;
+               return $clonedObject;
+       }
+
+       /**
+        * Return an instance with the specified port.
+        *
+        * This method MUST retain the state of the current instance, and return
+        * an instance that contains the specified port.
+        *
+        * Implementations MUST raise an exception for ports outside the
+        * established TCP and UDP port ranges.
+        *
+        * A null value provided for the port is equivalent to removing the port
+        * information.
+        *
+        * @param null|int $port The port to use with the new instance; a null value
+        *     removes the port information.
+        *
+        * @return self A new instance with the specified port.
+        * @throws \InvalidArgumentException for invalid ports.
+        */
+       public function withPort($port) {
+               if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($port) === FALSE) {
+                       $argumentType = is_object($port) ? get_class($port) : gettype($port);
+                       throw new \InvalidArgumentException('Invalid port "' . $argumentType . '" specified, must be an integer.', 1436717324);
+               }
+
+               $port = (int)$port;
+               if ($port < 1 || $port > 65535) {
+                       throw new \InvalidArgumentException('Invalid port "' . $port . '" specified, must be a valid TCP/UDP port.', 1436717326);
+               }
+
+               $clonedObject = clone $this;
+               $clonedObject->port = $port;
+               return $clonedObject;
+       }
+
+       /**
+        * Return an instance with the specified path.
+        *
+        * This method MUST retain the state of the current instance, and return
+        * an instance that contains the specified path.
+        *
+        * The path can either be empty or absolute (starting with a slash) or
+        * rootless (not starting with a slash). Implementations MUST support all
+        * three syntaxes.
+        *
+        * If the path is intended to be domain-relative rather than path relative then
+        * it must begin with a slash ("/"). Paths not starting with a slash ("/")
+        * are assumed to be relative to some base path known to the application or
+        * consumer.
+        *
+        * Users can provide both encoded and decoded path characters.
+        * Implementations ensure the correct encoding as outlined in getPath().
+        *
+        * @param string $path The path to use with the new instance.
+        *
+        * @return self A new instance with the specified path.
+        * @throws \InvalidArgumentException for invalid paths.
+        */
+       public function withPath($path) {
+               if (!is_string($path)) {
+                       throw new \InvalidArgumentException('Invalid path provided. Must be of type string.', 1436717328);
+               }
+
+               if (strpos($path, '?') !== FALSE) {
+                       throw new \InvalidArgumentException('Invalid path provided. Must not contain a query string.', 1436717330);
+               }
+
+               if (strpos($path, '#') !== FALSE) {
+                       throw new \InvalidArgumentException('Invalid path provided; must not contain a URI fragment', 1436717332);
+               }
+
+               $path = $this->sanitizePath($path);
+               $clonedObject = clone $this;
+               $clonedObject->path = $path;
+               return $clonedObject;
+       }
+
+       /**
+        * Return an instance with the specified query string.
+        *
+        * This method MUST retain the state of the current instance, and return
+        * an instance that contains the specified query string.
+        *
+        * Users can provide both encoded and decoded query characters.
+        * Implementations ensure the correct encoding as outlined in getQuery().
+        *
+        * An empty query string value is equivalent to removing the query string.
+        *
+        * @param string $query The query string to use with the new instance.
+        *
+        * @return self A new instance with the specified query string.
+        * @throws \InvalidArgumentException for invalid query strings.
+        */
+       public function withQuery($query) {
+               if (!is_string($query)) {
+                       throw new \InvalidArgumentException('Query string must be a string.', 1436717334);
+               }
+
+               if (strpos($query, '#') !== FALSE) {
+                       throw new \InvalidArgumentException('Query string must not include a URI fragment.', 1436717336);
+               }
+
+               $query = $this->sanitizeQuery($query);
+               $clonedObject = clone $this;
+               $clonedObject->query = $query;
+               return $clonedObject;
+       }
+
+       /**
+        * Return an instance with the specified URI fragment.
+        *
+        * This method MUST retain the state of the current instance, and return
+        * an instance that contains the specified URI fragment.
+        *
+        * Users can provide both encoded and decoded fragment characters.
+        * Implementations ensure the correct encoding as outlined in getFragment().
+        *
+        * An empty fragment value is equivalent to removing the fragment.
+        *
+        * @param string $fragment The fragment to use with the new instance.
+        *
+        * @return self A new instance with the specified fragment.
+        */
+       public function withFragment($fragment) {
+               $fragment = $this->sanitizeFragment($fragment);
+               $clonedObject = clone $this;
+               $clonedObject->fragment = $fragment;
+               return $clonedObject;
+       }
+
+       /**
+        * Return the string representation as a URI reference.
+        *
+        * Depending on which components of the URI are present, the resulting
+        * string is either a full URI or relative reference according to RFC 3986,
+        * Section 4.1. The method concatenates the various components of the URI,
+        * using the appropriate delimiters:
+        *
+        * - If a scheme is present, it MUST be suffixed by ":".
+        * - If an authority is present, it MUST be prefixed by "//".
+        * - The path can be concatenated without delimiters. But there are two
+        *   cases where the path has to be adjusted to make the URI reference
+        *   valid as PHP does not allow to throw an exception in __toString():
+        *     - If the path is rootless and an authority is present, the path MUST
+        *       be prefixed by "/".
+        *     - If the path is starting with more than one "/" and no authority is
+        *       present, the starting slashes MUST be reduced to one.
+        * - If a query is present, it MUST be prefixed by "?".
+        * - If a fragment is present, it MUST be prefixed by "#".
+        *
+        * @see http://tools.ietf.org/html/rfc3986#section-4.1
+        * @return string
+        */
+       public function __toString() {
+               $uri = '';
+
+               if (!empty($this->scheme)) {
+                       $uri .= $this->scheme . '://';
+               }
+
+               $authority = $this->getAuthority();
+               if (!empty($authority)) {
+                       $uri .= $authority;
+               }
+
+               $path = $this->getPath();
+               if (!empty($path)) {
+                       $uri .= '/' . ltrim($path, '/');
+               }
+
+               if ($this->query) {
+                       $uri .= '?' . $this->query;
+               }
+               if ($this->fragment) {
+                       $uri .= '#' . $this->fragment;
+               }
+               return $uri;
+       }
+
+       /**
+        * Is a given port non-standard for the current scheme?
+        *
+        * @param string $scheme
+        * @param string $host
+        * @param int $port
+        * @return bool
+        */
+       protected function isNonStandardPort($scheme, $host, $port) {
+               if (empty($scheme)) {
+                       return TRUE;
+               }
+
+               if (empty($host) || empty($port)) {
+                       return FALSE;
+               }
+
+               return !isset($this->supportedSchemes[$scheme]) || $port !== $this->supportedSchemes[$scheme];
+       }
+
+       /**
+        * Filters the scheme to ensure it is a valid scheme.
+        *
+        * @param string $scheme Scheme name.
+        *
+        * @return string Filtered scheme.
+        * @throws \InvalidArgumentException when a scheme is given which is not supported
+        */
+       protected function sanitizeScheme($scheme) {
+               $scheme = strtolower($scheme);
+               $scheme = preg_replace('#:(//)?$#', '', $scheme);
+
+               if (empty($scheme)) {
+                       return '';
+               }
+
+               if (!array_key_exists($scheme, $this->supportedSchemes)) {
+                       throw new \InvalidArgumentException('Unsupported scheme "' . $scheme . '"; must be any empty string or in the set (' . implode(', ', array_keys($this->supportedSchemes)) . ')', 1436717338);
+               }
+
+               return $scheme;
+       }
+
+       /**
+        * Filters the path of a URI to ensure it is properly encoded.
+        *
+        * @param string $path
+        * @return string
+        */
+       protected function sanitizePath($path) {
+               return preg_replace_callback(
+                       '/(?:[^' . self::UNRESERVED_CHARLIST . ':@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
+                       function($matches) {
+                               return rawurlencode($matches[0]);
+                       },
+                       $path
+               );
+       }
+
+       /**
+        * Filter a query string to ensure it is propertly encoded.
+        *
+        * Ensures that the values in the query string are properly urlencoded.
+        *
+        * @param string $query
+        * @return string
+        */
+       protected function sanitizeQuery($query) {
+               if (!empty($query) && strpos($query, '?') === 0) {
+                       $query = substr($query, 1);
+               }
+
+               $parts = explode('&', $query);
+               foreach ($parts as $index => $part) {
+                       list($key, $value) = $this->splitQueryValue($part);
+                       if ($value === NULL) {
+                               $parts[$index] = $this->sanitizeQueryOrFragment($key);
+                               continue;
+                       }
+                       $parts[$index] = $this->sanitizeQueryOrFragment($key) . '=' . $this->sanitizeQueryOrFragment($value);
+               }
+
+               return implode('&', $parts);
+       }
+
+       /**
+        * Split a query value into a key/value tuple.
+        *
+        * @param string $value
+        * @return array A value with exactly two elements, key and value
+        */
+       protected function splitQueryValue($value) {
+               $data = explode('=', $value, 2);
+               if (count($data) === 1) {
+                       $data[] = NULL;
+               }
+               return $data;
+       }
+
+       /**
+        * Filter a fragment value to ensure it is properly encoded.
+        *
+        * @param null|string $fragment
+        * @return string
+        */
+       protected function sanitizeFragment($fragment) {
+               if ($fragment === NULL) {
+                       $fragment = '';
+               }
+
+               if (!empty($fragment) && strpos($fragment, '#') === 0) {
+                       $fragment = substr($fragment, 1);
+               }
+
+               return $this->sanitizeQueryOrFragment($fragment);
+       }
+
+       /**
+        * Filter a query string key or value, or a fragment.
+        *
+        * @param string $value
+        * @return string
+        */
+       protected function sanitizeQueryOrFragment($value) {
+               return preg_replace_callback(
+                       '/(?:[^' . self::UNRESERVED_CHARLIST . self::SUBDELIMITER_CHARLIST . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/',
+                       function($matches) {
+                               return rawurlencode($matches[0]);
+                       },
+                       $value
+               );
+       }
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/MessageTest.php b/typo3/sysext/core/Tests/Unit/Http/MessageTest.php
new file mode 100644 (file)
index 0000000..75938b0
--- /dev/null
@@ -0,0 +1,302 @@
+<?php
+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 TYPO3\CMS\Core\Http\Stream;
+use TYPO3\CMS\Core\Http\Message;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\Message
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class MessageTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var Stream
+        */
+       protected $stream;
+
+       /**
+        * @var Message
+        */
+       protected $message;
+
+       public function setUp() {
+               $this->stream = new Stream('php://memory', 'wb+');
+               $this->message = (new Message())->withBody($this->stream);
+       }
+
+       /**
+        * @test
+        */
+       public function protocolHasAcceptableDefault() {
+               $this->assertEquals('1.1', $this->message->getProtocolVersion());
+       }
+
+       /**
+        * @test
+        */
+       public function protocolMutatorReturnsCloneWithChanges() {
+               $message = $this->message->withProtocolVersion('1.0');
+               $this->assertNotSame($this->message, $message);
+               $this->assertEquals('1.0', $message->getProtocolVersion());
+       }
+
+       /**
+        * @test
+        */
+       public function usesStreamProvidedInConstructorAsBody() {
+               $this->assertSame($this->stream, $this->message->getBody());
+       }
+
+       /**
+        * @test
+        */
+       public function bodyMutatorReturnsCloneWithChanges() {
+               $stream = new Stream('php://memory', 'wb+');
+               $message = $this->message->withBody($stream);
+               $this->assertNotSame($this->message, $message);
+               $this->assertSame($stream, $message->getBody());
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderReturnsHeaderValueAsArray() {
+               $message = $this->message->withHeader('X-Foo', ['Foo', 'Bar']);
+               $this->assertNotSame($this->message, $message);
+               $this->assertEquals(['Foo', 'Bar'], $message->getHeader('X-Foo'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderLineReturnsHeaderValueAsCommaConcatenatedString() {
+               $message = $this->message->withHeader('X-Foo', ['Foo', 'Bar']);
+               $this->assertNotSame($this->message, $message);
+               $this->assertEquals('Foo,Bar', $message->getHeaderLine('X-Foo'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeadersKeepsHeaderCaseSensitivity() {
+               $message = $this->message->withHeader('X-Foo', ['Foo', 'Bar']);
+               $this->assertNotSame($this->message, $message);
+               $this->assertEquals(['X-Foo' => ['Foo', 'Bar']], $message->getHeaders());
+       }
+
+       /**
+        * @test
+        */
+       public function getHeadersReturnsCaseWithWhichHeaderFirstRegistered() {
+               $message = $this->message
+                       ->withHeader('X-Foo', 'Foo')
+                       ->withAddedHeader('x-foo', 'Bar');
+               $this->assertNotSame($this->message, $message);
+               $this->assertEquals(['X-Foo' => ['Foo', 'Bar']], $message->getHeaders());
+       }
+
+       /**
+        * @test
+        */
+       public function hasHeaderReturnsFalseIfHeaderIsNotPresent() {
+               $this->assertFalse($this->message->hasHeader('X-Foo'));
+       }
+
+       /**
+        * @test
+        */
+       public function hasHeaderReturnsTrueIfHeaderIsPresent() {
+               $message = $this->message->withHeader('X-Foo', 'Foo');
+               $this->assertNotSame($this->message, $message);
+               $this->assertTrue($message->hasHeader('X-Foo'));
+       }
+
+       /**
+        * @test
+        */
+       public function addHeaderAppendsToExistingHeader() {
+               $message = $this->message->withHeader('X-Foo', 'Foo');
+               $this->assertNotSame($this->message, $message);
+               $message2 = $message->withAddedHeader('X-Foo', 'Bar');
+               $this->assertNotSame($message, $message2);
+               $this->assertEquals('Foo,Bar', $message2->getHeaderLine('X-Foo'));
+       }
+
+
+       /**
+        * @test
+        */
+       public function canRemoveHeaders() {
+               $message = $this->message->withHeader('X-Foo', 'Foo');
+               $this->assertNotSame($this->message, $message);
+               $this->assertTrue($message->hasHeader('x-foo'));
+               $message2 = $message->withoutHeader('x-foo');
+               $this->assertNotSame($this->message, $message2);
+               $this->assertNotSame($message, $message2);
+               $this->assertFalse($message2->hasHeader('X-Foo'));
+       }
+
+       /**
+        * @test
+        */
+       public function headerRemovalIsCaseInsensitive() {
+               $message = $this->message
+                       ->withHeader('X-Foo', 'Foo')
+                       ->withAddedHeader('x-foo', 'Bar')
+                       ->withAddedHeader('X-FOO', 'Baz');
+               $this->assertNotSame($this->message, $message);
+               $this->assertTrue($message->hasHeader('x-foo'));
+               $message2 = $message->withoutHeader('x-foo');
+               $this->assertNotSame($this->message, $message2);
+               $this->assertNotSame($message, $message2);
+               $this->assertFalse($message2->hasHeader('X-Foo'));
+               $headers = $message2->getHeaders();
+               $this->assertEquals(0, count($headers));
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidGeneralHeaderValuesDataProvider() {
+               return [
+                       'null'   => [NULL],
+                       'true'   => [TRUE],
+                       'false'  => [FALSE],
+                       'int'    => [1],
+                       'float'  => [1.1],
+                       'array'  => [['foo' => ['bar']]],
+                       'object' => [(object) ['foo' => 'bar']],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidGeneralHeaderValuesDataProvider
+        */
+       public function testWithHeaderRaisesExceptionForInvalidNestedHeaderValue($value) {
+               $this->setExpectedException('InvalidArgumentException', 'Invalid header value');
+               $message = $this->message->withHeader('X-Foo', [$value]);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidHeaderValuesDataProvider() {
+               return [
+                       'null'   => [NULL],
+                       'true'   => [TRUE],
+                       'false'  => [FALSE],
+                       'int'    => [1],
+                       'float'  => [1.1],
+                       'object' => [(object) ['foo' => 'bar']],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidHeaderValuesDataProvider
+        */
+       public function withHeaderRaisesExceptionForInvalidValueType($value) {
+               $this->setExpectedException('InvalidArgumentException', 'Invalid header value');
+               $message = $this->message->withHeader('X-Foo', $value);
+       }
+
+       /**
+        * @dataProvider invalidHeaderValuesDataProvider
+        */
+       public function withAddedHeaderRaisesExceptionForNonStringNonArrayValue($value) {
+               $this->setExpectedException('InvalidArgumentException', 'must be a string');
+               $message = $this->message->withAddedHeader('X-Foo', $value);
+       }
+
+       /**
+        * @test
+        */
+       public function withoutHeaderDoesNothingIfHeaderDoesNotExist() {
+               $this->assertFalse($this->message->hasHeader('X-Foo'));
+               $message = $this->message->withoutHeader('X-Foo');
+               $this->assertNotSame($this->message, $message);
+               $this->assertFalse($message->hasHeader('X-Foo'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderReturnsAnEmptyArrayWhenHeaderDoesNotExist() {
+               $this->assertSame([], $this->message->getHeader('X-Foo-Bar'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderLineReturnsEmptyStringWhenHeaderDoesNotExist() {
+               $this->assertSame('', $this->message->getHeaderLine('X-Foo-Bar'));
+       }
+
+       /**
+        * @return array
+        */
+       public function headersWithInjectionVectorsDataProvider() {
+               return [
+                       'name-with-cr'           => ["X-Foo\r-Bar", 'value'],
+                       'name-with-lf'           => ["X-Foo\n-Bar", 'value'],
+                       'name-with-crlf'         => ["X-Foo\r\n-Bar", 'value'],
+                       'name-with-2crlf'        => ["X-Foo\r\n\r\n-Bar", 'value'],
+                       'value-with-cr'          => ['X-Foo-Bar', "value\rinjection"],
+                       'value-with-lf'          => ['X-Foo-Bar', "value\ninjection"],
+                       'value-with-crlf'        => ['X-Foo-Bar', "value\r\ninjection"],
+                       'value-with-2crlf'       => ['X-Foo-Bar', "value\r\n\r\ninjection"],
+                       'array-value-with-cr'    => ['X-Foo-Bar', ["value\rinjection"]],
+                       'array-value-with-lf'    => ['X-Foo-Bar', ["value\ninjection"]],
+                       'array-value-with-crlf'  => ['X-Foo-Bar', ["value\r\ninjection"]],
+                       'array-value-with-2crlf' => ['X-Foo-Bar', ["value\r\n\r\ninjection"]],
+               ];
+       }
+
+       /**
+        * @dataProvider headersWithInjectionVectorsDataProvider
+        * @test
+        */
+       public function doesNotAllowCRLFInjectionWhenCallingWithHeader($name, $value) {
+               $this->setExpectedException('InvalidArgumentException');
+               $this->message->withHeader($name, $value);
+       }
+
+       /**
+        * @dataProvider headersWithInjectionVectorsDataProvider
+        * @test
+        */
+       public function doesNotAllowCRLFInjectionWhenCallingWithAddedHeader($name, $value) {
+               $this->setExpectedException('InvalidArgumentException');
+               $this->message->withAddedHeader($name, $value);
+       }
+
+       /**
+        * @test
+        */
+       public function testWithHeaderAllowsHeaderContinuations() {
+               $message = $this->message->withHeader('X-Foo-Bar', "value,\r\n second value");
+               $this->assertEquals("value,\r\n second value", $message->getHeaderLine('X-Foo-Bar'));
+       }
+
+       /**
+        * @test
+        */
+       public function testWithAddedHeaderAllowsHeaderContinuations() {
+               $message = $this->message->withAddedHeader('X-Foo-Bar', "value,\r\n second value");
+               $this->assertEquals("value,\r\n second value", $message->getHeaderLine('X-Foo-Bar'));
+       }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/RequestTest.php b/typo3/sysext/core/Tests/Unit/Http/RequestTest.php
new file mode 100644 (file)
index 0000000..7d9fd08
--- /dev/null
@@ -0,0 +1,453 @@
+<?php
+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 TYPO3\CMS\Core\Http\Uri;
+use TYPO3\CMS\Core\Http\Request;
+use TYPO3\CMS\Core\Http\Stream;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\Request
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class RequestTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var Request
+        */
+       protected $request;
+
+       public function setUp() {
+               $this->request = new Request();
+       }
+
+       /**
+        * @test
+        */
+       public function getMethodIsGetByDefault() {
+               $this->assertEquals('GET', $this->request->getMethod());
+       }
+
+       /**
+        * @test
+        */
+       public function getMethodMutatorReturnsCloneWithChangedMethod() {
+               $request = $this->request->withMethod('GET');
+               $this->assertNotSame($this->request, $request);
+               $this->assertEquals('GET', $request->getMethod());
+       }
+
+       /**
+        * @test
+        */
+       public function getUriIsNullByDefault() {
+               $this->assertNull($this->request->getUri());
+       }
+
+       /**
+        * @test
+        */
+       public function constructorRaisesExceptionForInvalidStream() {
+               $this->setExpectedException('InvalidArgumentException');
+               new Request(['TOTALLY INVALID']);
+       }
+
+       /**
+        * @test
+        */
+       public function withUriReturnsNewInstanceWithNewUri() {
+               $request = $this->request->withUri(new Uri('https://example.com:10082/foo/bar?baz=bat'));
+               $this->assertNotSame($this->request, $request);
+               $request2 = $request->withUri(new Uri('/baz/bat?foo=bar'));
+               $this->assertNotSame($this->request, $request2);
+               $this->assertNotSame($request, $request2);
+               $this->assertEquals('/baz/bat?foo=bar', (string) $request2->getUri());
+       }
+
+       /**
+        * @test
+        */
+       public function constructorCanAcceptAllMessageParts() {
+               $uri = new Uri('http://example.com/');
+               $body = new Stream('php://memory');
+               $headers = [
+                       'x-foo' => ['bar'],
+               ];
+               $request = new Request(
+                       $uri,
+                       'POST',
+                       $body,
+                       $headers
+               );
+
+               $this->assertSame($uri, $request->getUri());
+               $this->assertEquals('POST', $request->getMethod());
+               $this->assertSame($body, $request->getBody());
+               $testHeaders = $request->getHeaders();
+               foreach ($headers as $key => $value) {
+                       $this->assertArrayHasKey($key, $testHeaders);
+                       $this->assertEquals($value, $testHeaders[$key]);
+               }
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidRequestUriDataProvider() {
+               return [
+                       'true'     => [TRUE],
+                       'false'    => [FALSE],
+                       'int'      => [1],
+                       'float'    => [1.1],
+                       'array'    => [['http://example.com']],
+                       'stdClass' => [(object) ['href' => 'http://example.com']],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidRequestUriDataProvider
+        * @test
+        */
+       public function constructorRaisesExceptionForInvalidUri($uri) {
+               $this->setExpectedException('InvalidArgumentException', 'Invalid URI');
+               new Request($uri);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidRequestMethodDataProvider() {
+               return [
+                       'true'       => [TRUE],
+                       'false'      => [FALSE],
+                       'int'        => [1],
+                       'float'      => [1.1],
+                       'bad-string' => ['BOGUS-METHOD'],
+                       'array'      => [['POST']],
+                       'stdClass'   => [(object) ['method' => 'POST']],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidRequestMethodDataProvider
+        * @test
+        */
+       public function constructorRaisesExceptionForInvalidMethod($method) {
+               $this->setExpectedException('InvalidArgumentException', 'Unsupported HTTP method');
+               new Request(NULL, $method);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidRequestBodyDataProvider() {
+               return [
+                       'true'     => [TRUE],
+                       'false'    => [FALSE],
+                       'int'      => [1],
+                       'float'    => [1.1],
+                       'array'    => [['BODY']],
+                       'stdClass' => [(object) ['body' => 'BODY']],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidRequestBodyDataProvider
+        * @test
+        */
+       public function constructorRaisesExceptionForInvalidBody($body) {
+               $this->setExpectedException('InvalidArgumentException', 'stream');
+               new Request(NULL, NULL, $body);
+       }
+
+       /**
+        * @test
+        */
+       public function constructorIgnoresInvalidHeaders() {
+               $headers = [
+                       ['INVALID'],
+                       'x-invalid-null'   => NULL,
+                       'x-invalid-true'   => TRUE,
+                       'x-invalid-false'  => FALSE,
+                       'x-invalid-int'    => 1,
+                       'x-invalid-object' => (object) ['INVALID'],
+                       'x-valid-string'   => 'VALID',
+                       'x-valid-array'    => ['VALID'],
+               ];
+               $expected = [
+                       'x-valid-string' => ['VALID'],
+                       'x-valid-array'  => ['VALID'],
+               ];
+               $request = new Request(NULL, NULL, 'php://memory', $headers);
+               $this->assertEquals($expected, $request->getHeaders());
+       }
+
+       /**
+        * @test
+        */
+       public function getRequestTargetIsSlashWhenNoUriPresent() {
+               $request = new Request();
+               $this->assertEquals('/', $request->getRequestTarget());
+       }
+
+       /**
+        * @test
+        */
+       public function getRequestTargetIsSlashWhenUriHasNoPathOrQuery() {
+               $request = (new Request())
+                       ->withUri(new Uri('http://example.com'));
+               $this->assertEquals('/', $request->getRequestTarget());
+       }
+
+       /**
+        * @return array
+        */
+       public function requestsWithUriDataProvider() {
+               return [
+                       'absolute-uri'            => [
+                               (new Request())
+                                       ->withUri(new Uri('https://api.example.com/user'))
+                                       ->withMethod('POST'),
+                               '/user'
+                       ],
+                       'absolute-uri-with-query' => [
+                               (new Request())
+                                       ->withUri(new Uri('https://api.example.com/user?foo=bar'))
+                                       ->withMethod('POST'),
+                               '/user?foo=bar'
+                       ],
+                       'relative-uri'            => [
+                               (new Request())
+                                       ->withUri(new Uri('/user'))
+                                       ->withMethod('GET'),
+                               '/user'
+                       ],
+                       'relative-uri-with-query' => [
+                               (new Request())
+                                       ->withUri(new Uri('/user?foo=bar'))
+                                       ->withMethod('GET'),
+                               '/user?foo=bar'
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider requestsWithUriDataProvider
+        * @test
+        */
+       public function getRequestTargetWhenUriIsPresent($request, $expected) {
+               $this->assertEquals($expected, $request->getRequestTarget());
+       }
+
+       /**
+        * @return array
+        */
+       public function validRequestTargetsDataProvider() {
+               return [
+                       'asterisk-form'         => ['*'],
+                       'authority-form'        => ['api.example.com'],
+                       'absolute-form'         => ['https://api.example.com/users'],
+                       'absolute-form-query'   => ['https://api.example.com/users?foo=bar'],
+                       'origin-form-path-only' => ['/users'],
+                       'origin-form'           => ['/users?id=foo'],
+               ];
+       }
+
+       /**
+        * @dataProvider validRequestTargetsDataProvider
+        * @test
+        */
+       public function getRequestTargetCanProvideARequestTarget($requestTarget) {
+               $request = (new Request())->withRequestTarget($requestTarget);
+               $this->assertEquals($requestTarget, $request->getRequestTarget());
+       }
+
+       /**
+        * @test
+        */
+       public function withRequestTargetCannotContainWhitespace() {
+               $request = new Request();
+               $this->setExpectedException('InvalidArgumentException', 'Invalid request target');
+               $request->withRequestTarget('foo bar baz');
+       }
+
+       /**
+        * @test
+        */
+       public function getRequestTargetDoesNotCacheBetweenInstances() {
+               $request = (new Request())->withUri(new Uri('https://example.com/foo/bar'));
+               $original = $request->getRequestTarget();
+               $newRequest = $request->withUri(new Uri('http://mwop.net/bar/baz'));
+               $this->assertNotEquals($original, $newRequest->getRequestTarget());
+       }
+
+       /**
+        * @test
+        */
+       public function getRequestTargetIsResetWithNewUri() {
+               $request = (new Request())->withUri(new Uri('https://example.com/foo/bar'));
+               $original = $request->getRequestTarget();
+               $newRequest = $request->withUri(new Uri('http://mwop.net/bar/baz'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeadersContainsHostHeaderIfUriWithHostIsPresent() {
+               $request = new Request('http://example.com');
+               $headers = $request->getHeaders();
+               $this->assertArrayHasKey('host', $headers);
+               $this->assertContains('example.com', $headers['host']);
+       }
+
+       /**
+        * @test
+        */
+       public function getHeadersContainsNoHostHeaderIfNoUriPresent() {
+               $request = new Request();
+               $headers = $request->getHeaders();
+               $this->assertArrayNotHasKey('host', $headers);
+       }
+
+       /**
+        * @test
+        */
+       public function getHeadersContainsNoHostHeaderIfUriDoesNotContainHost() {
+               $request = new Request(new Uri());
+               $headers = $request->getHeaders();
+               $this->assertArrayNotHasKey('host', $headers);
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderWithHostReturnsUriHostWhenPresent() {
+               $request = new Request('http://example.com');
+               $header = $request->getHeader('host');
+               $this->assertEquals(array('example.com'), $header);
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderWithHostReturnsEmptyArrayIfNoUriPresent() {
+               $request = new Request();
+               $this->assertSame([], $request->getHeader('host'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderWithHostReturnsEmptyArrayIfUriDoesNotContainHost() {
+               $request = new Request(new Uri());
+               $this->assertSame([], $request->getHeader('host'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderLineWithHostReturnsUriHostWhenPresent() {
+               $request = new Request('http://example.com');
+               $header = $request->getHeaderLine('host');
+               $this->assertContains('example.com', $header);
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderLineWithHostReturnsEmptyStringIfNoUriPresent() {
+               $request = new Request();
+               $this->assertSame('', $request->getHeaderLine('host'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderLineWithHostReturnsEmptyStringIfUriDoesNotContainHost() {
+               $request = new Request(new Uri());
+               $this->assertSame('', $request->getHeaderLine('host'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderLineWithHostTakesPrecedenceOverModifiedUri() {
+               $request = (new Request())
+                       ->withAddedHeader('Host', 'example.com');
+
+               $uri = (new Uri())->withHost('www.example.com');
+               $new = $request->withUri($uri, TRUE);
+
+               $this->assertEquals('example.com', $new->getHeaderLine('Host'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderLineWithHostTakesPrecedenceOverEmptyUri() {
+               $request = (new Request())
+                       ->withAddedHeader('Host', 'example.com');
+
+               $uri = new Uri();
+               $new = $request->withUri($uri);
+
+               $this->assertEquals('example.com', $new->getHeaderLine('Host'));
+       }
+
+       /**
+        * @test
+        */
+       public function getHeaderLineWithHostDoesNotTakePrecedenceOverHostWithPortFromUri() {
+               $request = (new Request())
+                       ->withAddedHeader('Host', 'example.com');
+
+               $uri = (new Uri())
+                       ->withHost('www.example.com')
+                       ->withPort(10081);
+               $new = $request->withUri($uri);
+
+               $this->assertEquals('www.example.com:10081', $new->getHeaderLine('Host'));
+       }
+
+       /**
+        * @return array
+        */
+       public function headersWithInjectionVectorsDataProvider() {
+               return [
+                       'name-with-cr'           => ["X-Foo\r-Bar", 'value'],
+                       'name-with-lf'           => ["X-Foo\n-Bar", 'value'],
+                       'name-with-crlf'         => ["X-Foo\r\n-Bar", 'value'],
+                       'name-with-2crlf'        => ["X-Foo\r\n\r\n-Bar", 'value'],
+                       'value-with-cr'          => ['X-Foo-Bar', "value\rinjection"],
+                       'value-with-lf'          => ['X-Foo-Bar', "value\ninjection"],
+                       'value-with-crlf'        => ['X-Foo-Bar', "value\r\ninjection"],
+                       'value-with-2crlf'       => ['X-Foo-Bar', "value\r\n\r\ninjection"],
+                       'array-value-with-cr'    => ['X-Foo-Bar', ["value\rinjection"]],
+                       'array-value-with-lf'    => ['X-Foo-Bar', ["value\ninjection"]],
+                       'array-value-with-crlf'  => ['X-Foo-Bar', ["value\r\ninjection"]],
+                       'array-value-with-2crlf' => ['X-Foo-Bar', ["value\r\n\r\ninjection"]],
+               ];
+       }
+
+       /**
+        * @test
+        * @dataProvider headersWithInjectionVectorsDataProvider
+        */
+       public function constructorRaisesExceptionForHeadersWithCRLFVectors($name, $value) {
+               $this->setExpectedException('InvalidArgumentException');
+               $request = new Request(NULL, NULL, 'php://memory', [$name => $value]);
+       }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/ResponseTest.php b/typo3/sysext/core/Tests/Unit/Http/ResponseTest.php
new file mode 100644 (file)
index 0000000..4e4d153
--- /dev/null
@@ -0,0 +1,214 @@
+<?php
+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 TYPO3\CMS\Core\Http\Response;
+use TYPO3\CMS\Core\Http\Stream;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\Response
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class ResponseTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var Response
+        */
+       protected $response;
+
+       public function setUp() {
+               $this->response = new Response();
+       }
+
+       /**
+        * @test
+        */
+       public function testStatusCodeIs200ByDefault() {
+               $this->assertEquals(200, $this->response->getStatusCode());
+       }
+
+       /**
+        * @test
+        */
+       public function testStatusCodeMutatorReturnsCloneWithChanges() {
+               $response = $this->response->withStatus(400);
+               $this->assertNotSame($this->response, $response);
+               $this->assertEquals(400, $response->getStatusCode());
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidStatusCodesDataProvider() {
+               return [
+                       'too-low'  => [99],
+                       'too-high' => [600],
+                       'null'     => [NULL],
+                       'bool'     => [TRUE],
+                       'string'   => ['foo'],
+                       'array'    => [[200]],
+                       'object'   => [(object) [200]],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidStatusCodesDataProvider
+        * @test
+        */
+       public function testCannotSetInvalidStatusCode($code) {
+               $this->setExpectedException('InvalidArgumentException');
+               $response = $this->response->withStatus($code);
+       }
+
+       /**
+        * @test
+        */
+       public function testReasonPhraseDefaultsToStandards() {
+               $response = $this->response->withStatus(422);
+               $this->assertEquals('Unprocessable Entity', $response->getReasonPhrase());
+       }
+
+       /**
+        * @test
+        */
+       public function testCanSetCustomReasonPhrase() {
+               $response = $this->response->withStatus(422, 'Foo Bar!');
+               $this->assertEquals('Foo Bar!', $response->getReasonPhrase());
+       }
+
+       /**
+        * @test
+        */
+       public function testConstructorRaisesExceptionForInvalidStream() {
+               $this->setExpectedException('InvalidArgumentException');
+               new Response(['TOTALLY INVALID']);
+       }
+
+       /**
+        * @test
+        */
+       public function testConstructorCanAcceptAllMessageParts() {
+               $body = new Stream('php://memory');
+               $status = 302;
+               $headers = [
+                       'location' => ['http://example.com/'],
+               ];
+
+               $response = new Response($body, $status, $headers);
+               $this->assertSame($body, $response->getBody());
+               $this->assertEquals(302, $response->getStatusCode());
+               $this->assertEquals($headers, $response->getHeaders());
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidStatusDataProvider() {
+               return [
+                       'true'       => [TRUE],
+                       'false'      => [FALSE],
+                       'float'      => [100.1],
+                       'bad-string' => ['Two hundred'],
+                       'array'      => [[200]],
+                       'object'     => [(object) ['statusCode' => 200]],
+                       'too-small'  => [1],
+                       'too-big'    => [600],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidStatusDataProvider
+        * @test
+        */
+       public function testConstructorRaisesExceptionForInvalidStatus($code) {
+               $this->setExpectedException('InvalidArgumentException', 'The given status code is not a valid HTTP status code.');
+               new Response('php://memory', $code);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidResponseBodyDataProvider() {
+               return [
+                       'true'     => [TRUE],
+                       'false'    => [FALSE],
+                       'int'      => [1],
+                       'float'    => [1.1],
+                       'array'    => [['BODY']],
+                       'stdClass' => [(object) ['body' => 'BODY']],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidResponseBodyDataProvider
+        * @test
+        */
+       public function testConstructorRaisesExceptionForInvalidBody($body) {
+               $this->setExpectedException('InvalidArgumentException', 'stream');
+               new Response($body);
+       }
+
+       /**
+        * @test
+        */
+       public function constructorIgonoresInvalidHeaders() {
+               $headers = [
+                       ['INVALID'],
+                       'x-invalid-null'   => NULL,
+                       'x-invalid-true'   => TRUE,
+                       'x-invalid-false'  => FALSE,
+                       'x-invalid-int'    => 1,
+                       'x-invalid-object' => (object) ['INVALID'],
+                       'x-valid-string'   => 'VALID',
+                       'x-valid-array'    => ['VALID'],
+               ];
+               $expected = [
+                       'x-valid-string' => ['VALID'],
+                       'x-valid-array'  => ['VALID'],
+               ];
+               $response = new Response('php://memory', 200, $headers);
+               $this->assertEquals($expected, $response->getHeaders());
+       }
+
+       /**
+        * @return array
+        */
+       public function headersWithInjectionVectorsDataProvider() {
+               return [
+                       'name-with-cr'           => ["X-Foo\r-Bar", 'value'],
+                       'name-with-lf'           => ["X-Foo\n-Bar", 'value'],
+                       'name-with-crlf'         => ["X-Foo\r\n-Bar", 'value'],
+                       'name-with-2crlf'        => ["X-Foo\r\n\r\n-Bar", 'value'],
+                       'value-with-cr'          => ['X-Foo-Bar', "value\rinjection"],
+                       'value-with-lf'          => ['X-Foo-Bar', "value\ninjection"],
+                       'value-with-crlf'        => ['X-Foo-Bar', "value\r\ninjection"],
+                       'value-with-2crlf'       => ['X-Foo-Bar', "value\r\n\r\ninjection"],
+                       'array-value-with-cr'    => ['X-Foo-Bar', ["value\rinjection"]],
+                       'array-value-with-lf'    => ['X-Foo-Bar', ["value\ninjection"]],
+                       'array-value-with-crlf'  => ['X-Foo-Bar', ["value\r\ninjection"]],
+                       'array-value-with-2crlf' => ['X-Foo-Bar', ["value\r\n\r\ninjection"]],
+               ];
+       }
+
+       /**
+        * @test
+        * @dataProvider headersWithInjectionVectorsDataProvider
+        */
+       public function cnstructorRaisesExceptionForHeadersWithCRLFVectors($name, $value) {
+               $this->setExpectedException('InvalidArgumentException');
+               $request = new Response('php://memory', 200, [$name => $value]);
+       }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/ServerRequestFactoryTest.php b/typo3/sysext/core/Tests/Unit/Http/ServerRequestFactoryTest.php
new file mode 100644 (file)
index 0000000..88a5213
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+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 TYPO3\CMS\Core\Http\ServerRequestFactory;
+use TYPO3\CMS\Core\Http\UploadedFile;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\ServerRequestFactory
+ */
+class ServerRequestFactoryTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @test
+        */
+       public function uploadedFilesAreNormalizedFromFilesSuperGlobal() {
+               $_SERVER['HTTP_HOST'] = 'localhost';
+               $_SERVER['REQUEST_URI'] = '/index.php';
+               $_FILES = array(
+                       'tx_uploadexample_piexample' => array(
+                               'name' => array(
+                                       'newExample' => array(
+                                               'image' => 'o51pb.jpg',
+                                               'imageCollection' => array(
+                                                       0 => 'composer.json',
+                                               ),
+                                       ),
+                                       ),
+                                       'type' => array(
+                                               'newExample' => array(
+                                                       'image' => 'image/jpeg',
+                                                       'imageCollection' => array(
+                                                               0 => 'application/json'
+                                                       )
+                                               )
+                                       ),
+                                       'tmp_name' => array(
+                                               'newExample' => array(
+                                                       'image' => '/Applications/MAMP/tmp/php/phphXdbcd',
+                                                       'imageCollection' => array(
+                                                               0 => '/Applications/MAMP/tmp/php/phpgrZ4bb'
+                                                       )
+                                               )
+                                       ),
+                                       'error' => array(
+                                               'newExample' => array(
+                                                               'image' => 0,
+                                                               'imageCollection' => array(
+                                                                       0 => 0
+                                                               )
+                                               )
+                                       ),
+                                       'size' => array(
+                                               'newExample' => array(
+                                                       'image' => 59065,
+                                                       'imageCollection' => array(
+                                                               0 => 683
+                                                       )
+                                               )
+                                       )
+                       )
+               );
+
+               $uploadedFiles = ServerRequestFactory::fromGlobals()->getUploadedFiles();
+
+               $this->assertNotEmpty($uploadedFiles['tx_uploadexample_piexample']['newExample']['image']);
+               $this->assertTrue($uploadedFiles['tx_uploadexample_piexample']['newExample']['image'] instanceof UploadedFile);
+               $this->assertNotEmpty($uploadedFiles['tx_uploadexample_piexample']['newExample']['imageCollection'][0]);
+               $this->assertTrue($uploadedFiles['tx_uploadexample_piexample']['newExample']['imageCollection'][0] instanceof UploadedFile);
+       }
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/ServerRequestTest.php b/typo3/sysext/core/Tests/Unit/Http/ServerRequestTest.php
new file mode 100644 (file)
index 0000000..2d72981
--- /dev/null
@@ -0,0 +1,175 @@
+<?php
+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 TYPO3\CMS\Core\Http\Uri;
+use TYPO3\CMS\Core\Http\ServerRequest;
+use TYPO3\CMS\Core\Http\UploadedFile;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\ServerRequest
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class ServerRequestTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var ServerRequest
+        */
+       protected $request;
+
+       public function setUp() {
+               $this->request = new ServerRequest();
+       }
+
+       /**
+        * @test
+        */
+       public function getServerParamsAreEmptyByDefault() {
+               $this->assertEmpty($this->request->getServerParams());
+       }
+
+       /**
+        * @test
+        */
+       public function getQueryParamsAreEmptyByDefault() {
+               $this->assertEmpty($this->request->getQueryParams());
+       }
+
+       /**
+        * @test
+        */
+       public function withQueryParamsMutatorReturnsCloneWithChanges() {
+               $value = ['foo' => 'bar'];
+               $request = $this->request->withQueryParams($value);
+               $this->assertNotSame($this->request, $request);
+               $this->assertEquals($value, $request->getQueryParams());
+       }
+
+       /**
+        * @test
+        */
+       public function getCookieParamsAreEmptyByDefault() {
+               $this->assertEmpty($this->request->getCookieParams());
+       }
+
+       /**
+        * @test
+        */
+       public function withCookieParamsMutatorReturnsCloneWithChanges() {
+               $value = ['foo' => 'bar'];
+               $request = $this->request->withCookieParams($value);
+               $this->assertNotSame($this->request, $request);
+               $this->assertEquals($value, $request->getCookieParams());
+       }
+
+       /**
+        * @test
+        */
+       public function getUploadedFilesAreEmptyByDefault() {
+               $this->assertEmpty($this->request->getUploadedFiles());
+       }
+
+       /**
+        * @test
+        */
+       public function getParsedBodyIsEmptyByDefault() {
+               $this->assertEmpty($this->request->getParsedBody());
+       }
+
+       /**
+        * @test
+        */
+       public function withParsedBodyMutatorReturnsCloneWithChanges() {
+               $value = ['foo' => 'bar'];
+               $request = $this->request->withParsedBody($value);
+               $this->assertNotSame($this->request, $request);
+               $this->assertEquals($value, $request->getParsedBody());
+       }
+
+       /**
+        * @test
+        */
+       public function getAttributesAreEmptyByDefault() {
+               $this->assertEmpty($this->request->getAttributes());
+       }
+
+       /**
+        * @depends testAttributesAreEmptyByDefault
+        * @test
+        */
+       public function withAttributeMutatorReturnsCloneWithChanges() {
+               $request = $this->request->withAttribute('foo', 'bar');
+               $this->assertNotSame($this->request, $request);
+               $this->assertEquals('bar', $request->getAttribute('foo'));
+
+               return $request;
+       }
+
+       /**
+        * @depends testAttributeMutatorReturnsCloneWithChanges
+        * @test
+        */
+       public function withoutAttributeReturnsCloneWithoutAttribute($request) {
+               $new = $request->withoutAttribute('foo');
+               $this->assertNotSame($request, $new);
+               $this->assertNull($new->getAttribute('foo', NULL));
+       }
+
+       /**
+        * @test
+        */
+       public function constructorUsesProvidedArguments() {
+               $server = [
+                       'foo' => 'bar',
+                       'baz' => 'bat',
+               ];
+
+               $server['server'] = TRUE;
+
+               $files = [
+                       'files' => new UploadedFile('php://temp', 0, 0),
+               ];
+
+               $uri = new Uri('http://example.com');
+               $method = 'POST';
+               $headers = [
+                       'host' => ['example.com'],
+               ];
+
+               $request = new ServerRequest(
+                       $uri,
+                       $method,
+                       'php://memory',
+                       $headers,
+                       $server,
+                       $files
+               );
+
+               $this->assertEquals($server, $request->getServerParams());
+               $this->assertEquals($files, $request->getUploadedFiles());
+
+               $this->assertSame($uri, $request->getUri());
+               $this->assertEquals($method, $request->getMethod());
+               $this->assertEquals($headers, $request->getHeaders());
+
+               $body = $request->getBody();
+               $r = new \ReflectionProperty($body, 'stream');
+               $r->setAccessible(TRUE);
+               $stream = $r->getValue($body);
+               $this->assertEquals('php://memory', $stream);
+       }
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/StreamTest.php b/typo3/sysext/core/Tests/Unit/Http/StreamTest.php
new file mode 100644 (file)
index 0000000..2ffd0a7
--- /dev/null
@@ -0,0 +1,536 @@
+<?php
+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 TYPO3\CMS\Core\Http\Stream;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\StreamTest
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class StreamTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var Stream
+        */
+       protected $stream;
+
+       public function setUp() {
+               $this->stream = new Stream('php://memory', 'wb+');
+       }
+
+       /**
+        * @test
+        */
+       public function canInstantiateWithStreamIdentifier() {
+               $this->assertInstanceOf(Stream::class, $this->stream);
+       }
+
+       /**
+        * @test
+        */
+       public function canInstantiteWithStreamResource() {
+               $resource = fopen('php://memory', 'wb+');
+               $stream = new Stream($resource);
+               $this->assertInstanceOf(Stream::class, $stream);
+       }
+
+       /**
+        * @test
+        */
+       public function isReadableReturnsFalseIfStreamIsNotReadable() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $stream = new Stream($fileName, 'w');
+               $this->assertFalse($stream->isReadable());
+       }
+
+       /**
+        * @test
+        */
+       public function isWritableReturnsFalseIfStreamIsNotWritable() {
+               $stream = new Stream('php://memory', 'r');
+               $this->assertFalse($stream->isWritable());
+       }
+
+       /**
+        * @test
+        */
+       public function toStringRetrievesFullContentsOfStream() {
+               $message = 'foo bar';
+               $this->stream->write($message);
+               $this->assertEquals($message, (string) $this->stream);
+       }
+
+       /**
+        * @test
+        */
+       public function detachReturnsResource() {
+               $resource = fopen('php://memory', 'wb+');
+               $stream = new Stream($resource);
+               $this->assertSame($resource, $stream->detach());
+       }
+
+       /**
+        * @test
+        */
+       public function constructorRaisesExceptionWhenPassingInvalidStreamResource() {
+               $this->setExpectedException('InvalidArgumentException');
+               $stream = new Stream(['  THIS WILL NOT WORK  ']);
+       }
+
+       /**
+        * @test
+        */
+       public function toStringSerializationReturnsEmptyStringWhenStreamIsNotReadable() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $stream = new Stream($fileName, 'w');
+
+               $this->assertEquals('', $stream->__toString());
+       }
+
+       /**
+        * @test
+        */
+       public function closeClosesResource() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $stream->close();
+               $this->assertFalse(is_resource($resource));
+       }
+
+       /**
+        * @test
+        */
+       public function closeUnsetsResource() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $stream->close();
+
+               $this->assertNull($stream->detach());
+       }
+
+       /**
+        * @test
+        */
+       public function closeDoesNothingAfterDetach() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $detached = $stream->detach();
+
+               $stream->close();
+               $this->assertTrue(is_resource($detached));
+               $this->assertSame($resource, $detached);
+       }
+
+       /**
+        * @test
+        */
+       public function getSizeReportsNullWhenNoResourcePresent() {
+               $this->stream->detach();
+               $this->assertNull($this->stream->getSize());
+       }
+
+       /**
+        * @test
+        */
+       public function tellReportsCurrentPositionInResource() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+
+               fseek($resource, 2);
+
+               $this->assertEquals(2, $stream->tell());
+       }
+
+       /**
+        * @test
+        */
+       public function tellRaisesExceptionIfResourceIsDetached() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+
+               fseek($resource, 2);
+               $stream->detach();
+               $this->setExpectedException('RuntimeException', 'No resource');
+               $stream->tell();
+       }
+
+       /**
+        * @test
+        */
+       public function eofReportsFalseWhenNotAtEndOfStream() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+
+               fseek($resource, 2);
+               $this->assertFalse($stream->eof());
+       }
+
+       /**
+        * @test
+        */
+       public function eofReportsTrueWhenAtEndOfStream() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+
+               while (!feof($resource)) {
+                       fread($resource, 4096);
+               }
+               $this->assertTrue($stream->eof());
+       }
+
+       /**
+        * @test
+        */
+       public function eofReportsTrueWhenStreamIsDetached() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+
+               fseek($resource, 2);
+               $stream->detach();
+               $this->assertTrue($stream->eof());
+       }
+
+       /**
+        * @test
+        */
+       public function isSeekableReturnsTrueForReadableStreams() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $this->assertTrue($stream->isSeekable());
+       }
+
+       /**
+        * @test
+        */
+       public function isSeekableReturnsFalseForDetachedStreams() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $stream->detach();
+               $this->assertFalse($stream->isSeekable());
+       }
+
+       /**
+        * @test
+        */
+       public function seekAdvancesToGivenOffsetOfStream() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $stream->seek(2);
+               $this->assertEquals(2, $stream->tell());
+       }
+
+       /**
+        * @test
+        */
+       public function rewindResetsToStartOfStream() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $stream->seek(2);
+               $stream->rewind();
+               $this->assertEquals(0, $stream->tell());
+       }
+
+       /**
+        * @test
+        */
+       public function seekRaisesExceptionWhenStreamIsDetached() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $stream->detach();
+               $this->setExpectedException('RuntimeException', 'No resource');
+               $stream->seek(2);
+       }
+
+       /**
+        * @test
+        */
+       public function isWritableReturnsFalseWhenStreamIsDetached() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $stream->detach();
+               $this->assertFalse($stream->isWritable());
+       }
+
+       /**
+        * @test
+        */
+       public function writeRaisesExceptionWhenStreamIsDetached() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $stream->detach();
+               $this->setExpectedException('RuntimeException', 'No resource');
+               $stream->write('bar');
+       }
+
+       /**
+        * @test
+        */
+       public function isReadableReturnsFalseWhenStreamIsDetached() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'wb+');
+               $stream = new Stream($resource);
+               $stream->detach();
+               $this->assertFalse($stream->isReadable());
+       }
+
+       /**
+        * @test
+        */
+       public function readRaisesExceptionWhenStreamIsDetached() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'r');
+               $stream = new Stream($resource);
+               $stream->detach();
+               $this->setExpectedException('RuntimeException', 'No resource');
+               $stream->read(4096);
+       }
+
+       /**
+        * @test
+        */
+       public function readReturnsEmptyStringWhenAtEndOfFile() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'r');
+               $stream = new Stream($resource);
+               while (!feof($resource)) {
+                       fread($resource, 4096);
+               }
+               $this->assertEquals('', $stream->read(4096));
+       }
+
+       /**
+        * @test
+        */
+       public function getContentsReturnsEmptyStringIfStreamIsNotReadable() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               $this->testFilesToDelete[] = $fileName;
+               file_put_contents($fileName, 'FOO BAR');
+               $resource = fopen($fileName, 'w');
+               $stream = new Stream($resource);
+               $this->assertEquals('', $stream->getContents());
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidResourcesDataProvider() {
+               $fileName = tempnam(sys_get_temp_dir(), 'PHLY');
+               $this->testFilesToDelete[] = $fileName;
+
+               return [
+                       'null'                => [NULL],
+                       'false'               => [FALSE],
+                       'true'                => [TRUE],
+                       'int'                 => [1],
+                       'float'               => [1.1],
+                       'string-non-resource' => ['foo-bar-baz'],
+                       'array'               => [[fopen($fileName, 'r+')]],
+                       'object'              => [(object) ['resource' => fopen($fileName, 'r+')]],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidResourcesDataProvider
+        * @test
+        */
+       public function attachWithNonStringNonResourceRaisesException($resource) {
+               $this->setExpectedException('InvalidArgumentException', 'Invalid stream');
+               $this->stream->attach($resource);
+       }
+
+       /**
+        * @test
+        */
+       public function attachWithResourceAttachesResource() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $resource = fopen($fileName, 'r+');
+               $this->stream->attach($resource);
+
+               $r = new \ReflectionProperty($this->stream, 'resource');
+               $r->setAccessible(TRUE);
+               $test = $r->getValue($this->stream);
+               $this->assertSame($resource, $test);
+       }
+
+       /**
+        * @test
+        */
+       public function attachWithStringRepresentingResourceCreatesAndAttachesResource() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $this->stream->attach($fileName);
+
+               $resource = fopen($fileName, 'r+');
+               fwrite($resource, 'FooBar');
+
+               $this->stream->rewind();
+               $test = (string) $this->stream;
+               $this->assertEquals('FooBar', $test);
+       }
+
+       /**
+        * @test
+        */
+       public function getContentsShouldGetFullStreamContents() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $resource = fopen($fileName, 'r+');
+               $this->stream->attach($resource);
+
+               fwrite($resource, 'FooBar');
+
+               // rewind, because current pointer is at end of stream!
+               $this->stream->rewind();
+               $test = $this->stream->getContents();
+               $this->assertEquals('FooBar', $test);
+       }
+
+       /**
+        * @test
+        */
+       public function getContentsShouldReturnStreamContentsFromCurrentPointer() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $resource = fopen($fileName, 'r+');
+               $this->stream->attach($resource);
+
+               fwrite($resource, 'FooBar');
+
+               // seek to position 3
+               $this->stream->seek(3);
+               $test = $this->stream->getContents();
+               $this->assertEquals('Bar', $test);
+       }
+
+       /**
+        * @test
+        */
+       public function getMetadataReturnsAllMetadataWhenNoKeyPresent() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $resource = fopen($fileName, 'r+');
+               $this->stream->attach($resource);
+
+               $expected = stream_get_meta_data($resource);
+               $test = $this->stream->getMetadata();
+
+               $this->assertEquals($expected, $test);
+       }
+
+       /**
+        * @test
+        */
+       public function getMetadataReturnsDataForSpecifiedKey() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $resource = fopen($fileName, 'r+');
+               $this->stream->attach($resource);
+
+               $metadata = stream_get_meta_data($resource);
+               $expected = $metadata['uri'];
+
+               $test = $this->stream->getMetadata('uri');
+
+               $this->assertEquals($expected, $test);
+       }
+
+       /**
+        * @test
+        */
+       public function getMetadataReturnsNullIfNoDataExistsForKey() {
+               $fileName = PATH_site . 'typo3temp/' . $this->getUniqueId('test_');
+               touch($fileName);
+               $this->testFilesToDelete[] = $fileName;
+               $resource = fopen($fileName, 'r+');
+               $this->stream->attach($resource);
+
+               $this->assertNull($this->stream->getMetadata('TOTALLY_MADE_UP'));
+       }
+
+       /**
+        * @test
+        */
+       public function getSizeReturnsStreamSize() {
+               $resource = fopen(__FILE__, 'r');
+               $expected = fstat($resource);
+               $stream = new Stream($resource);
+               $this->assertEquals($expected['size'], $stream->getSize());
+       }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/UploadedFileTest.php b/typo3/sysext/core/Tests/Unit/Http/UploadedFileTest.php
new file mode 100644 (file)
index 0000000..a7106e0
--- /dev/null
@@ -0,0 +1,258 @@
+<?php
+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 TYPO3\CMS\Core\Http\Stream;
+use TYPO3\CMS\Core\Http\UploadedFile;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\UploadedFile
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class UploadedFileTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       protected $tmpFile;
+
+       public function setUp() {
+               $this->tmpfile = NULL;
+       }
+
+       public function tearDown() {
+               if (is_scalar($this->tmpFile) && file_exists($this->tmpFile)) {
+                       unlink($this->tmpFile);
+               }
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidStreamsDataProvider() {
+               return [
+                       'null'   => [NULL],
+                       'true'   => [TRUE],
+                       'false'  => [FALSE],
+                       'int'    => [1],
+                       'float'  => [1.1],
+                       /* Have not figured out a valid way to test an invalid path yet; null byte injection
+                        * appears to get caught by fopen()
+                       'invalid-path' => [ ('WIN' === strtoupper(substr(PHP_OS, 0, 3))) ? '[:]' : 'foo' . chr(0) ],
+                        */
+                       'array'  => [['filename']],
+                       'object' => [(object) ['filename']],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidStreamsDataProvider
+        * @test
+        */
+       public function constructorRaisesExceptionOnInvalidStreamOrFile($streamOrFile) {
+               $this->setExpectedException('InvalidArgumentException');
+               new UploadedFile($streamOrFile, 0, UPLOAD_ERR_OK);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidSizesDataProvider() {
+               return [
+                       'null'   => [NULL],
+                       'true'   => [TRUE],
+                       'false'  => [FALSE],
+                       'float'  => [1.1],
+                       'string' => ['1'],
+                       'array'  => [[1]],
+                       'object' => [(object) [1]],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidSizesDataProvider
+        * @test
+        */
+       public function constructorRaisesExceptionOnInvalidSize($size) {
+               $this->setExpectedException('InvalidArgumentException', 'size');
+               new UploadedFile(fopen('php://temp', 'wb+'), $size, UPLOAD_ERR_OK);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidErrorStatusesDataProvider() {
+               return [
+                       'null'     => [NULL],
+                       'true'     => [TRUE],
+                       'false'    => [FALSE],
+                       'float'    => [1.1],
+                       'string'   => ['1'],
+                       'array'    => [[1]],
+                       'object'   => [(object) [1]],
+                       'negative' => [-1],
+                       'too-big'  => [9],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidErrorStatusesDataProvider
+        * @test
+        */
+       public function constructorRaisesExceptionOnInvalidErrorStatus($status) {
+               $this->setExpectedException('InvalidArgumentException', 'status');
+               new UploadedFile(fopen('php://temp', 'wb+'), 0, $status);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidFilenamesAndMediaTypesDataProvider() {
+               return [
+                       'true'   => [TRUE],
+                       'false'  => [FALSE],
+                       'int'    => [1],
+                       'float'  => [1.1],
+                       'array'  => [['string']],
+                       'object' => [(object) ['string']],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidFilenamesAndMediaTypesDataProvider
+        * @test
+        */
+       public function constructorRaisesExceptionOnInvalidClientFilename($filename) {
+               $this->setExpectedException('InvalidArgumentException', 'filename');
+               new UploadedFile(fopen('php://temp', 'wb+'), 0, UPLOAD_ERR_OK, $filename);
+       }
+
+       /**
+        * @dataProvider invalidFilenamesAndMediaTypesDataProvider
+        * @test
+        */
+       public function constructorRaisesExceptionOnInvalidClientMediaType($mediaType) {
+               $this->setExpectedException('InvalidArgumentException', 'media type');
+               new UploadedFile(fopen('php://temp', 'wb+'), 0, UPLOAD_ERR_OK, 'foobar.baz', $mediaType);
+       }
+
+       /**
+        * @test
+        */
+       public function getStreamReturnsOriginalStreamObject() {
+               $stream = new Stream('php://temp');
+               $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+               $this->assertSame($stream, $upload->getStream());
+       }
+
+       /**
+        * @test
+        */
+       public function getStreamReturnsWrappedPhpStream() {
+               $stream = fopen('php://temp', 'wb+');
+               $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+               $uploadStream = $upload->getStream()->detach();
+               $this->assertSame($stream, $uploadStream);
+       }
+
+       /**
+        * @test
+        */
+       public function getStreamReturnsStreamForFile() {
+               $this->tmpFile = $stream = tempnam(sys_get_temp_dir(), 'phly');
+               $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+               $uploadStream = $upload->getStream();
+               $r = new \ReflectionProperty($uploadStream, 'stream');
+               $r->setAccessible(TRUE);
+               $this->assertSame($stream, $r->getValue($uploadStream));
+       }
+
+       /**
+        * @test
+        */
+       public function moveToMovesFileToDesignatedPath() {
+               $stream = new Stream('php://temp', 'wb+');
+               $stream->write('Foo bar!');
+               $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+
+               $this->tmpFile = $to = GeneralUtility::tempnam('psr7');
+               $upload->moveTo($to);
+               $this->assertTrue(file_exists($to));
+               $contents = file_get_contents($to);
+               $this->assertEquals($stream->__toString(), $contents);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidMovePathsDataProvider() {
+               return [
+                       'null'   => [NULL],
+                       'true'   => [TRUE],
+                       'false'  => [FALSE],
+                       'int'    => [1],
+                       'float'  => [1.1],
+                       'empty'  => [''],
+                       'array'  => [['filename']],
+                       'object' => [(object) ['filename']],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidMovePathsDataProvider
+        * @test
+        */
+       public function moveToRaisesExceptionForInvalidPath($path) {
+               $stream = new Stream('php://temp', 'wb+');
+               $stream->write('Foo bar!');
+               $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+
+               $this->tmpFile = $path;
+               $this->setExpectedException('InvalidArgumentException', 'path');
+               $upload->moveTo($path);
+       }
+
+       /**
+        * @test
+        */
+       public function moveToCannotBeCalledMoreThanOnce() {
+               $stream = new Stream('php://temp', 'wb+');
+               $stream->write('Foo bar!');
+               $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+
+               $this->tmpFile = $to = GeneralUtility::tempnam('psr7');
+               $upload->moveTo($to);
+               $this->assertTrue(file_exists($to));
+
+               $this->setExpectedException('RuntimeException', 'moved');
+               $upload->moveTo($to);
+       }
+
+       /**
+        * @test
+        */
+       public function getGetStreamRaisesExceptionAfterMove() {
+               $stream = new Stream('php://temp', 'wb+');
+               $stream->write('Foo bar!');
+               $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK);
+
+               $this->tmpFile = $to = GeneralUtility::tempnam('psr7');
+               $upload->moveTo($to);
+               $this->assertTrue(file_exists($to));
+
+               $this->setExpectedException('RuntimeException', 'moved');
+               $upload->getStream();
+       }
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Http/UriTest.php b/typo3/sysext/core/Tests/Unit/Http/UriTest.php
new file mode 100644 (file)
index 0000000..5024f4c
--- /dev/null
@@ -0,0 +1,486 @@
+<?php
+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 TYPO3\CMS\Core\Http\Uri;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Http\Uri
+ *
+ * Adapted from https://github.com/phly/http/
+ */
+class UriTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @test
+        */
+       public function constructorSetsAllProperties() {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $this->assertEquals('https', $uri->getScheme());
+               $this->assertEquals('user:pass', $uri->getUserInfo());
+               $this->assertEquals('local.example.com', $uri->getHost());
+               $this->assertEquals(3001, $uri->getPort());
+               $this->assertEquals('user:pass@local.example.com:3001', $uri->getAuthority());
+               $this->assertEquals('/foo', $uri->getPath());
+               $this->assertEquals('bar=baz', $uri->getQuery());
+               $this->assertEquals('quz', $uri->getFragment());
+       }
+
+       /**
+        * @test
+        */
+       public function canSerializeToString() {
+               $url = 'https://user:pass@local.example.com:3001/foo?bar=baz#quz';
+               $uri = new Uri($url);
+               $this->assertEquals($url, (string) $uri);
+       }
+
+       /**
+        * @test
+        */
+       public function withSchemeReturnsNewInstanceWithNewScheme() {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $new = $uri->withScheme('http');
+               $this->assertNotSame($uri, $new);
+               $this->assertEquals('http', $new->getScheme());
+               $this->assertEquals('http://user:pass@local.example.com:3001/foo?bar=baz#quz', (string) $new);
+       }
+
+       /**
+        * @test
+        */
+       public function withUserInfoReturnsNewInstanceWithProvidedUser() {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $new = $uri->withUserInfo('matthew');
+               $this->assertNotSame($uri, $new);
+               $this->assertEquals('matthew', $new->getUserInfo());
+               $this->assertEquals('https://matthew@local.example.com:3001/foo?bar=baz#quz', (string) $new);
+       }
+
+       /**
+        * @test
+        */
+       public function withUserInfoReturnsNewInstanceWithProvidedUserAndPassword() {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $new = $uri->withUserInfo('matthew', 'zf2');
+               $this->assertNotSame($uri, $new);
+               $this->assertEquals('matthew:zf2', $new->getUserInfo());
+               $this->assertEquals('https://matthew:zf2@local.example.com:3001/foo?bar=baz#quz', (string) $new);
+       }
+
+       /**
+        * @test
+        */
+       public function withHostReturnsNewInstanceWithProvidedHost() {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $new = $uri->withHost('framework.zend.com');
+               $this->assertNotSame($uri, $new);
+               $this->assertEquals('framework.zend.com', $new->getHost());
+               $this->assertEquals('https://user:pass@framework.zend.com:3001/foo?bar=baz#quz', (string) $new);
+       }
+
+       /**
+        * @return array
+        */
+       public function validPortsDataProvider() {
+               return [
+                       'int'    => [3000],
+                       'string' => ["3000"]
+               ];
+       }
+
+       /**
+        * @dataProvider validPortsDataProvider
+        * @test
+        */
+       public function withPortReturnsNewInstanceWithProvidedPort($port) {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $new = $uri->withPort($port);
+               $this->assertNotSame($uri, $new);
+               $this->assertEquals($port, $new->getPort());
+               $this->assertEquals(
+                       sprintf('https://user:pass@local.example.com:%d/foo?bar=baz#quz', $port),
+                       (string) $new
+               );
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidPortsDataProvider() {
+               return [
+                       'null'      => [NULL],
+                       'true'      => [TRUE],
+                       'false'     => [FALSE],
+                       'string'    => ['string'],
+                       'array'     => [[3000]],
+                       'object'    => [(object) [3000]],
+                       'zero'      => [0],
+                       'too-small' => [-1],
+                       'too-big'   => [65536],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidPortsDataProvider
+        */
+       public function withPortRaisesExceptionForInvalidPorts($port) {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $this->setExpectedException('InvalidArgumentException', 'Invalid port');
+               $new = $uri->withPort($port);
+       }
+
+       /**
+        * @test
+        */
+       public function withPathReturnsNewInstanceWithProvidedPath() {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $new = $uri->withPath('/bar/baz');
+               $this->assertNotSame($uri, $new);
+               $this->assertEquals('/bar/baz', $new->getPath());
+               $this->assertEquals('https://user:pass@local.example.com:3001/bar/baz?bar=baz#quz', (string) $new);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidPathsDataProvider() {
+               return [
+                       'null'     => [NULL],
+                       'true'     => [TRUE],
+                       'false'    => [FALSE],
+                       'array'    => [['/bar/baz']],
+                       'object'   => [(object) ['/bar/baz']],
+                       'query'    => ['/bar/baz?bat=quz'],
+                       'fragment' => ['/bar/baz#bat'],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidPathsDataProvider
+        * @test
+        */
+       public function withPathRaisesExceptionForInvalidPaths($path) {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $this->setExpectedException('InvalidArgumentException', 'Invalid path');
+               $new = $uri->withPath($path);
+       }
+
+       /**
+        * @test
+        */
+       public function withQueryReturnsNewInstanceWithProvidedQuery() {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $new = $uri->withQuery('baz=bat');
+               $this->assertNotSame($uri, $new);
+               $this->assertEquals('baz=bat', $new->getQuery());
+               $this->assertEquals('https://user:pass@local.example.com:3001/foo?baz=bat#quz', (string) $new);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidQueryStringsDataProvider() {
+               return [
+                       'null'     => [NULL],
+                       'true'     => [TRUE],
+                       'false'    => [FALSE],
+                       'array'    => [['baz=bat']],
+                       'object'   => [(object) ['baz=bat']],
+                       'fragment' => ['baz=bat#quz'],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidQueryStringsDataProvider
+        * @test
+        */
+       public function withQueryRaisesExceptionForInvalidQueryStrings($query) {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $this->setExpectedException('InvalidArgumentException', 'Query string');
+               $new = $uri->withQuery($query);
+       }
+
+       /**
+        * @test
+        */
+       public function withFragmentReturnsNewInstanceWithProvidedFragment() {
+               $uri = new Uri('https://user:pass@local.example.com:3001/foo?bar=baz#quz');
+               $new = $uri->withFragment('qat');
+               $this->assertNotSame($uri, $new);
+               $this->assertEquals('qat', $new->getFragment());
+               $this->assertEquals('https://user:pass@local.example.com:3001/foo?bar=baz#qat', (string) $new);
+       }
+
+       /**
+        * @return array
+        */
+       public function authorityInfoDataProvider() {
+               return [
+                       'host-only'      => ['http://foo.com/bar', 'foo.com'],
+                       'host-port'      => ['http://foo.com:3000/bar', 'foo.com:3000'],
+                       'user-host'      => ['http://me@foo.com/bar', 'me@foo.com'],
+                       'user-host-port' => ['http://me@foo.com:3000/bar', 'me@foo.com:3000'],
+               ];
+       }
+
+       /**
+        * @dataProvider authorityInfoDataProvider
+        * @test
+        */
+       public function getAuthorityReturnsExpectedValues($url, $expected) {
+               $uri = new Uri($url);
+               $this->assertEquals($expected, $uri->getAuthority());
+       }
+
+       /**
+        * @test
+        */
+       public function canEmitOriginFormUrl() {
+               $url = '/foo/bar?baz=bat';
+               $uri = new Uri($url);
+               $this->assertEquals($url, (string) $uri);
+       }
+
+       /**
+        * @test
+        */
+       public function settingEmptyPathOnAbsoluteUriReturnsAnEmptyPath() {
+               $uri = new Uri('http://example.com/foo');
+               $new = $uri->withPath('');
+               $this->assertEquals('', $new->getPath());
+       }
+
+       /**
+        * @test
+        */
+       public function stringRepresentationOfAbsoluteUriWithNoPathSetsAnEmptyPath() {
+               $uri = new Uri('http://example.com');
+               $this->assertEquals('http://example.com', (string) $uri);
+       }
+
+       /**
+        * @test
+        */
+       public function getPathOnOriginFormRemainsAnEmptyPath() {
+               $uri = new Uri('?foo=bar');
+               $this->assertEquals('', $uri->getPath());
+       }
+
+       /**
+        * @test
+        */
+       public function stringRepresentationOfOriginFormWithNoPathRetainsEmptyPath() {
+               $uri = new Uri('?foo=bar');
+               $this->assertEquals('?foo=bar', (string) $uri);
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidConstructorUrisDataProvider() {
+               return [
+                       'null'   => [NULL],
+                       'true'   => [TRUE],
+                       'false'  => [FALSE],
+                       'int'    => [1],
+                       'float'  => [1.1],
+                       'array'  => [['http://example.com/']],
+                       'object' => [(object) ['uri' => 'http://example.com/']],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidConstructorUrisDataProvider
+        */
+       public function constructorRaisesExceptionForNonStringURI($uri) {
+               $this->setExpectedException('InvalidArgumentException');
+               new Uri($uri);
+       }
+
+       /**
+        * @test
+        */
+       public function constructorRaisesExceptionForSeriouslyMalformedURI() {
+               $this->setExpectedException('InvalidArgumentException');
+               new Uri('http:///www.php-fig.org/');
+       }
+
+       /**
+        * @test
+        */
+       public function withSchemeStripsOffDelimiter() {
+               $uri = new Uri('http://example.com');
+               $new = $uri->withScheme('https://');
+               $this->assertEquals('https', $new->getScheme());
+       }
+
+       /**
+        * @return array
+        */
+       public function invalidSchemesDataProvider() {
+               return [
+                       'mailto' => ['mailto'],
+                       'ftp'    => ['ftp'],
+                       'telnet' => ['telnet'],
+                       'ssh'    => ['ssh'],
+                       'git'    => ['git'],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidSchemesDataProvider
+        * @test
+        */
+       public function constructWithUnsupportedSchemeRaisesAnException($scheme) {
+               $this->setExpectedException('InvalidArgumentException', 'Unsupported scheme');
+               $uri = new Uri($scheme . '://example.com');
+       }
+
+       /**
+        * @dataProvider invalidSchemesDataProvider
+        * @test
+        */
+       public function withSchemeUsingUnsupportedSchemeRaisesAnException($scheme) {
+               $uri = new Uri('http://example.com');
+               $this->setExpectedException('InvalidArgumentException', 'Unsupported scheme');
+               $uri->withScheme($scheme);
+       }
+
+       /**
+        * @test
+        */
+       public function withPathIsNotPrefixedWithSlashIfSetWithoutOne() {
+               $uri = new Uri('http://example.com');
+               $new = $uri->withPath('foo/bar');
+               $this->assertEquals('foo/bar', $new->getPath());
+       }
+
+       /**
+        * @test
+        */
+       public function withPathNotSlashPrefixedIsEmittedWithSlashDelimiterWhenUriIsCastToString() {
+               $uri = new Uri('http://example.com');
+               $new = $uri->withPath('foo/bar');
+               $this->assertEquals('http://example.com/foo/bar', $new->__toString());
+       }
+
+       /**
+        * @test
+        */
+       public function withQueryStripsQueryPrefixIfPresent() {
+               $uri = new Uri('http://example.com');
+               $new = $uri->withQuery('?foo=bar');
+               $this->assertEquals('foo=bar', $new->getQuery());
+       }
+
+       /**
+        * @test
+        */
+       public function withFragmentStripsFragmentPrefixIfPresent() {
+               $uri = new Uri('http://example.com');
+               $new = $uri->withFragment('#/foo/bar');
+               $this->assertEquals('/foo/bar', $new->getFragment());
+       }
+
+       /**
+        * @return array
+        */
+       public function standardSchemePortCombinationsDataProvider() {
+               return [
+                       'http'  => ['http', 80],
+                       'https' => ['https', 443],
+               ];
+       }
+
+       /**
+        * @dataProvider standardSchemePortCombinationsDataProvider
+        * @test
+        */
+       public function getAuthorityOmitsPortForStandardSchemePortCombinations($scheme, $port) {
+               $uri = (new Uri())
+                       ->withHost('example.com')
+                       ->withScheme($scheme)
+                       ->withPort($port);
+               $this->assertEquals('example.com', $uri->getAuthority());
+       }
+
+       /**
+        * @test
+        */
+       public function getPathIsProperlyEncoded() {
+               $uri = (new Uri())->withPath('/foo^bar');
+               $expected = '/foo%5Ebar';
+               $this->assertEquals($expected, $uri->getPath());
+       }
+
+       /**
+        * @test
+        */
+       public function getPathDoesNotBecomeDoubleEncoded() {
+               $uri = (new Uri())->withPath('/foo%5Ebar');
+               $expected = '/foo%5Ebar';
+               $this->assertEquals($expected, $uri->getPath());
+       }
+
+       /**
+        * @return array
+        */
+       public function queryStringsForEncodingDataProvider() {
+               return [
+                       'key-only'        => ['k^ey', 'k%5Eey'],
+                       'key-value'       => ['k^ey=valu`', 'k%5Eey=valu%60'],
+                       'array-key-only'  => ['key[]', 'key%5B%5D'],
+                       'array-key-value' => ['key[]=valu`', 'key%5B%5D=valu%60'],
+                       'complex'         => ['k^ey&key[]=valu`&f<>=`bar', 'k%5Eey&key%5B%5D=valu%60&f%3C%3E=%60bar'],
+               ];
+       }
+
+       /**
+        * @dataProvider queryStringsForEncodingDataProvider
+        * @test
+        */
+       public function getQueryIsProperlyEncoded($query, $expected) {
+               $uri = (new Uri())->withQuery($query);
+               $this->assertEquals($expected, $uri->getQuery());
+       }
+
+       /**
+        * @dataProvider queryStringsForEncodingDataProvider
+        * @test
+        */
+       public function getQueryIsNotDoubleEncoded($query, $expected) {
+               $uri = (new Uri())->withQuery($expected);
+               $this->assertEquals($expected, $uri->getQuery());
+       }
+
+       /**
+        * @test
+        */
+       public function getFragmentIsProperlyEncoded() {
+               $uri = (new Uri())->withFragment('/p^th?key^=`bar#b@z');
+               $expected = '/p%5Eth?key%5E=%60bar%23b@z';
+               $this->assertEquals($expected, $uri->getFragment());
+       }
+
+       /**
+        * @test
+        */
+       public function getFragmentIsNotDoubleEncoded() {
+               $expected = '/p%5Eth?key%5E=%60bar%23b@z';
+               $uri = (new Uri())->withFragment($expected);
+               $this->assertEquals($expected, $uri->getFragment());
+       }
+}
index a2fbef8..2fe2551 100644 (file)
@@ -20,6 +20,7 @@ use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Frontend\Utility\EidUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Core\RequestHandlerInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 /**
  * Lightweight alternative to the regular RequestHandler used when $_GET[eID] is set.
@@ -34,7 +35,7 @@ class EidRequestHandler implements RequestHandlerInterface {
        protected $bootstrap;
 
        /**
-        * Constructor handing over the bootstrap
+        * Constructor handing over the bootstrap and the original request
         *
         * @param Bootstrap $bootstrap
         */
@@ -45,15 +46,17 @@ class EidRequestHandler implements RequestHandlerInterface {
        /**
         * Handles a frontend request based on the _GP "eID" variable.
         *
-        * @return void
+        * @param ServerRequestInterface $request
+        * @return NULL|\Psr\Http\Message\ResponseInterface
         */
-       public function handleRequest() {
+       public function handleRequest(ServerRequestInterface $request) {
+               $response = NULL;
                // Timetracking started
                $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
                if (empty($configuredCookieName)) {
                        $configuredCookieName = 'be_typo_user';
                }
-               if ($_COOKIE[$configuredCookieName]) {
+               if ($request->getCookieParams()[$configuredCookieName]) {
                        $GLOBALS['TT'] = new TimeTracker();
                } else {
                        $GLOBALS['TT'] = new NullTimeTracker();
@@ -74,17 +77,18 @@ class EidRequestHandler implements RequestHandlerInterface {
                // Remove any output produced until now
                $this->bootstrap->endOutputBufferingAndCleanPreviousOutput();
                require EidUtility::getEidScriptPath();
-               $this->bootstrap->shutdown();
-               exit;
+
+               return $response;
        }
 
        /**
         * This request handler can handle any frontend request.
         *
+        * @param ServerRequestInterface $request The request to process
         * @return bool If the request is not an eID request, TRUE otherwise FALSE
         */
-       public function canHandleRequest() {
-               return GeneralUtility::_GP('eID') ? TRUE : FALSE;
+       public function canHandleRequest(ServerRequestInterface $request) {
+               return $request->getQueryParams()['eID'] || $request->getParsedBody()['eID'] ? TRUE : FALSE;
        }
 
        /**
@@ -96,4 +100,5 @@ class EidRequestHandler implements RequestHandlerInterface {
        public function getPriority() {
                return 80;
        }
+
 }
index d11a1d6..b223440 100644 (file)
@@ -58,7 +58,13 @@ class RequestHandler implements RequestHandlerInterface {
        protected $controller;
 
        /**
-        * Constructor handing over the bootstrap
+        * The request handed over
+        * @var \Psr\Http\Message\ServerRequestInterface
+        */
+       protected $request;
+
+       /**
+        * Constructor handing over the bootstrap and the original request
         *
         * @param Bootstrap $bootstrap
         */
@@ -69,9 +75,12 @@ class RequestHandler implements RequestHandlerInterface {
        /**
         * Handles a frontend 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) {
+               $response = NULL;
+               $this->request = $request;
                $this->initializeTimeTracker();
 
                // Hook to preprocess the current request:
@@ -266,7 +275,9 @@ class RequestHandler implements RequestHandlerInterface {
                }
 
                if ($sendTSFEContent) {
-                       echo $this->controller->content;
+                       /** @var \TYPO3\CMS\Core\Http\Response $response */
+                       $response = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\Response::class);
+                       $response->getBody()->write($this->controller->content);
                }
                // Debugging Output
                if (isset($GLOBALS['error']) && is_object($GLOBALS['error']) && @is_callable(array($GLOBALS['error'], 'debugOutput'))) {
@@ -275,15 +286,17 @@ class RequestHandler implements RequestHandlerInterface {
                if (TYPO3_DLOG) {
                        GeneralUtility::devLog('END of FRONTEND session', 'cms', 0, array('_FLUSH' => TRUE));
                }
+               return $response;
        }
 
        /**
         * This request handler can handle any frontend request.
         *
+        * @param \Psr\Http\Message\ServerRequestInterface $request
         * @return bool If the request is not an eID request, TRUE otherwise FALSE
         */
-       public function canHandleRequest() {
-               return GeneralUtility::_GP('eID') ? FALSE : TRUE;
+       public function canHandleRequest(\Psr\Http\Message\ServerRequestInterface $request) {
+               return $request->getQueryParams()['eID'] || $request->getParsedBody()['eID'] ? FALSE : TRUE;
        }
 
        /**
@@ -319,7 +332,7 @@ class RequestHandler implements RequestHandlerInterface {
                if (empty($configuredCookieName)) {
                        $configuredCookieName = 'be_typo_user';
                }
-               if ($_COOKIE[$configuredCookieName]) {
+               if ($this->request->getCookieParams()[$configuredCookieName]) {
                        $this->timeTracker = new TimeTracker();
                } else {
                        $this->timeTracker = new NullTimeTracker();
@@ -357,4 +370,5 @@ class RequestHandler implements RequestHandlerInterface {
                // This is a dirty workaround and bypasses the protected access modifier of the controller member.
                $GLOBALS['TSFE'] = &$this->controller;
        }
+
 }