MDL-82565 core: Add support for routing of front-end pages

This is the beginning of the end for non-routed pages in Moodle and the
start of SEO-friendly page URLs.

A 'shim' is provided for backwards compatibility.
This commit is contained in:
Andrew Nicols
2025-03-26 21:20:58 +08:00
parent 89d76fdc25
commit 9137558cd5
22 changed files with 664 additions and 90 deletions
+2 -33
View File
@@ -20,37 +20,6 @@
*
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated since 5.0
*/
require_once("../config.php");
$courseid = required_param('courseid', PARAM_INT);
$PAGE->set_url('/course/admin.php', array('courseid'=>$courseid));
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
require_login($course);
$context = context_course::instance($course->id);
$PAGE->set_pagelayout('incourse');
if ($courseid == $SITE->id) {
$title = get_string('frontpagesettings');
$node = $PAGE->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
$PAGE->set_primary_active_tab('home');
} else {
$title = get_string('courseadministration');
$node = $PAGE->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
}
$PAGE->set_title($title);
$PAGE->set_heading($course->fullname);
$PAGE->navbar->add($title);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
if ($node) {
echo $OUTPUT->render_from_template('core/settings_link_page', ['node' => $node]);
}
echo $OUTPUT->footer();
require_once("../r.php");
@@ -0,0 +1,81 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\route\controller;
use core\router\route;
use core\router\require_login;
use navigation_node;
use Psr\Http\Message\ResponseInterface;
/**
* Course Management.
*
* @package core_course
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_management {
use \core\router\route_controller;
/**
* Administer a course.
*
* @param ResponseInterface $response
* @param \stdClass $course
* @return ResponseInterface
*/
#[route(
path: '/{course}/manage',
pathtypes: [
new \core\router\parameters\path_course(),
],
requirelogin: new require_login(
requirelogin: true,
courseattributename: 'course',
),
)]
public function administer_course(
ResponseInterface $response,
\stdClass $course,
): ResponseInterface {
global $PAGE, $SITE, $OUTPUT;
$PAGE->set_pagelayout('incourse');
if ($course->id == $SITE->id) {
$title = get_string('frontpagesettings');
$node = $PAGE->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
$PAGE->set_primary_active_tab('home');
} else {
$title = get_string('courseadministration');
$node = $PAGE->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
}
$PAGE->set_title($title);
$PAGE->set_heading($course->fullname);
$PAGE->navbar->add($title);
$response->getBody()->write($OUTPUT->header());
$response->getBody()->write($OUTPUT->heading($title));
if ($node) {
$response->getBody()->write($OUTPUT->render_from_template('core/settings_link_page', ['node' => $node]));
}
$response->getBody()->write($OUTPUT->footer());
return $response;
}
}
@@ -0,0 +1,67 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\route\shim;
use core\param;
use core\router\route;
use core\router\route_controller;
use core\router\schema\parameters\query_parameter;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* A shim for the course routes.
*
* @package core_course
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class course_routes {
use route_controller;
/**
* Shim /course/admin.php to the course management controller.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
#[route(
path: '/admin.php',
queryparams: [
new query_parameter(
name: 'courseid',
type: param::INT,
description: 'The course ID',
required: true,
),
],
)]
public function administer_course(
ServerRequestInterface $request,
ResponseInterface $response,
): ResponseInterface {
$params = $request->getQueryParams();
return self::redirect_to_callable(
$request,
$response,
[\core_course\route\controller\course_management::class, 'administer_course'],
pathparams: $params + ['course' => $params['courseid']],
excludeparams: ['courseid'],
);
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -32,7 +32,7 @@
* });
*/
import Cfg from 'core/config';
import * as Cfg from 'core/config';
import PendingPromise from './pending';
/**
+2 -1
View File
@@ -218,7 +218,8 @@ function behat_clean_init_config() {
'umaskpermissions', 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix',
'dboptions', 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword',
'proxybypass', 'pathtogs', 'pathtophp', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade',
'altcacheconfigpath', 'pathtounoconv', 'alternative_file_system_class', 'pathtopython'
'altcacheconfigpath', 'pathtounoconv', 'alternative_file_system_class', 'pathtopython',
'routerconfigured',
));
// Add extra allowed settings.
+8 -2
View File
@@ -4372,7 +4372,10 @@ EOD;
// We only add a list to the full settings menu if we didn't include every node in the short menu.
if ($skipped) {
$text = get_string('morenavigationlinks');
$url = new moodle_url('/course/admin.php', ['courseid' => $this->page->course->id]);
$url = \core\router\util::get_path_for_callable(
[\core_course\route\controller\course_management::class, 'administer_course'],
['course' => $this->page->course->id],
);
$link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
$menu->add_secondary_action($link);
}
@@ -4386,7 +4389,10 @@ EOD;
// We only add a list to the full settings menu if we didn't include every node in the short menu.
if ($skipped) {
$text = get_string('morenavigationlinks');
$url = new moodle_url('/course/admin.php', ['courseid' => $this->page->course->id]);
$url = \core\router\util::get_path_for_callable(
[\core_course\route\controller\course_management::class, 'administer_course'],
['course' => $this->page->course->id],
);
$link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
$menu->add_secondary_action($link);
}
+35 -7
View File
@@ -19,6 +19,8 @@ namespace core;
use core\output\routed_error_handler;
use core\router\middleware\cors_middleware;
use core\router\middleware\error_handling_middleware;
use core\router\middleware\moodle_api_authentication_middleware;
use core\router\middleware\moodle_authentication_middleware;
use core\router\middleware\moodle_bootstrap_middleware;
use core\router\middleware\moodle_route_attribute_middleware;
use core\router\middleware\uri_normalisation_middleware;
@@ -32,6 +34,7 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;
use Slim\Interfaces\RouteGroupInterface;
use Slim\Interfaces\RouteInterface;
/**
* Moodle Router.
@@ -99,19 +102,19 @@ class router {
);
// Replace occurrences of backslashes with forward slashes, especially on Windows.
$scriptfile = str_replace('\\', '/', $scriptfile);
$relativeroot = sprintf(
'%s%s',
$scriptroot,
$scriptfile,
);
// The server is not configured to rewrite unknown requests to automatically use the router.
$userphp = false;
if ($_SERVER && array_key_exists('REQUEST_URI', $_SERVER)) {
if (str_starts_with($_SERVER['REQUEST_URI'], $relativeroot)) {
$scriptroot .= '/r.php';
if (str_starts_with($_SERVER['REQUEST_URI'], "{$scriptroot}/r.php")) {
$userphp = true;
}
}
if ($CFG->routerconfigured !== true || $userphp) {
$scriptroot .= '/r.php';
}
return $scriptroot;
}
@@ -200,6 +203,8 @@ class router {
foreach ($routegroups as $name => $collection) {
match ($name) {
route_loader_interface::ROUTE_GROUP_API => $this->configure_api_route($collection),
route_loader_interface::ROUTE_GROUP_PAGE => $this->configure_standard_route($collection),
route_loader_interface::ROUTE_GROUP_SHIM => $this->configure_shim_route($collection),
default => null,
};
}
@@ -215,6 +220,29 @@ class router {
->add(di::get(error_handling_middleware::class))
// Add a Middleware to set the CORS headers for all REST Responses.
->add(di::get(cors_middleware::class))
->add(di::get(moodle_api_authentication_middleware::class))
->add(di::get(validation_middleware::class));
}
/**
* Configure the Standard page Route Middleware.
*
* @param RouteGroupInterface $group
*/
protected function configure_standard_route(RouteGroupInterface $group): void {
$group
->add(di::get(moodle_authentication_middleware::class))
->add(di::get(validation_middleware::class));
}
/**
* Configure the Shim Route Middleware.
*
* @param RouteGroupInterface $group
*/
protected function configure_shim_route(RouteGroupInterface $group): void {
$group
// Note: In the future we may wish to add a shim middleware to notify users of updated bookmarks.
->add(di::get(validation_middleware::class));
}
+7 -14
View File
@@ -31,11 +31,13 @@ abstract class abstract_route_loader {
*
* @param string $namespace The namespace to get the routes for
* @param callable $componentpathcallback A callback to get the component path for a class
* @param null|callable $filtercallback A callback to use to filter routes before they are added
* @return array[]
*/
protected function get_all_routes_in_namespace(
string $namespace,
callable $componentpathcallback,
?callable $filtercallback = null,
): array {
$routes = [];
@@ -43,6 +45,9 @@ abstract class abstract_route_loader {
$classes = \core_component::get_component_classes_in_namespace(namespace: $namespace);
foreach (array_keys($classes) as $classname) {
$classinfo = new \ReflectionClass($classname);
if ($filtercallback && !$filtercallback($classname)) {
continue;
}
$component = \core_component::get_component_from_classname($classname);
$componentpath = $componentpathcallback($component);
@@ -135,7 +140,7 @@ abstract class abstract_route_loader {
): ?route {
// Fetch the route attribute from the method.
// Each method can only have a single route attribute.
$routeattributes = $methodinfo->getAttributes(route::class);
$routeattributes = $methodinfo->getAttributes(route::class, \ReflectionAttribute::IS_INSTANCEOF);
if (empty($routeattributes)) {
return null;
}
@@ -166,19 +171,7 @@ abstract class abstract_route_loader {
protected function normalise_component_path(
string $component,
): string {
if ($component === 'core') {
return $component;
}
[$type, $subsystem] = \core_component::normalize_component($component);
if ($type === 'core') {
$component = $subsystem;
}
if ($component === null) {
$component = '';
}
return $component;
return util::normalise_component_path($component);
}
/**
@@ -0,0 +1,59 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\router\middleware;
use core\router\route;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Middleware to check Moodle authentication.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodle_api_authentication_middleware extends moodle_authentication_middleware {
#[\Override]
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
// Get the Moodle Route from the request. We need this to determine if login is required for this page.
$moodleroute = $request->getAttribute(route::class);
// Currently only Cookie authentication is supported.
if ($moodleroute && $moodleroute->requirelogin) {
$requirements = $moodleroute->requirelogin;
if ($courseattributename = $requirements->get_course_attribute_name()) {
$courseorid = $request->getAttribute($courseattributename, null);
}
if ($requirements->should_require_course_login()) {
require_course_login(
$courseorid,
$requirements->should_autologin_guest(),
);
} else if ($requirements->should_require_login()) {
require_login(
$courseorid,
$requirements->should_autologin_guest(),
);
}
}
return $handler->handle($request);
}
}
@@ -0,0 +1,59 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\router\middleware;
use core\router\route;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Middleware to check Moodle authentication.
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodle_authentication_middleware implements MiddlewareInterface {
#[\Override]
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
// Get the Moodle Route from the request. We need this to determine if login is required for this page.
$moodleroute = $request->getAttribute(route::class);
if ($moodleroute && $moodleroute->requirelogin) {
$requirements = $moodleroute->requirelogin;
if ($courseattributename = $requirements->get_course_attribute_name()) {
$courseorid = $request->getAttribute($courseattributename, null);
}
if ($requirements->should_require_course_login()) {
require_course_login(
$courseorid,
$requirements->should_autologin_guest(),
);
} else if ($requirements->should_require_login()) {
require_login(
$courseorid,
$requirements->should_autologin_guest(),
);
}
}
return $handler->handle($request);
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\router;
/**
* Login metadata requirements for routes.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class require_login {
/**
* Create a new instance of the metadata class.
*
* @param bool $requirelogin Whether login is required
* @param bool $requirecourselogin Whether a course login is required
* @param mixed $courseattributename The name of the route attribute that the course object can be found in
* @param bool $autologinguest Whether to automatically log in as guest
* @throws \InvalidArgumentException
*/
public function __construct(
/** @var bool Whether login is required */
public bool $requirelogin = false,
/** @var bool Whether a course login is required */
public bool $requirecourselogin = false,
/** @var bool The name of the route attribute that the course object can be found in */
protected ?string $courseattributename = null,
/** @var bool Whether to automatically log in as guest */
public bool $autologinguest = true,
) {
if ($requirelogin && $requirecourselogin) {
throw new \InvalidArgumentException('Cannot require login and course login at the same time');
}
}
/**
* Get the course attribute name.
*
* @return string
*/
public function get_course_attribute_name(): string {
return $this->courseattributename;
}
/**
* Whether course login is required.
*
* @return bool
*/
public function should_require_course_login(): bool {
return $this->requirecourselogin;
}
/**
* Whether login is required.
*
* @return bool
*/
public function should_require_login(): bool {
return $this->requirelogin;
}
/**
* Whether automatic guest login is enabled.
*
* @return bool
*/
public function should_autologin_guest(): bool {
return $this->autologinguest;
}
}
+6
View File
@@ -117,6 +117,12 @@ class route {
/** @var bool Whether to abort after configuration */
public readonly bool $abortafterconfig = false,
/** @var null|array Whether to require login or not */
public readonly ?require_login $requirelogin = null,
/** @var string[] The list of scopes required to access this page */
public readonly ?array $scopes = null,
// Note. We do not make use of these extras.
// These allow us to add additional arguments in future versions, whilst allowing plugins to use this version.
...$extra,
+4 -16
View File
@@ -17,7 +17,6 @@
namespace core\router;
use moodle_url;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -32,17 +31,6 @@ use Psr\Http\Message\ServerRequestInterface;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait route_controller {
/**
* Constructor for Route Controllers.
*
* @param ContainerInterface $container
*/
public function __construct(
/** @var ContainerInterface The DI Container */
protected ContainerInterface $container,
) {
}
/**
* Generate a Page Not Found result.
*
@@ -51,7 +39,7 @@ trait route_controller {
* @return ResponseInterface
* @throws \Slim\Exception\HttpNotFoundException
*/
protected function page_not_found(
public static function page_not_found(
ServerRequestInterface $request,
ResponseInterface $response,
): ResponseInterface {
@@ -65,7 +53,7 @@ trait route_controller {
* @param string|moodle_url $url
* @return ResponseInterface
*/
protected function redirect(
public static function redirect(
ResponseInterface $response,
string|moodle_url $url,
): ResponseInterface {
@@ -85,7 +73,7 @@ trait route_controller {
* @param null|array $excludeparams A list of any parameters to remove the URI during the redirect
* @return ResponseInterface
*/
protected function redirect_to_callable(
public static function redirect_to_callable(
ServerRequestInterface $request,
ResponseInterface $response,
array|callable|string $callable,
@@ -111,7 +99,7 @@ trait route_controller {
// Remove any params.
$url->remove_params($excludeparams);
return $this->redirect($response, $url);
return self::redirect($response, $url);
}
/**
+94
View File
@@ -18,6 +18,7 @@ namespace core\router;
use Slim\App;
use Slim\Interfaces\RouteGroupInterface;
use Slim\Interfaces\RouteInterface;
use Slim\Routing\RouteCollectorProxy;
/**
@@ -32,6 +33,8 @@ class route_loader extends abstract_route_loader implements route_loader_interfa
public function configure_routes(App $app): array {
return [
route_loader_interface::ROUTE_GROUP_API => $this->configure_api_routes($app, route_loader_interface::ROUTE_GROUP_API),
route_loader_interface::ROUTE_GROUP_PAGE => $this->configure_standard_routes($app),
route_loader_interface::ROUTE_GROUP_SHIM => $this->configure_shim_routes($app),
];
}
@@ -59,6 +62,36 @@ class route_loader extends abstract_route_loader implements route_loader_interfa
});
}
/**
* Configure all standard routes.
*
* @param App $app
* @return RouteGroupInterface
*/
protected function configure_standard_routes(App $app): RouteGroupInterface {
return $app->group('', function (RouteCollectorProxy $group): void {
foreach ($this->get_all_standard_routes() as $moodleroute) {
$slimroute = $group->map(...$moodleroute);
$this->set_route_name_for_callable($slimroute, $moodleroute['callable']);
}
});
}
/**
* Configure all route shims.
*
* @param App $app
* @return RouteGroupInterface
*/
protected function configure_shim_routes(App $app): RouteGroupInterface {
return $app->group('', function (RouteCollectorProxy $group): void {
foreach ($this->get_all_shimmed_routes() as $moodleroute) {
$slimroute = $group->map(...$moodleroute);
$this->set_route_name_for_callable($slimroute, $moodleroute['callable']);
}
});
}
/**
* Fetch all API routes.
*
@@ -80,4 +113,65 @@ class route_loader extends abstract_route_loader implements route_loader_interfa
return $routes;
}
/**
* Fetch all shimmed routes.
*
* Shimmed routes are routes that are not part of the standard route namespace but allow backwards compatibility with
* pages which have been moved to the new routing system.
*
* Note: This method caches results in MUC.
*
* @return array|bool|mixed
*/
protected function get_all_shimmed_routes(): array {
$cache = \cache::make('core', 'routes');
if (!($cachedata = $cache->get('shimmed_routes'))) {
$cachedata = $this->get_all_routes_in_namespace(
namespace: 'route\shim',
componentpathcallback: function (string $component): string {
global $CFG;
if ($component === 'core') {
// The core component is a special case.
// It can place routes _anywhere_ in the codebase.
return '';
}
// Use the component directory path listed in \core\componnet.
return substr(
\core_component::get_component_directory($component),
strlen($CFG->dirroot) + 1,
);
},
);
$cache->set('shimmed_routes', $cachedata);
}
return $cachedata;
}
/**
* Fetch all standard routes.
*
* Note: This method caches results in MUC.
*
* @return array[]
*/
protected function get_all_standard_routes(): array {
$cache = \cache::make('core', 'routes');
if (!($cachedata = $cache->get('standard_routes'))) {
$cachedata = $this->get_all_routes_in_namespace(
namespace: 'route\controller',
componentpathcallback: $this->normalise_component_path(...),
filtercallback: fn(string $classname) => !str_contains($classname, '\\shim\\'),
);
$cache->set('standard_routes', $cachedata);
}
return $cachedata;
}
}
@@ -31,6 +31,12 @@ interface route_loader_interface {
/** @var string The route path prefix to use for API calls */
public const ROUTE_GROUP_API = '/api/rest/v2';
/** @var string The route path prefix to use for API calls */
public const ROUTE_GROUP_SHIM = 'shim';
/** @var string The route path prefix to use for API calls */
public const ROUTE_GROUP_PAGE = '/';
/**
* Configure all routes for the Application.
*
+3 -11
View File
@@ -23,6 +23,7 @@ use core\router\route;
use core\router\route_loader_interface;
use core\router\schema\objects\type_base;
use core\router\schema\response\response;
use core\router\util;
use core\url;
use stdClass;
@@ -263,17 +264,8 @@ class specification implements
route $route,
): self {
// Compile the final path, complete with component prefix.
[$type, $subsystem] = \core_component::normalize_component($component);
if ($type === 'core') {
if ($subsystem) {
$path = "/{$subsystem}";
} else {
$path = "/core";
}
} else {
$path = "/{$component}";
}
$path = "/";
$path .= util::normalise_component_path($component);
$path .= $route->get_path();
// Helper to add the path to the specification.
+24
View File
@@ -227,4 +227,28 @@ class util {
return $methodroute;
}
/**
* Normalise the component for use as part of the path.
*
* If the component is a subsystem, the `core_` prefix will be removed.
* If the component is 'core', it will be kept.
* All other components will use their frankenstyle name.
*
* @param string $component
* @return string
*/
public static function normalise_component_path(
string $component,
): string {
if ($component === 'core') {
return $component;
}
[$type, $subsystem] = \core\component::normalize_component($component);
if ($type === 'core') {
return str_replace('core_', '', $subsystem);
}
return $component ?? '';
}
}
+1 -1
View File
@@ -637,7 +637,7 @@ class url {
): self {
global $CFG;
if (!$CFG->routerconfigured) {
if ($CFG->routerconfigured !== true) {
$path = '/r.php/' . ltrim($path, '/');
}
$url = new self($path, $params, $anchor);
+5
View File
@@ -41,6 +41,11 @@ final class util_test extends route_testcase {
* Test getting the path for a callable.
*/
public function test_get_path_for_callable(): void {
global $CFG;
$this->resetAfterTest();
$CFG->routerconfigured = true;
self::load_fixture('core', 'router/route_on_class.php');
$this->add_route_to_route_loader(
+112 -2
View File
@@ -106,8 +106,8 @@ final class router_test extends route_testcase {
}
public static function basepath_provider(): \Iterator {
yield 'Domain' => ['http://example.com', ''];
yield 'Subdirectory' => ['http://example.com/moodle', '/moodle'];
yield 'Domain' => ['http://example.com', '/r.php'];
yield 'Subdirectory' => ['http://example.com/moodle', '/moodle/r.php'];
}
public function test_basepath_guessed_rphp(): void {
@@ -119,4 +119,114 @@ final class router_test extends route_testcase {
$this->assertEquals($wwwroot->get_path(), $router->basepath);
}
/**
* Test that the basepath is correctly guessed when the router is configured.
*
* @param string $wwwroot The wwwroot to use.
* @param bool|null $configured The value of $CFG->routerconfigured.
* @param string $requestedpath The path that was requested.
* @param string $expected The expected basepath.
*/
#[\PHPUnit\Framework\Attributes\DataProvider('router_configured_basepath_provider')]
public function test_basepath_guessed_rphp_configuration_provided(
string $wwwroot,
?bool $configured,
string $requestedpath,
string $expected,
): void {
global $CFG;
$this->resetAfterTest();
$CFG->wwwroot = $wwwroot;
$CFG->routerconfigured = $configured;
$_SERVER['SCRIPT_FILENAME'] = "{$CFG->dirroot}/r.php";
$_SERVER['REQUEST_URI'] = $requestedpath;
$router = di::get(router::class);
$this->assertEquals($expected, $router->basepath);
}
/**
* Data provider for test_basepath_guessed_rphp_configuration_provided.
*
* @return \Generator<string, array<bool|string|null>, mixed, void>
*/
public static function router_configured_basepath_provider(): \Iterator {
global $CFG;
yield 'Root domain, Not configured, accessed via r.php' => [
'http://example.com',
null,
'/r.php/example',
'/r.php',
];
yield 'Root domain, Configured true, accessed via r.php' => [
'http://example.com',
true,
"/r.php/example",
'/r.php',
];
yield 'Root domain, Configured false, accessed via r.php' => [
'http://example.com',
false,
'/r.php/example',
'/r.php',
];
yield 'Sub directory, Not configured, accessed via r.php' => [
'http://example.com/moodle',
null,
'/moodle/r.php/example',
'/moodle/r.php',
];
yield 'Sub directory, Configured true, accessed via r.php' => [
'http://example.com/moodle',
true,
'/moodle/r.php/example',
'/moodle/r.php',
];
yield 'Sub directory, Configured false, accessed via r.php' => [
'http://example.com/moodle',
false,
'/moodle/r.php/example',
'/moodle/r.php',
];
yield 'Root domain, Not configured, accessed without r.php' => [
'http://example.com',
null,
'/example',
'/r.php',
];
yield 'Root domain, Configured true, accessed without r.php' => [
'http://example.com',
true,
'/example',
'',
];
yield 'Root domain, Configured false, accessed without r.php' => [
'http://example.com',
false,
'/example',
'/r.php',
];
yield 'Sub directory, Not configured, accessed without r.php' => [
'http://example.com/moodle',
null,
'/example',
'/moodle/r.php',
];
yield 'Sub directory, Configured true, accessed without r.php' => [
'http://example.com/moodle',
true,
'/example',
'/moodle',
];
yield 'Sub directory, Configured false, accessed without r.php' => [
'http://example.com/moodle',
false,
'/example',
'/moodle/r.php',
];
}
}