diff --git a/lib/classes/Request.class.php b/lib/classes/Request.class.php
index 34029720390dd879f8818b3794934cf210da2d8e..5aa52b744b61d44eb397f6586d225a86dd998533 100644
--- a/lib/classes/Request.class.php
+++ b/lib/classes/Request.class.php
@@ -131,6 +131,33 @@ class Request implements ArrayAccess, IteratorAggregate
         return $_SERVER['REQUEST_URI'];
     }
 
+    /**
+     * Return the filename of the currently executing script.
+     * @return string
+     */
+    public static function script_name(): string
+    {
+        return $_SERVER['SCRIPT_NAME'];
+    }
+
+    /**
+     * Returns the complete path info including duplicated slashes.
+     * $_SERVER['PATH_INFO'] will remove them.
+     */
+    public static function path_info(): string
+    {
+        $script_name = self::script_name();
+        $script_name = preg_quote($script_name, '#');
+        $script_name = str_replace('/', '/+', $script_name);
+
+        $path_info = preg_replace(
+            "#^{$script_name}#",
+            '',
+            urldecode(self::path())
+        );
+        return parse_url($path_info, PHP_URL_PATH);
+    }
+
     /**
      * Set the selected query parameter to a specific value.
      *
@@ -715,7 +742,7 @@ class Request implements ArrayAccess, IteratorAggregate
             $extract[] = array_values(array_filter(array_map('trim', explode(' ', $one))));
         }
         foreach ($extract as $one) {
-            list($param, $func) = $one;
+            [$param, $func] = $one;
             if (!$func) {
                 $func = 'get';
             }
diff --git a/lib/classes/restapi/Router.php b/lib/classes/restapi/Router.php
index 34db554d437bd5f75edcfad1defed2f4ae835c78..7650919f7286295ce63a4c616b0f4aea54d0fd1c 100644
--- a/lib/classes/restapi/Router.php
+++ b/lib/classes/restapi/Router.php
@@ -523,7 +523,7 @@ class Router
 
     private function normalizeDispatchURI($uri)
     {
-        return $uri === null ? $_SERVER['PATH_INFO'] : $uri;
+        return $uri ?? \Request::path_info();
     }
 
     private function normalizeRequestMethod($method)
diff --git a/public/api.php b/public/api.php
index 51f06727ecf195e013ad751dabb2844c31ab6e39..1376ae4cd56245f0a69ce611ca5233bc0c6bbff7 100644
--- a/public/api.php
+++ b/public/api.php
@@ -30,6 +30,7 @@ namespace {
 }
 
 namespace RESTAPI {
+
     use Config;
 
     // A potential api exception will lead to an according response with the
@@ -44,7 +45,7 @@ namespace RESTAPI {
         // Initialize RESTAPI plugins
         \PluginEngine::getPlugins('RESTAPIPlugin');
 
-        $uri = $_SERVER['PATH_INFO'];
+        $uri = \Request::path_info();
 
         // Check version
         if (defined('RESTAPI\\VERSION') && preg_match('~^/v(\d+)~i', $uri, $match)) {
diff --git a/public/assets.php b/public/assets.php
index 8e066859b3b6630e22a9797209aeb6bb552538c7..824f84c4102de9802e40a0b8b47ceaa54adc97fa 100644
--- a/public/assets.php
+++ b/public/assets.php
@@ -15,7 +15,7 @@
 require_once '../lib/bootstrap.php';
 
 // Obtain request information
-$uri = ltrim($_SERVER['PATH_INFO'], '/');
+$uri = ltrim(Request::path_info(), '/');
 list($type, $id) = explode('/', $uri, 2);
 
 // Setup response
diff --git a/public/dispatch.php b/public/dispatch.php
index b6f6847714b20dee3257a3f4a9357cc98959b6d5..3bd9291cb20d5438a1a5e0a426e93b4070479c84 100644
--- a/public/dispatch.php
+++ b/public/dispatch.php
@@ -21,7 +21,5 @@ require '../lib/bootstrap.php';
 // prepare environment
 URLHelper::setBaseUrl($GLOBALS['ABSOLUTE_URI_STUDIP']);
 
-$request_uri = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '/';
-
 $dispatcher = app(\Trails_Dispatcher::class);
-$dispatcher->dispatch($request_uri);
+$dispatcher->dispatch(Request::path_info());
diff --git a/public/install.php b/public/install.php
index a82e84f8b748e50422694b0ac793fcae4cdbefd0..c10f928f46affb7bd8f8fc3aee7a2a8d4db35d73 100644
--- a/public/install.php
+++ b/public/install.php
@@ -55,7 +55,7 @@ if (!function_exists('_')) {
 $GLOBALS['template_factory'] = new Flexi_TemplateFactory('../templates/');
 
 # get plugin class from request
-$dispatch_to = $_SERVER['PATH_INFO'] ?: '';
+$dispatch_to = ltrim(Request::path_info(), '/');
 
 $dispatcher = new Trails_Dispatcher( '../app', $_SERVER['SCRIPT_NAME'], 'admin/install');
 $dispatcher->dispatch("admin/install/{$dispatch_to}");
diff --git a/public/plugins.php b/public/plugins.php
index 1488118720b7e5f0736275167430be7c496b09b2..368113dc9e33432e4aa3649f6e701dfb8d30eacc 100644
--- a/public/plugins.php
+++ b/public/plugins.php
@@ -27,7 +27,7 @@ try {
     require_once 'lib/seminar_open.php';
 
     // get plugin class from request
-    $dispatch_to = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
+    $dispatch_to = Request::path_info();
     list($plugin_class, $unconsumed) = PluginEngine::routeRequest($dispatch_to);
 
     // handle legacy forum plugin URLs
diff --git a/public/web_migrate.php b/public/web_migrate.php
index 216a1fdb6c3c2ce05e7141fcf0fcd70110104ad4..02eeb956e080f63c5eee8ef5618d891db2b8cd7f 100644
--- a/public/web_migrate.php
+++ b/public/web_migrate.php
@@ -34,7 +34,7 @@ $_language_path = init_i18n($_SESSION['_language']);
 $GLOBALS['template_factory'] = new Flexi_TemplateFactory('../templates/');
 
 # get plugin class from request
-$dispatch_to = $_SERVER['PATH_INFO'] ?: '';
+$dispatch_to = Request::path_info() ?: '';
 
 $dispatcher = app(\Trails_Dispatcher::class);
 $dispatcher->trails_uri = $_SERVER['SCRIPT_NAME'];
diff --git a/tests/unit/lib/FunctionsTest.php b/tests/unit/lib/FunctionsTest.php
index b7d2498825687c9bb4609fbb39fde81a5bf24142..9a92f1a2755979dbf8c88f3906b3066950003832 100644
--- a/tests/unit/lib/FunctionsTest.php
+++ b/tests/unit/lib/FunctionsTest.php
@@ -73,4 +73,18 @@ class FunctionsTest extends \Codeception\Test\Unit
         $output = 'https://m%C3%A4uschen-h%C3%BCpft.de/%C3%B6ffnungszeiten?menu=Spa%C3%9F&page=23';
         $this->assertEquals($output, encodeURI($input));
     }
+
+    /**
+     * @covers Trails_Controller::extract_action_and_args()
+     */
+    public function testTrailsControllerExtractActionAndArgs()
+    {
+        $controller = new Trails_Controller(null);
+        list($action, $args, $format) = $controller->extract_action_and_args('foo/bar//42.html');
+
+        $this->assertEquals('foo', $action);
+        $this->assertEquals(['bar', '', '42'], $args);
+        $this->assertEquals('html', $format);
+
+    }
 }
