diff --git a/lib/classes/router/middleware/moodle_bootstrap_middleware.php b/lib/classes/router/middleware/moodle_bootstrap_middleware.php index 004cfc5f784..c6872d252bb 100644 --- a/lib/classes/router/middleware/moodle_bootstrap_middleware.php +++ b/lib/classes/router/middleware/moodle_bootstrap_middleware.php @@ -33,7 +33,7 @@ use Psr\Http\Server\RequestHandlerInterface; class moodle_bootstrap_middleware implements MiddlewareInterface { #[\Override] public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - global $PAGE; + global $CFG, $PAGE; if (str_contains($request->getUri(), route_loader_interface::ROUTE_GROUP_API)) { // @codeCoverageIgnoreStart @@ -58,7 +58,52 @@ class moodle_bootstrap_middleware implements MiddlewareInterface { $this->load_full_moodle(); } - $PAGE->set_url((string) $request->getUri()); + // Set the URL for the page. + // Normally in Moodle this is a largely hard-coded value with only the query string changing dynamically in the page. + // However, in this instance, we are generating the URL dynamically because we are the request terminator + // for a large number of requests at different endpoints. + + // In basic cases this will just work, but there are some edge cases to consider - specficially were the site + // is behind a reverse proxy and/or an SSL terminator. + // In these cases the URL we generate from the ServerRequestInterface may be the _terminated_ URL, + // and not the URL that was requested by the client. + + // We need to generate the URL that the client requested, not the URL that the server received. + $url = $request->getUri(); + + if (!empty($CFG->reverseproxy)) { + // This site is behind a reverse proxy. The requested URI may have a different: + // - scheme + // - host + // - port + // to the URL that the client requested. + + $url = $url + // Start by setting the scheme and host to the wwwroot. + ->withScheme(parse_url($CFG->wwwroot, PHP_URL_SCHEME)) + ->withHost(parse_url($CFG->wwwroot, PHP_URL_HOST)) + + // Update the URL to match the port of the wwwroot. + // While it is highly unlikely that a wwwroot includes an explicit port, we should still handle it. + ->withPort(parse_url($CFG->wwwroot, PHP_URL_PORT)); + + } + + if (!empty($CFG->sslproxy)) { + // This site is behind an ssl terminating proxy. The requested URI may have a different: + // - scheme + // - port + // to the URL that the client requested. + $url = $url + // The wwwroot must use the https scheme, but the terminating request may have been received using http. + ->withScheme('https') + + // Update the URL to match the port of the wwwroot. + // While it is highly unlikely that a wwwroot includes an explicit port, we should still handle it. + ->withPort(parse_url($CFG->wwwroot, PHP_URL_PORT)); + } + + $PAGE->set_url((string) $url); return $handler->handle($request); } diff --git a/lib/tests/router/middleware/moodle_bootstrap_middleware_test.php b/lib/tests/router/middleware/moodle_bootstrap_middleware_test.php index 078a5a295ba..323a3b9e911 100644 --- a/lib/tests/router/middleware/moodle_bootstrap_middleware_test.php +++ b/lib/tests/router/middleware/moodle_bootstrap_middleware_test.php @@ -17,8 +17,10 @@ namespace core\router\middleware; use core\di; +use core\url; use core\tests\route_testcase; use GuzzleHttp\Psr7\ServerRequest; +use Slim\Exception\HttpNotFoundException; /** * Tests for the Moodle Bootstrap middleware. @@ -30,21 +32,198 @@ use GuzzleHttp\Psr7\ServerRequest; * @covers \core\router\middleware\moodle_bootstrap_middleware */ final class moodle_bootstrap_middleware_test extends route_testcase { - public function test_set_page_to_uri(): void { - global $PAGE; + /** + * Test setting of the page URI based on the request URI. + * + * @dataProvider page_url_provider + * + * @param string $pattern The pattern to register with the app. + * @param string $basepath The basepath of the wwwroot. + * @param string $uri The URI to test. + * @param array $cfg The configuration to use. + * @param string|false $expected The expected page URI, or false if no page URI is expected. + */ + public function test_set_page_to_uri( + string $pattern, + string $basepath, + string $uri, + array $cfg, + string|false $expected + ): void { + global $CFG, $PAGE; + + $this->resetAfterTest(); + + foreach ($cfg as $key => $value) { + $CFG->{$key} = $value; + } + $app = $this->get_simple_app(); + $app->setBasePath($basepath); $app->add(di::get(moodle_bootstrap_middleware::class)); $app->addRoutingMiddleware(); - $app->map(['GET'], '/example', function ($request, $response) { - return $response; - }); + $app->map( + methods: ['GET'], + pattern: $pattern, + callable: fn ($request, $response) => $response, + ); // Handle the request. - $request = new ServerRequest('GET', '/example'); - $app->handle($request); + $request = new ServerRequest('GET', $uri); - $expect = new \moodle_url('/example'); - $this->assertEquals($expect->out(), $PAGE->url->out()); + if ($expected) { + $app->handle($request); + $expect = new url($expected); + $this->assertEquals($expect->out(), $PAGE->url->out()); + } else { + $this->expectException(HttpNotFoundException::class); + $app->handle($request); + } + } + + /** + * Data provider for test_set_page_to_uri. + * + * @return array + */ + public static function page_url_provider(): array { + return [ + 'A basic URI' => [ + 'pattern' => '/example', + 'basepath' => '', + 'uri' => '/example', + 'cfg' => [], + 'expected' => '/example', + ], + 'A basic URI including wwwroot' => [ + 'pattern' => '/example', + 'basepath' => '/example/path', + 'uri' => "https://example.com/example/path/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/example/path', + ], + 'expected' => '/example', + ], + 'A basic URI with query parameters' => [ + 'pattern' => '/example', + 'basepath' => '', + 'uri' => "https://example.com/example?foo=bar&baz=qux", + 'cfg' => [ + 'wwwroot' => 'https://example.com', + ], + 'expected' => "https://example.com/example?foo=bar&baz=qux", + ], + 'A request behind a terminating SSL proxy' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:443/moodle/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'sslproxy' => true, + ], + 'expected' => 'https://example.com/moodle/example', + ], + 'A request behind a terminating SSL proxy with query parameters' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:443/moodle/example?foo=bar&baz=qux", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'sslproxy' => true, + ], + 'expected' => 'https://example.com/moodle/example?foo=bar&baz=qux', + ], + 'A request behind a terminating SSL proxy and random port' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:1024/moodle/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'sslproxy' => true, + ], + 'expected' => 'https://example.com/moodle/example', + ], + 'A request behind a terminating SSL proxy and random port where the wwwroot has an explicit port' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:1024/moodle/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com:8443/moodle', + 'sslproxy' => true, + ], + 'expected' => 'https://example.com:8443/moodle/example', + ], + 'A request behind a terminating SSL proxy where the wwwroot has an explicity port and query parameters' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:1024/moodle/example?foo=bar&baz=qux", + 'cfg' => [ + 'wwwroot' => 'https://example.com:8443/moodle', + 'sslproxy' => true, + ], + 'expected' => 'https://example.com:8443/moodle/example?foo=bar&baz=qux', + ], + 'A request behind a SSL proxy proxy with different path is invalid' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://example.com:443/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'sslproxy' => true, + ], + 'expected' => false, + ], + 'A request behind a reverse proxy' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://172.30.10.101:443/moodle/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'reverseproxy' => true, + ], + 'expected' => 'https://example.com/moodle/example', + ], + 'A request behind a reverse proxy with query params' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://172.30.10.101:443/moodle/example?foo=bar&baz=qux", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'reverseproxy' => true, + ], + 'expected' => 'https://example.com/moodle/example?foo=bar&baz=qux', + ], + 'A request behind a reverse proxy with different path is invalid' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://172.30.10.101:443/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com/moodle', + 'reverseproxy' => true, + ], + 'expected' => false, + ], + 'A request behind a reverse proxy where the wwwroot has an explicit port' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://172.30.10.101:443/moodle/example", + 'cfg' => [ + 'wwwroot' => 'https://example.com:8443/moodle', + 'reverseproxy' => true, + ], + 'expected' => 'https://example.com:8443/moodle/example', + ], + 'A request behind a reverse proxy where the wwwroot has an explicit port and parameters' => [ + 'pattern' => '/example', + 'basepath' => '/moodle', + 'uri' => "http://172.30.10.101:443/moodle/example?foo=bar&baz=qux", + 'cfg' => [ + 'wwwroot' => 'https://example.com:8443/moodle', + 'reverseproxy' => true, + ], + 'expected' => 'https://example.com:8443/moodle/example?foo=bar&baz=qux', + ], + ]; } }