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:
+2
-33
@@ -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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -32,7 +32,7 @@
|
||||
* });
|
||||
*/
|
||||
|
||||
import Cfg from 'core/config';
|
||||
import * as Cfg from 'core/config';
|
||||
import PendingPromise from './pending';
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -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.
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user