diff --git a/tests/unit/lib/classes/RequestMethodTest.php b/tests/unit/lib/classes/RequestMethodTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..64670154b4611426dca4abe4ea97caeae1096f22
--- /dev/null
+++ b/tests/unit/lib/classes/RequestMethodTest.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * @backupGlobals enabled
+ */
+class RequestMethodTest extends \Codeception\Test\Unit
+{
+    public function setUp(): void
+    {
+        unset($_SERVER['REQUEST_METHOD']);
+        unset($_SERVER['HTTP_X_REQUESTED_WITH']);
+    }
+
+    protected function setRequestMethod($method)
+    {
+        $_SERVER['REQUEST_METHOD'] = (string)$method;
+    }
+
+    public function testMethod()
+    {
+        $this->setRequestMethod('GET');
+        $this->assertEquals('GET', Request::method());
+    }
+
+    public function testMethodUppercases()
+    {
+        $this->setRequestMethod('gEt');
+        $this->assertEquals('GET', Request::method());
+    }
+
+    public function testRequestMethodGet()
+    {
+        $this->setRequestMethod('GET');
+        $this->assertTrue(Request::isGet());
+    }
+
+    public function testRequestMethodPost()
+    {
+        $this->setRequestMethod('POST');
+        $this->assertTrue(Request::isPost());
+    }
+
+    public function testRequestMethodPut()
+    {
+        $this->setRequestMethod('PUT');
+        $this->assertTrue(Request::isPut());
+    }
+
+    public function testRequestMethodDelete()
+    {
+        $this->setRequestMethod('DELETE');
+        $this->assertTrue(Request::isDelete());
+    }
+
+    public function testIsNotXhr()
+    {
+        $this->assertFalse(Request::isXhr());
+        $this->assertFalse(Request::isAjax());
+    }
+
+    public function testIsXhr()
+    {
+        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'XmlHttpRequest';
+        $this->assertTrue(Request::isAjax());
+        $this->assertTrue(Request::isXhr());
+    }
+}
diff --git a/tests/unit/lib/classes/RequestParametersTest.php b/tests/unit/lib/classes/RequestParametersTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..85194c48efbb253542b29791542e372ad972d236
--- /dev/null
+++ b/tests/unit/lib/classes/RequestParametersTest.php
@@ -0,0 +1,197 @@
+<?php
+/*
+ * RequestParametersTest.php - unit tests for the Request class handling of
+ * parameters.
+ *
+ * Copyright (c) 2009  Elmar Ludwig
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ */
+
+/**
+ * @backupGlobals enabled
+ */
+class RequestParametersTest extends Codeception\Test\Unit
+{
+    public function setUp (): void
+    {
+        $_GET['a']    = 'test';
+        $_POST['b']   = '\\h1"';
+        $_GET['c']    = '-23';
+        $_POST['d']   = '12.7';
+        $_GET['e']    = '3,14';
+        $_POST['s_x'] = '0';
+        $_GET['f']    = 'root@studip';
+        $_POST['g']   = '1';
+        $_GET['h']    = '';
+
+        $_GET['v1']  = ['1', '2.4', '3,7'];
+        $_POST['v2'] = ['on\'e', 'two', 'thr33'];
+        $_GET['v3']  = ['root@studip', 'hotte.testfreund', 42, '!"$%&/()'];
+        $_POST['v4'] = ['0', '1', '', 'foo'];
+
+        $testconfig = new Config([
+            'USERNAME_REGULAR_EXPRESSION' => '/^([a-zA-Z0-9_@.-]{4,})$/',
+        ]);
+        Config::set($testconfig);
+    }
+
+    public function testArrayAccess ()
+    {
+        $request = Request::getInstance();
+
+        $this->assertNull($request['null']);
+        $this->assertSame($request['a'], 'test');
+        $this->assertSame($request['b'], '\\h1"');
+        $this->assertSame($request['c'], '-23');
+    }
+
+    public function testSetParam ()
+    {
+        Request::set('yyy', 'xyzzy');
+        Request::set('zzz', [1, 2]);
+
+        $this->assertSame(Request::get('yyy'), 'xyzzy');
+        $this->assertSame(Request::getArray('zzz'), [1, 2]);
+    }
+
+    public function testStringParam ()
+    {
+        $this->assertNull(Request::get('null'));
+        $this->assertSame(Request::get('null', 'foo'), 'foo');
+        $this->assertSame(Request::get('a'), 'test');
+        $this->assertSame(Request::get('b'), '\\h1"');
+        $this->assertSame(Request::get('c'), '-23');
+        $this->assertSame(Request::get('d'), '12.7');
+        $this->assertNull(Request::get('v2'));
+
+        $this->assertNull(Request::quoted('null'));
+        $this->assertSame(Request::quoted('null', 'foo'), 'foo');
+        $this->assertSame(Request::quoted('b'), '\\\\h1\\"');
+        $this->assertNull(Request::quoted('v2'));
+    }
+
+    public function testOptionParam ()
+    {
+        $this->assertNull(Request::option('null'));
+        $this->assertSame(Request::option('a'), 'test');
+        $this->assertNull(Request::option('b'));
+        $this->assertNull(Request::option('v1'));
+    }
+
+    public function testIntParam ()
+    {
+        $this->assertNull(Request::int('null'));
+        $this->assertSame(Request::int('a'), 0);
+        $this->assertSame(Request::int('c'), -23);
+        $this->assertSame(Request::int('d'), 12);
+        $this->assertSame(Request::int('e'), 3);
+        $this->assertNull(Request::int('v1'));
+    }
+
+    public function testFloatParam ()
+    {
+        $this->assertNull(Request::float('null'));
+        $this->assertSame(Request::float('a'), 0.0);
+        $this->assertSame(Request::float('c'), -23.0);
+        $this->assertSame(Request::float('d'), 12.7);
+        $this->assertSame(Request::float('e'), 3.14);
+        $this->assertNull(Request::float('v1'));
+    }
+
+    public function testBoolParam ()
+    {
+        $this->assertNull(Request::bool('null'));
+        $this->assertTrue(Request::bool('a'));
+        $this->assertTrue(Request::bool('c'));
+        $this->assertTrue(Request::bool('d'));
+        $this->assertTrue(Request::bool('e'));
+        $this->assertTrue(Request::bool('g'));
+        $this->assertFalse(Request::bool('h'));
+        $this->assertFalse(Request::bool('s_x'));
+        $this->assertNull(Request::bool('v1'));
+    }
+
+    public function testUsernameParam ()
+    {
+        $this->assertNull(Request::username('null'));
+        $this->assertSame(Request::username('a'), 'test');
+        $this->assertSame(Request::username('f'), 'root@studip');
+        $this->assertNull(Request::username('b'));
+        $this->assertNull(Request::username('v1'));
+    }
+
+    public function testStringArrayParam ()
+    {
+        $this->assertSame(Request::getArray('null'), []);
+        $this->assertSame(Request::getArray('b'), []);
+        $this->assertSame(Request::getArray('v1'), ['1', '2.4', '3,7']);
+        $this->assertSame(Request::getArray('v2'), ['on\'e', 'two', 'thr33']);
+
+        $this->assertSame(Request::quotedArray('null'), []);
+        $this->assertSame(Request::quotedArray('b'), []);
+        $this->assertSame(Request::quotedArray('v1'), ['1', '2.4', '3,7']);
+        $this->assertSame(Request::quotedArray('v2'), ['on\\\'e', 'two', 'thr33']);
+    }
+
+    public function testOptionArrayParam ()
+    {
+        $this->assertSame(Request::optionArray('null'), []);
+        $this->assertSame(Request::optionArray('a'), []);
+        $this->assertSame(Request::optionArray('v1'), ['1']);
+        $this->assertSame(Request::optionArray('v2'), [1 => 'two', 2 => 'thr33']);
+    }
+
+    public function testIntArrayParam ()
+    {
+        $this->assertSame(Request::intArray('null'), []);
+        $this->assertSame(Request::intArray('c'), []);
+        $this->assertSame(Request::intArray('v1'), [1, 2, 3]);
+        $this->assertSame(Request::intArray('v2'), [0, 0, 0]);
+    }
+
+    public function testFloatArrayParam ()
+    {
+        $this->assertSame(Request::floatArray('null'), []);
+        $this->assertSame(Request::floatArray('c'), []);
+        $this->assertSame(Request::floatArray('v1'), [1.0, 2.4, 3.7]);
+        $this->assertSame(Request::floatArray('v2'), [0.0, 0.0, 0.0]);
+    }
+
+    public function testBoolArrayParam ()
+    {
+        $this->assertSame(Request::boolArray('null'), []);
+        $this->assertSame(Request::boolArray('c'), []);
+        $this->assertSame(Request::boolArray('v4'), [false, true, false, true]);
+    }
+
+    public function testUsernameArrayParam ()
+    {
+        $this->assertSame(Request::usernameArray('null'), []);
+        $this->assertSame(Request::usernameArray('a'), []);
+        $this->assertSame(Request::usernameArray('v1'), []);
+        $this->assertSame(Request::usernameArray('v2'), [2 => 'thr33']);
+        $this->assertSame(Request::usernameArray('v3'), ['root@studip', 'hotte.testfreund']);
+    }
+
+    public function testSubmitted ()
+    {
+        $this->assertFalse(Request::submitted('null'));
+        $this->assertTrue(Request::submitted('s'));
+        $this->assertTrue(Request::submitted('v1'));
+    }
+
+    public function testSubmittedSome ()
+    {
+        $this->assertFalse(Request::submittedSome('null', 'null'));
+        $this->assertTrue(Request::submittedSome('null', 's', 'v'));
+    }
+
+    public function tearDown(): void
+    {
+        Config::set(null);
+    }
+}
diff --git a/tests/unit/lib/classes/RequestTest.php b/tests/unit/lib/classes/RequestTest.php
index f3afd7c76782dedf4082824ba5830e99704027a3..00351968839509467c0286f48a5c7b2aa8b05add 100644
--- a/tests/unit/lib/classes/RequestTest.php
+++ b/tests/unit/lib/classes/RequestTest.php
@@ -10,264 +10,137 @@
  * the License, or (at your option) any later version.
  */
 
