This commit is contained in:
Ilya Tregubov
2024-09-20 10:37:05 +08:00
2 changed files with 235 additions and 11 deletions
@@ -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);
}
@@ -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',
],
];
}
}