diff --git a/public/lib/classes/router/hook_callbacks.php b/public/lib/classes/router/hook_callbacks.php index 44bf7c2ae21..933cf8f30e0 100644 --- a/public/lib/classes/router/hook_callbacks.php +++ b/public/lib/classes/router/hook_callbacks.php @@ -45,5 +45,26 @@ class hook_callbacks { route_loader_interface::class, \DI\get(route_loader::class), ); + + // Until Moodle 6.0 we will allow the router to be unconfigured for backwards compatibility. + // To support legacy shims like /course/tags.php we need a custom route resolver which tries both. + // The following definition can be removed in Moodle 6.0. + // - \Slim\Interfaces\RouteResolverInterface + // - \Slim\Interfaces\RouteCollectorInterface + // - \Psr\Http\Message\ResponseFactoryInterface + // Along with the route_resolver class itself. + // TODO MDL-87637 Remove in Moodle 6.0. + $hook->add_definition( + \Slim\Interfaces\RouteResolverInterface::class, + \DI\get(route_resolver::class), + ); + $hook->add_definition( + \Slim\Interfaces\RouteCollectorInterface::class, + \DI\get(\Slim\Routing\RouteCollector::class), + ); + $hook->add_definition( + \Psr\Http\Message\ResponseFactoryInterface::class, + \DI\factory([\Slim\Factory\AppFactory::class, 'determineResponseFactory']), + ); } } diff --git a/public/lib/classes/router/route_resolver.php b/public/lib/classes/router/route_resolver.php new file mode 100644 index 00000000000..55d63380680 --- /dev/null +++ b/public/lib/classes/router/route_resolver.php @@ -0,0 +1,78 @@ +. + +namespace core\router; + +use Slim\Routing\RouteResolver; +use Slim\Routing\RoutingResults; + +/** + * Route Resolver that supports routing via r.php. + * + * Note: This is a temporary shim to support legacy shims until Moodle 6.0. + * After that, this class and its references can be removed. + * See MDL-87625 for more details. + * This class and it's DI definitions will be removed in Moodle 6.0. + * + * @package core + * @copyright Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class route_resolver extends RouteResolver { + #[\Override] + public function computeRoutingResults(string $uri, string $method): RoutingResults { + $originalresult = parent::computeRoutingResults($uri, $method); + if ($originalresult->getRouteStatus() === RoutingResults::FOUND) { + return $originalresult; + } + + // In some situations, like hardcoded shims, the request will look like /course/tags.php. + // When the router is not correctly configured (that ism it still has /r.php in the basepath), + // we need to adjust the URI to include /r.php so that the routing can work. + // The basepath includes the wwwroot and /r.php. We need to remove the /r.php from that to leave just the wwwroot. + // Then we can replae that with the basepath including /r.php. + $basepath = \core\di::get(\core\router::class)->basepath; + + if (str_ends_with($basepath, '/r.php')) { + if (str_starts_with($uri, $basepath)) { + // The router is in legacy mode and the requested URI already includes /r.php. + return $originalresult; + } else { + // The router is in legacy mode but the requested URI does not include /r.php, so add it. + $updateduri = $basepath . substr($uri, strlen($basepath) - strlen('/r.php')); + } + } else { + $suffixedbasepath = $basepath . '/r.php'; + if (str_starts_with($uri, $suffixedbasepath)) { + // The router is correctly configured but the requested URI includes /r.php, so remove it. + $updateduri = $basepath . substr($uri, strlen($suffixedbasepath)); + } else { + // The router is correctly configured and the requested URI does not include /r.php. + return $originalresult; + } + } + + // Try again with the updated URI. + $result = parent::computeRoutingResults($updateduri, $method); + + if ($result->getRouteStatus() === RoutingResults::FOUND) { + return $result; + } + + // Still not found, return the original result. + return $originalresult; + } +} diff --git a/public/lib/tests/router/route_resolver_test.php b/public/lib/tests/router/route_resolver_test.php new file mode 100644 index 00000000000..71dffabd016 --- /dev/null +++ b/public/lib/tests/router/route_resolver_test.php @@ -0,0 +1,149 @@ +. + +namespace core\router; + +/** + * Tests for the route resolver that supports routing with and without the r.php prefix. + * + * @package core + * @category test + * @copyright Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\PHPUnit\Framework\Attributes\CoversClass(route_resolver::class)] +final class route_resolver_test extends \core\tests\router\route_testcase { + /** + * Ensure that the computation of routing results can add or remove /r.php as needed. + */ + #[\PHPUnit\Framework\Attributes\DataProvider('compute_routing_results_provider')] + public function test_compute_routing_results( + string $basepath, + string $path, + string $requestedpath, + int $expectedresult, + ): void { + $this->add_route_to_route_loader( + self::class, + 'method_under_test', + '', + ); + + $router = $this->get_router($basepath); + $result = $router->get_app()->getRouteResolver()->computeRoutingResults( + $requestedpath, + 'GET', + ); + + $this->assertEquals($expectedresult, $result->getRouteStatus()); + } + + /** + * A test route handler. + */ + #[\core\router\route(path: '/method_under_test')] + public function method_under_test(): void { + // Do nothing. + } + + /** + * Data provider for the routing provider. + * + * @return \Generator, mixed, void> + */ + public static function compute_routing_results_provider(): \Generator { + $prefixes = ['/moodle', '']; + + foreach ($prefixes as $prefix) { + yield "Router in legacy mode, request without r.php, wwwroot is '{$prefix}'" => [ + "{$prefix}/r.php", + '/method_under_test', + "{$prefix}/method_under_test", + \Slim\Routing\RoutingResults::FOUND, + ]; + + yield "Router in legacy mode, request with r.php, wwwroot is '{$prefix}'" => [ + "{$prefix}/r.php", + '/method_under_test', + "{$prefix}/r.php/method_under_test", + \Slim\Routing\RoutingResults::FOUND, + ]; + + yield "Router in normal mode, request without r.php, wwwroot is '{$prefix}'" => [ + "{$prefix}", + '/method_under_test', + "{$prefix}/method_under_test", + \Slim\Routing\RoutingResults::FOUND, + ]; + + yield "Router in normal mode, request with r.php, wwwroot is '{$prefix}'" => [ + "{$prefix}", + '/method_under_test', + "{$prefix}/r.php/method_under_test", + \Slim\Routing\RoutingResults::FOUND, + ]; + + yield "Router in legacy mode, request unrelated path, wwwroot is '{$prefix}'" => [ + "{$prefix}/r.php", + '/method_under_test', + "{$prefix}/otherpath", + \Slim\Routing\RoutingResults::NOT_FOUND, + ]; + + yield "Router in normal mode, request unrelated path, wwwroot is '{$prefix}'" => [ + "{$prefix}", + '/method_under_test', + "{$prefix}/otherpath", + \Slim\Routing\RoutingResults::NOT_FOUND, + ]; + + yield "Router in legacy mode, request root path, wwwroot is '{$prefix}'" => [ + "{$prefix}/r.php", + '/method_under_test', + "{$prefix}/", + \Slim\Routing\RoutingResults::NOT_FOUND, + ]; + + yield "Router in normal mode, request root path, wwwroot is '{$prefix}'" => [ + "{$prefix}", + '/method_under_test', + "{$prefix}/", + \Slim\Routing\RoutingResults::NOT_FOUND, + ]; + + yield "Router in legacy mode, request r.php only, wwwroot is '{$prefix}'" => [ + "{$prefix}/r.php", + '/method_under_test', + "{$prefix}/r.php", + \Slim\Routing\RoutingResults::NOT_FOUND, + ]; + + yield "Router in normal mode, request r.php only, wwwroot is '{$prefix}'" => [ + "{$prefix}", + '/method_under_test', + "{$prefix}/r.php", + \Slim\Routing\RoutingResults::NOT_FOUND, + ]; + + yield "Router in legacy mode, request empty path, wwwroot is '{$prefix}'" => [ + "{$prefix}/r.php", + '/method_under_test', + "{$prefix}", + \Slim\Routing\RoutingResults::NOT_FOUND, + ]; + } + } +}