+/**
+ * @backupGlobals enabled
+ */
 class RequestTest extends \Codeception\Test\Unit
 {
-    public function setUp (): void
-    {
-        $_GET['a']    = 'test';
-        $_POST['b']   = '\\h1"';
-        $_GET['c']    = '-23';
-        $_POST['d']   = '12.7';
-        $_GET['e']    = '3,14';
-        $_POST['s_x'] = '0';
-        $_GET['f']    = 'root@studip';
-        $_POST['g']   = '1';
-        $_GET['h']    = '';
-
-        $_GET['v1']  = ['1', '2.4', '3,7'];
-        $_POST['v2'] = ['on\'e', 'two', 'thr33'];
-        $_GET['v3']  = ['root@studip', 'hotte.testfreund', 42, '!"$%&/()'];
-        $_POST['v4'] = ['0', '1', '', 'foo'];
-
-        $testconfig = new Config([
-            'USERNAME_REGULAR_EXPRESSION' => '/^([a-zA-Z0-9_@.-]{4,})$/',
-        ]);
-        Config::set($testconfig);
-    }
-
-    public function testURL ()
-    {
-        $_SERVER['HTTPS'] = 'on';
-        $_SERVER['SERVER_NAME'] = 'www.example.com';
-        $_SERVER['SERVER_PORT'] = '443';
-        $_SERVER['REQUEST_URI'] = '/do/it?now=1';
-
-        $this->assertEquals('https://www.example.com/do/it?now=1', Request::url());
-
-        $_SERVER['HTTPS'] = '';
-        $_SERVER['SERVER_NAME'] = 'www.example.com';
-        $_SERVER['SERVER_PORT'] = '8080';
-        $_SERVER['REQUEST_URI'] = '/index.php';
-
-        $this->assertEquals('http://www.example.com:8080/index.php', Request::url());
-    }
-
-    public function testArrayAccess ()
-    {
-        $request = Request::getInstance();
-
-        $this->assertNull($request['null']);
-        $this->assertSame($request['a'], 'test');
-        $this->assertSame($request['b'], '\\h1"');
-        $this->assertSame($request['c'], '-23');
-    }
-
-    public function testSetParam ()
-    {
-        Request::set('yyy', 'xyzzy');
-        Request::set('zzz', [1, 2]);
-
-        $this->assertSame(Request::get('yyy'), 'xyzzy');
-        $this->assertSame(Request::getArray('zzz'), [1, 2]);
-    }
-
-    public function testStringParam ()
-    {
-        $this->assertNull(Request::get('null'));
-        $this->assertSame(Request::get('null', 'foo'), 'foo');
-        $this->assertSame(Request::get('a'), 'test');
-        $this->assertSame(Request::get('b'), '\\h1"');
-        $this->assertSame(Request::get('c'), '-23');
-        $this->assertSame(Request::get('d'), '12.7');
-        $this->assertNull(Request::get('v2'));
-
-        $this->assertNull(Request::quoted('null'));
-        $this->assertSame(Request::quoted('null', 'foo'), 'foo');
-        $this->assertSame(Request::quoted('b'), '\\\\h1\\"');
-        $this->assertNull(Request::quoted('v2'));
-    }
-
-    public function testOptionParam ()
-    {
-        $this->assertNull(Request::option('null'));
-        $this->assertSame(Request::option('a'), 'test');
-        $this->assertNull(Request::option('b'));
-        $this->assertNull(Request::option('v1'));
-    }
-
-    public function testIntParam ()
-    {
-        $this->assertNull(Request::int('null'));
-        $this->assertSame(Request::int('a'), 0);
-        $this->assertSame(Request::int('c'), -23);
-        $this->assertSame(Request::int('d'), 12);
-        $this->assertSame(Request::int('e'), 3);
-        $this->assertNull(Request::int('v1'));
-    }
-
-    public function testFloatParam ()
-    {
-        $this->assertNull(Request::float('null'));
-        $this->assertSame(Request::float('a'), 0.0);
-        $this->assertSame(Request::float('c'), -23.0);
-        $this->assertSame(Request::float('d'), 12.7);
-        $this->assertSame(Request::float('e'), 3.14);
-        $this->assertNull(Request::float('v1'));
-    }
-
-    public function testBoolParam ()
-    {
-        $this->assertNull(Request::bool('null'));
-        $this->assertTrue(Request::bool('a'));
-        $this->assertTrue(Request::bool('c'));
-        $this->assertTrue(Request::bool('d'));
-        $this->assertTrue(Request::bool('e'));
-        $this->assertTrue(Request::bool('g'));
-        $this->assertFalse(Request::bool('h'));
-        $this->assertFalse(Request::bool('s_x'));
-        $this->assertNull(Request::bool('v1'));
-    }
-
-    public function testUsernameParam ()
-    {
-        $this->assertNull(Request::username('null'));
-        $this->assertSame(Request::username('a'), 'test');
-        $this->assertSame(Request::username('f'), 'root@studip');
-        $this->assertNull(Request::username('b'));
-        $this->assertNull(Request::username('v1'));
-    }
-
-    public function testStringArrayParam ()
+    public function setUp(): void
     {
-        $this->assertSame(Request::getArray('null'), []);
-        $this->assertSame(Request::getArray('b'), []);
-        $this->assertSame(Request::getArray('v1'), ['1', '2.4', '3,7']);
-        $this->assertSame(Request::getArray('v2'), ['on\'e', 'two', 'thr33']);
-
-        $this->assertSame(Request::quotedArray('null'), []);
-        $this->assertSame(Request::quotedArray('b'), []);
-        $this->assertSame(Request::quotedArray('v1'), ['1', '2.4', '3,7']);
-        $this->assertSame(Request::quotedArray('v2'), ['on\\\'e', 'two', 'thr33']);
+        unset($_SERVER['HTTPS']);
+        unset($_SERVER['HTTP_X_FORWARDED_PROTO']);
+        unset($_SERVER['REQUEST_URI']);
+        unset($_SERVER['SCRIPT_NAME']);
+        unset($_SERVER['SERVER_NAME']);
+        unset($_SERVER['SERVER_PORT']);
     }
 
-    public function testOptionArrayParam ()
+    protected function setScriptName(string $script_name): void
     {
-        $this->assertSame(Request::optionArray('null'), []);
-        $this->assertSame(Request::optionArray('a'), []);
-        $this->assertSame(Request::optionArray('v1'), ['1']);
-        $this->assertSame(Request::optionArray('v2'), [1 => 'two', 2 => 'thr33']);
+        $_SERVER['SCRIPT_NAME'] = $script_name;
     }
 
-    public function testIntArrayParam ()
+    protected function setRequestUri(string $request_uri): void
     {
-        $this->assertSame(Request::intArray('null'), []);
-        $this->assertSame(Request::intArray('c'), []);
-        $this->assertSame(Request::intArray('v1'), [1, 2, 3]);
-        $this->assertSame(Request::intArray('v2'), [0, 0, 0]);
+        $_SERVER['REQUEST_URI'] = $request_uri;
     }
 
-    public function testFloatArrayParam ()
+    protected function setServerNameAndPort(string $name, int $port, bool $ssl = false): void
     {
-        $this->assertSame(Request::floatArray('null'), []);
-        $this->assertSame(Request::floatArray('c'), []);
-        $this->assertSame(Request::floatArray('v1'), [1.0, 2.4, 3.7]);
-        $this->assertSame(Request::floatArray('v2'), [0.0, 0.0, 0.0]);
+        $_SERVER['SERVER_NAME'] = $name;
+        $_SERVER['SERVER_PORT'] = $port;
+        $_SERVER['HTTPS'] = $ssl ? 'on' : 'off';
     }
 
-    public function testBoolArrayParam ()
+    /**
+     * @covers Request::protocol
+     */
+    public function testProtocol(): void
     {
-        $this->assertSame(Request::boolArray('null'), []);
-        $this->assertSame(Request::boolArray('c'), []);
-        $this->assertSame(Request::boolArray('v4'), [false, true, false, true]);
-    }
+        $this->assertEquals('http', Request::protocol());
 
-    public function testUsernameArrayParam ()
-    {
-        $this->assertSame(Request::usernameArray('null'), []);
-        $this->assertSame(Request::usernameArray('a'), []);
-        $this->assertSame(Request::usernameArray('v1'), []);
-        $this->assertSame(Request::usernameArray('v2'), [2 => 'thr33']);
-        $this->assertSame(Request::usernameArray('v3'), ['root@studip', 'hotte.testfreund']);
-    }
+        $_SERVER['HTTPS'] = 'on';
+        $this->assertEquals('https', Request::protocol());
 
-    public function testSubmitted ()
-    {
-        $this->assertFalse(Request::submitted('null'));
-        $this->assertTrue(Request::submitted('s'));
-        $this->assertTrue(Request::submitted('v1'));
+        $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'foo';
+        $this->assertEquals('foo', Request::protocol());
     }
 
-    public function testSubmittedSome ()
+    /**
+     * @covers Request::server
+     */
+    public function testServer(): void
     {
-        $this->assertFalse(Request::submittedSome('null', 'null'));
-        $this->assertTrue(Request::submittedSome('null', 's', 'v'));
-    }
+        // Usual ports for http and https
+        $this->setServerNameAndPort('www.studip.de', 80);
+        $this->assertEquals('www.studip.de', Request::server());
 
-    public function tearDown(): void
-    {
-        Config::set(null);
-    }
-}
+        $this->setServerNameAndPort('www.studip.de', 443, true);
+        $this->assertEquals('www.studip.de', Request::server());
 
-class RequestMethodTest extends \Codeception\Test\Unit
-{
-    public function setUp (): void
-    {
-        unset($_SERVER['REQUEST_METHOD']);
-        unset($_SERVER['HTTP_X_REQUESTED_WITH']);
-    }
+        // Unusual ports for http and https
+        $this->setServerNameAndPort('www.studip.de', 80, true);
+        $this->assertEquals('www.studip.de:80', Request::server());
 
-    protected function setRequestMethod($method)
-    {
-        $_SERVER['REQUEST_METHOD'] = (string) $method;
-    }
+        $this->setServerNameAndPort('www.studip.de', 443, false);
+        $this->assertEquals('www.studip.de:443', Request::server());
 
-    public function testMethod()
-    {
-        $this->setRequestMethod('GET');
-        $this->assertEquals('GET', Request::method());
-    }
+        // Other tests
+        $this->setServerNameAndPort('www.studip.de', 8088);
+        $this->assertEquals('www.studip.de:8088', Request::server());
 
-    public function testMethodUppercases()
-    {
-        $this->setRequestMethod('gEt');
-        $this->assertEquals('GET', Request::method());
+        $this->setServerNameAndPort('www.studip.de', 8088, true);
+        $this->assertEquals('www.studip.de:8088', Request::server());
     }
 
-    public function testRequestMethodGet()
+    /**
+     * @covers Request::path()
+     */
+    public function testPath(): void
     {
-        $this->setRequestMethod('GET');
-        $this->assertTrue(Request::isGet());
+        $this->setRequestUri('/foo');
+        $this->assertEquals('/foo', Request::path());
     }
 
-    public function testRequestMethodPost()
+    /**
+     * @depends testProtocol
+     * @depends testServer
+     * @depends testPath
+     * @covers Request::url
+     */
+    public function testURL(): void
     {
-        $this->setRequestMethod('POST');
-        $this->assertTrue(Request::isPost());
-    }
+        $this->setServerNameAndPort('www.example.com', 443, true);
+        $this->setRequestUri('/do/it?now=1');
+        $this->assertEquals('https://www.example.com/do/it?now=1', Request::url());
 
-    public function testRequestMethodPut()
-    {
-        $this->setRequestMethod('PUT');
-        $this->assertTrue(Request::isPut());
+        $this->setServerNameAndPort('www.example.com', 8080);
+        $this->setRequestUri('/index.php');
+        $this->assertEquals('http://www.example.com:8080/index.php', Request::url());
     }
 
-    public function testRequestMethodDelete()
+    public function testScriptName(): void
     {
-        $this->setRequestMethod('DELETE');
-        $this->assertTrue(Request::isDelete());
+        $this->setScriptName('/index.php');
+        $this->assertEquals('/index.php', Request::script_name());
     }
 
-    public function testIsNotXhr()
+    /**
+     * @depends testPath
+     * @depends testScriptName
+     * @covers       Request::path_info
+     * @dataProvider PathProvider
+     */
+    public function testPathInfo(string $request_uri, string $script_name, string $expected): void
     {
-        $this->assertFalse(Request::isXhr());
-        $this->assertFalse(Request::isAjax());
+        $this->setScriptName($script_name);
+        $this->setRequestUri($request_uri);
+        $this->assertEquals($expected, Request::path_info());
     }
 
-    public function testIsXhr()
+    /**
+     * Data provider for testGetCompletePathInfo
+     *
+     * @return array[]
+     * @see RequestTest::testPathInfo
+     */
+    public function PathProvider(): array
     {
-        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'XmlHttpRequest';
-        $this->assertTrue(Request::isAjax());
-        $this->assertTrue(Request::isXhr());
+        return [
+            'Regular'                => ['/studip/dispatch.php/start', '/studip/dispatch.php', '/start'],
+            'With duplicate slash'   => ['/plugins.php/foo/bar//42', '/plugins.php', '/foo/bar//42'],
+            'With duplicate slashes' => ['/bogus.php/1/2//4///7///', '/bogus.php', '/1/2//4///7///'],
+            'Encoded'                => ['/dispatch.php/%62lu%62%62er', '/dispatch.php', '/blubber'],
+        ];
     }
 }