This commit is contained in:
Jun Pataleta
2025-03-27 10:45:20 +08:00
34 changed files with 1364 additions and 286 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");
@@ -14,40 +14,37 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Edit course tags form
*
* @package core_course
* @copyright 2015 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/formslib.php');
require_once($CFG->libdir . '/formslib.php');
/**
* Edit course tags form
* Edit course tags form.
*
* @package core_course
* @copyright 2015 Marina Glancy
* @copyright Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class coursetags_form extends moodleform {
class tags_form extends \moodleform {
#[\Override]
public function definition(): void {
$mform = $this->_form;
/**
* Form definition
*/
public function definition() {
$mform = $this->_form;
$mform->addElement('tags', 'tags', get_string('tags'),
array('itemtype' => 'course', 'component' => 'core'));
$mform->addElement(
'tags',
'tags',
get_string('tags'),
[
'itemtype' => 'course',
'component' => 'core',
],
);
$mform->addElement('hidden', 'id', null);
$mform->setType('id', PARAM_INT);
$this->add_action_buttons();
}
}
@@ -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,105 @@
<?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\exception\moodle_exception;
use core\router\parameters\query_returnurl;
use core\router\route;
use core\router\require_login;
use core\router\util;
use core_course\form\tags_form;
use core_tag_tag;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Tag management for courses.
*
* @package core_course
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tags_controller {
use \core\router\route_controller;
/**
* Administer course tags.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param \stdClass $course
* @param \core\context\course $coursecontext
* @return ResponseInterface
*/
#[route(
path: '/{course}/tags',
method: ['GET', 'POST'],
pathtypes: [
new \core\router\parameters\path_course(),
],
queryparams: [
new query_returnurl(),
],
requirelogin: new require_login(
requirelogin: true,
courseattributename: 'course',
),
)]
public function administer_tags(
ServerRequestInterface $request,
ResponseInterface $response,
\stdClass $course,
\core\context\course $coursecontext,
): ResponseInterface {
global $CFG, $OUTPUT, $PAGE;
if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
throw new moodle_exception('coursehidden', '', $CFG->wwwroot .'/');
}
require_capability('moodle/course:tag', $coursecontext);
$PAGE->set_course($course);
$PAGE->set_pagelayout('incourse');
$PAGE->set_url(util::get_path_for_callable([self::class, 'administer_tags'], [
'course' => $course->id,
]));
$PAGE->set_title(get_string('coursetags', 'tag'));
$PAGE->set_heading($course->fullname);
$form = new tags_form();
$data = [
'id' => $course->id,
'tags' => core_tag_tag::get_item_tags_array('core', 'course', $course->id),
];
$form->set_data($data);
$redirecturl = $this->get_param($request, 'returnurl') ?? course_get_url($course);
if ($form->is_cancelled()) {
return util::redirect($response, $redirecturl);
} else if ($data = $form->get_data()) {
core_tag_tag::set_item_tags('core', 'course', $course->id, $coursecontext, $data->tags);
return util::redirect($response, $redirecturl);
}
$response->getBody()->write($OUTPUT->header());
$response->getBody()->write($OUTPUT->heading(get_string('coursetags', 'tag')));
$response->getBody()->write($form->render());
$response->getBody()->write($OUTPUT->footer());
return $response;
}
}
+105
View File
@@ -0,0 +1,105 @@
<?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'],
);
}
/**
* Shim /course/tags.php to the course tag management controller.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
#[route(
path: '/tags.php',
queryparams: [
new query_parameter(
name: 'id',
type: param::INT,
description: 'The course ID',
required: true,
),
new query_parameter(
name: 'returnurl',
type: param::LOCALURL,
description: 'The return URL',
required: false,
),
],
)]
public function administer_tags(
ServerRequestInterface $request,
ResponseInterface $response,
): ResponseInterface {
$params = $request->getQueryParams();
return self::redirect_to_callable(
$request,
$response,
[\core_course\route\controller\tags_controller::class, 'administer_tags'],
pathparams: $params + ['course' => $params['id']],
excludeparams: ['id'],
);
}
}
+2 -44
View File
@@ -20,48 +20,6 @@
* @package core_course
* @copyright 2015 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated since 5.0
*/
require_once("../config.php");
require_once($CFG->dirroot . '/course/tags_form.php');
$id = required_param('id', PARAM_INT); // Course id.
$returnurl = optional_param('return', null, PARAM_LOCALURL);
$course = get_course($id);
require_login();
// Check capabilities but do not call require_login($course) - the user does not have to be enrolled.
$context = context_course::instance($course->id);
if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
throw new \moodle_exception('coursehidden', '', $CFG->wwwroot .'/');
}
require_capability('moodle/course:tag', $context);
if (!core_tag_tag::is_enabled('core', 'course')) {
throw new \moodle_exception('tagsaredisabled', 'tag');
}
$PAGE->set_course($course);
$PAGE->set_pagelayout('incourse');
$PAGE->set_url('/course/tags.php', array('id' => $course->id));
$PAGE->set_title(get_string('coursetags', 'tag'));
$PAGE->set_heading($course->fullname);
$form = new coursetags_form();
$data = array('id' => $course->id, 'tags' => core_tag_tag::get_item_tags_array('core', 'course', $course->id));
$form->set_data($data);
$redirecturl = $returnurl ? new moodle_url($returnurl) : course_get_url($course);
if ($form->is_cancelled()) {
redirect($redirecturl);
} else if ($data = $form->get_data()) {
core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
redirect($redirecturl);
}
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('coursetags', 'tag'));
$form->display();
echo $OUTPUT->footer();
require_once("../r.php");
+2 -75
View File
@@ -23,80 +23,7 @@
* ErrorDocument 404 /error/index.php
*
* @package core
* @copyright 2020 Brendan Heywood <[email protected]>
* @copyright Brendan Heywood <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require('../config.php'); // phpcs:ignore
// Until we have a more robust routing api in place this is a very simple
// and clean way to handle arbitrary urls without a php extension.
if ($ME === '/.well-known/change-password') {
redirect(new moodle_url('/login/change_password.php'));
}
$context = context_system::instance();
$title = get_string('pagenotexisttitle', 'error');
$PAGE->set_url('/error/index.php');
$PAGE->set_context($context);
$PAGE->set_title($title);
$PAGE->set_heading($title);
$PAGE->navbar->add($title);
// This allows the webserver to dictate wether the http status should remain
// what it would have been, or force it to be a 404. Under other conditions
// it could most often be a 403, 405 or a 50x error.
$code = optional_param('code', 0, PARAM_INT);
if ($code == 404) {
header("HTTP/1.0 404 Not Found");
}
$canmessage = has_capability('moodle/site:senderrormessage', $context);
$supportuser = core_user::get_support_user();
// We can only message support if both the user has the capability
// and the support user is a real user.
if ($canmessage) {
$canmessage = core_user::is_real_user($supportuser->id);
}
$mform = new \core\form\error_feedback($CFG->wwwroot . '/error/index.php');
if ($data = $mform->get_data()) {
if (!$canmessage) {
redirect($CFG->wwwroot);
}
// Send the message and redirect.
$message = new \core\message\message();
$message->courseid = SITEID;
$message->component = 'moodle';
$message->name = 'errors';
$message->userfrom = $USER;
$message->userto = core_user::get_support_user();
$message->subject = 'Error: '. $data->referer .' -> '. $data->requested;
$message->fullmessage = $data->text;
$message->fullmessageformat = FORMAT_PLAIN;
$message->fullmessagehtml = '';
$message->smallmessage = '';
$message->contexturl = $data->requested;
message_send($message);
redirect($CFG->wwwroot, get_string('sendmessagesent', 'error', $data->requested), 5);
exit;
}
echo $OUTPUT->header();
echo $OUTPUT->notification(get_string('pagenotexist', 'error', s($ME)), 'error');
echo $OUTPUT->supportemail(['class' => 'text-center d-block mb-3 fw-bold']);
if ($canmessage) {
echo \html_writer::tag('h4', get_string('sendmessage', 'error'));
$mform->display();
} else {
echo $OUTPUT->continue_button($CFG->wwwroot);
}
echo $OUTPUT->footer();
require_once("../r.php");
+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.
+1 -5
View File
@@ -37,10 +37,7 @@ require_once($CFG->libdir.'/formslib.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class error_feedback extends moodleform {
/**
* Error form definition
*/
#[\Override]
public function definition() {
global $CFG;
@@ -55,4 +52,3 @@ class error_feedback extends moodleform {
$mform->addElement('submit', 'submitbutton', get_string('sendmessage', 'error'));
}
}
+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);
}
@@ -0,0 +1,178 @@
<?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\route\controller;
use core\form\error_feedback;
use core\router;
use core\router\route;
use core\router\schema\parameters\query_parameter;
use core\router\util;
use core\url;
use core_user;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Page Not Found Controller.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class page_not_found_controller {
use \core\router\route_controller;
/**
* Constructor for the page not found handler.
*
* @param \core\router $router The router.
*/
public function __construct(
/** @var router The routing engine */
private router $router,
) {
}
/**
* Administer a course.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
#[route(
path: '/error',
method: ['GET', 'POST'],
queryparams: [
new query_parameter(
name: 'code',
type: \core\param::INT,
default: 404,
),
],
)]
public function page_not_found_handler(
ServerRequestInterface $request,
): ResponseInterface {
global $CFG, $PAGE, $OUTPUT, $ME;
$context = \core\context\system::instance();
$title = get_string('pagenotexisttitle', 'error');
$PAGE->set_url('/error');
$PAGE->set_context($context);
$PAGE->set_title($title);
$PAGE->set_heading($title);
$PAGE->navbar->add($title);
// This allows the webserver to dictate wether the http status should remain
// what it would have been, or force it to be a 404. Under other conditions
// it could most often be a 403, 405 or a 50x error.
$code = $request->getQueryParams()['code'] ?? 404;
$response = $this->router->get_app()->getResponseFactory()->createResponse($code);
$mform = $this->get_message_form($response);
if ($mform) {
if ($this->process_message_form($mform)) {
// The form was submitted. Redirect to the home page.
return util::redirect(
$response,
new url('/'),
);
}
// Form not submitted. We need to set the referer and request path because the URI may be different on submission.
$mform->set_data([
'referer' => $request->getHeaderLine('Referer'),
'requested' => $request->getUri()->getPath(),
]);
}
$response->getBody()->write($OUTPUT->header());
$response->getBody()->write($OUTPUT->notification(get_string('pagenotexist', 'error', s($ME)), 'error'));
$response->getBody()->write($OUTPUT->supportemail(['class' => 'text-center d-block mb-3 fw-bold']));
if ($mform) {
$response->getBody()->write(\html_writer::tag('h4', get_string('sendmessage', 'error')));
$response->getBody()->write($mform->render());
} else {
$response->getBody()->write($OUTPUT->continue_button($CFG->wwwroot));
}
$response->getBody()->write($OUTPUT->footer());
return $response;
}
/**
* Get the message form, or null if it should not be displayed.
*
* @param \Psr\Http\Message\ResponseInterface $response
* @return error_feedback|null
*/
protected function get_message_form(
ResponseInterface $response,
): ?\moodleform {
$canmessage = has_capability('moodle/site:senderrormessage', \core\context\system::instance());
$supportuser = core_user::get_support_user();
// We can only message support if both the user has the capability
// and the support user is a real user.
$canmessage = $canmessage && core_user::is_real_user($supportuser->id);
if (!$canmessage) {
return null;
}
return new error_feedback(util::get_path_for_callable([self::class, 'page_not_found_handler'], [], [])->out());
}
/**
* Process the message form.
*
* If the form was submitted, send the message and return a redirect response.
*
* @param error_feedback $mform
* @return bool
*/
protected function process_message_form(
error_feedback $mform,
): bool {
global $CFG, $USER;
if ($data = $mform->get_data()) {
// Send the message and redirect.
$message = new \core\message\message();
$message->courseid = SITEID;
$message->component = 'moodle';
$message->name = 'errors';
$message->userfrom = $USER;
$message->userto = core_user::get_support_user();
$message->subject = 'Error: ' . $data->referer . ' -> ' . $data->requested;
$message->fullmessage = $data->text;
$message->fullmessageformat = FORMAT_PLAIN;
$message->fullmessagehtml = '';
$message->smallmessage = '';
$message->contexturl = $data->requested;
message_send($message);
\core\notification::success(get_string('sendmessagesent', 'error', $data->requested));
return true;
}
return false;
}
}
@@ -0,0 +1,61 @@
<?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\route\shim;
use core\param;
use core\router\route;
use core\router\schema\parameters\query_parameter;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Redirect requests for /error/index.php and /error to the page_not_found_controller.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class error_controller {
/**
* Shim /error/index.php to /core/error
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
#[route(
path: '/error[/index.php]',
queryparams: [
new query_parameter(
name: 'code',
type: param::INT,
description: 'The HTTP Code',
),
],
)]
public function administer_course(
ServerRequestInterface $request,
ResponseInterface $response,
): ResponseInterface {
$params = $request->getQueryParams();
return \core\router\util::redirect_to_callable(
$request,
$response,
[\core\route\controller\page_not_found_controller::class, 'page_not_found_handler'],
);
}
}
+66 -11
View File
@@ -16,9 +16,10 @@
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;
@@ -31,7 +32,10 @@ use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;
use Slim\Exception\HttpForbiddenException;
use Slim\Exception\HttpNotFoundException;
use Slim\Interfaces\RouteGroupInterface;
use Slim\Middleware\ErrorMiddleware;
/**
* Moodle Router.
@@ -99,19 +103,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;
}
@@ -186,10 +190,36 @@ class router {
// This must be done before the Routing Middleware to ensure that the route is matched correctly.
$this->app->add(di::get(uri_normalisation_middleware::class));
// Add the Error Handling Middleware to the App.
$this->add_error_handler_middleware();
}
/**
* Add the Error Handling Middleware to the RouteGroup.
*/
protected function add_error_handler_middleware(): void {
// Add the Error Handling Middleware and configure it to show Moodle Errors for HTML pages.
$errormiddleware = $this->app->addErrorMiddleware(true, true, true);
$errorhandler = $errormiddleware->getDefaultErrorHandler();
$errorhandler->registerErrorRenderer('text/html', routed_error_handler::class);
$errormiddleware = new ErrorMiddleware(
$this->app->getCallableResolver(),
$this->app->getResponseFactory(),
displayErrorDetails: true,
logErrors: true,
logErrorDetails: true,
);
// Set a custom error handler for the HttpNotFoundException and HttpForbiddenException.
// We route these to a custom error handler to ensure that the error is displayed with a feedback form.
$errormiddleware->setErrorHandler(
[
HttpNotFoundException::class,
HttpForbiddenException::class,
],
new router\error_handler($this->app),
);
$errormiddleware->getDefaultErrorHandler()->registerErrorRenderer('text/html', router\error_renderer::class);
$this->app->add($errormiddleware);
}
/**
@@ -200,6 +230,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 +247,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);
}
/**
+58
View File
@@ -0,0 +1,58 @@
<?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;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;
use Slim\Handlers\ErrorHandler;
/**
* An Eerror Handler implementation for Moodle which is aware of the REST API.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class error_handler extends ErrorHandler {
/**
* Construct a new Error Handler.
*
* @param \Slim\App $app
*/
public function __construct(
App $app,
) {
parent::__construct(
$app->getCallableResolver(),
$app->getResponseFactory(),
);
$this->registerErrorRenderer('text/html', error_renderer::class);
}
#[\Override]
protected function determineContentType(ServerRequestInterface $request): ?string {
// For anything hitting /rest/api/v2 we will default to JSON.
$restbase = (new \core\url('/rest/api/v2/'))->get_path();
if (substr($request->getUri()->getPath(), 0, strlen($restbase)) === $restbase) {
return 'application/json';
}
// Fall back to the default behaviour of using the Accept header.
return parent::determineContentType($request);
}
}
@@ -14,8 +14,9 @@
// 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\output;
namespace core\router;
use Slim\Exception\HttpNotFoundException;
use Slim\Interfaces\ErrorRendererInterface;
use Throwable;
@@ -28,7 +29,7 @@ use Throwable;
* @copyright Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class routed_error_handler implements ErrorRendererInterface {
class error_renderer implements ErrorRendererInterface {
#[\Override]
public function __invoke(Throwable $exception, bool $displayErrorDetails): string {
// @codeCoverageIgnoreStart
@@ -38,8 +39,19 @@ class routed_error_handler implements ErrorRendererInterface {
}
// @codeCoverageIgnoreEnd
if ($exception instanceof HttpNotFoundException) {
// This is a 404 error.
$controller = \core\di::get(\core\route\controller\page_not_found_controller::class);
return $controller->page_not_found_handler(
$exception->getRequest(),
)->getBody();
}
if ($whoops = get_whoops()) {
$whoops->sendHttpCode($exception->getCode());
if ($exception instanceof \Slim\Exception\HttpException) {
$whoops->sendHttpCode($exception->getCode());
}
$whoops->handleException($exception);
} else {
default_exception_handler($exception);
@@ -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,58 @@
<?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\parameters;
use core\param;
use core\router\schema\parameters\mapped_property_parameter;
use core\router\schema\referenced_object;
use Psr\Http\Message\ServerRequestInterface;
/**
* A return URL parameter referenced in the query parameters.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class query_returnurl extends \core\router\schema\parameters\query_parameter implements
mapped_property_parameter,
referenced_object
{
/**
* Create a new query_returnurl parameter.
*
* @param string $name The name of the parameter to use for the return URL
* @param mixed ...$extra Additional arguments
*/
public function __construct(
string $name = 'returnurl',
...$extra,
) {
$extra['name'] = $name;
$extra['type'] = param::LOCALURL;
parent::__construct(...$extra);
}
#[\Override]
public function add_attributes_for_parameter_value(
ServerRequestInterface $request,
string $value,
): ServerRequestInterface {
return $request->withAttribute($this->name, new \core\url($value));
}
}
+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,
+11 -36
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,11 +39,11 @@ 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 {
throw new \Slim\Exception\HttpNotFoundException($request);
return util::throw_page_not_found($request, $response);
}
/**
@@ -65,13 +53,11 @@ trait route_controller {
* @param string|moodle_url $url
* @return ResponseInterface
*/
protected function redirect(
public static function redirect(
ResponseInterface $response,
string|moodle_url $url,
): ResponseInterface {
return $response
->withStatus(302)
->withHeader('Location', (string) $url);
return util::redirect($response, $url);
}
/**
@@ -85,7 +71,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,
@@ -93,25 +79,14 @@ trait route_controller {
?array $queryparams = null,
?array $excludeparams = null,
): ResponseInterface {
// Provide defaults for the path and query params if not specified.
if ($pathparams === null) {
$pathparams = $request->getQueryParams();
}
if ($queryparams === null) {
$queryparams = $request->getQueryParams();
}
// Generate a URI from the callable and the parameters.
$url = util::get_path_for_callable(
return util::redirect_to_callable(
$request,
$response,
$callable,
$pathparams ?? [],
$queryparams ?? [],
$pathparams,
$queryparams,
$excludeparams,
);
// Remove any params.
$url->remove_params($excludeparams);
return $this->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.
+100 -24
View File
@@ -16,8 +16,9 @@
namespace core\router;
use moodle_url;
use core\url;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Routing\RouteContext;
@@ -31,52 +32,103 @@ use Slim\Routing\RouteContext;
* - helpers to fetch the \core\router\route instance
*
* @package core
* @copyright 2024 Andrew Lyons <[email protected]>
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class util {
/**
* Redirect to the specified URL, carrying all parameters across too.
*
* @param string|moodle_url $path
* @param string|url $path
* @param array $excludeparams Any parameters to exclude from the query params
* @codeCoverageIgnore
*/
public static function redirect_with_params(
string|moodle_url $path,
string|url $path,
array $excludeparams = [],
): never {
$params = $_GET;
$url = new moodle_url(
$url = new url(
$path,
$params,
);
$url->remove_params($excludeparams);
$url->remove_params(...$excludeparams);
redirect($url);
}
/**
* Redirect to the route at the callable supplied.
* Redirect to the requested callable.
*
* @param callable|array|string $callable
* @param array $params Any parameters to include in the path
* @codeCoverageIgnore
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param array|callable|string $callable
* @param null|array $pathparams
* @param null|array $queryparams
* @param null|array $excludeparams A list of any parameters to remove the URI during the redirect
* @return ResponseInterface
*/
public static function redirect_to_callable(
callable|array|string $callable,
array $params = [],
): never {
$params = array_merge(
$_GET,
$params,
ServerRequestInterface $request,
ResponseInterface $response,
array|callable|string $callable,
?array $pathparams = null,
?array $queryparams = null,
?array $excludeparams = null,
): ResponseInterface {
// Provide defaults for the path and query params if not specified.
if ($pathparams === null) {
$pathparams = $request->getQueryParams();
}
if ($queryparams === null) {
$queryparams = $request->getQueryParams();
}
// Generate a URI from the callable and the parameters.
$url = self::get_path_for_callable(
$callable,
$pathparams ?? [],
$queryparams ?? [],
);
$url = self::get_path_for_callable($callable, $params, $params);
// Remove any params.
$url->remove_params($excludeparams);
redirect($url);
return self::redirect($response, $url);
}
/**
* Generate a Page Not Found result.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
* @throws \Slim\Exception\HttpNotFoundException
*/
public static function throw_page_not_found(
ServerRequestInterface $request,
ResponseInterface $response,
): ResponseInterface {
throw new \Slim\Exception\HttpNotFoundException($request);
}
/**
* Redirect to a URL.
*
* @param ResponseInterface $response
* @param string|url $url
* @return ResponseInterface
*/
public static function redirect(
ResponseInterface $response,
string|url $url,
): ResponseInterface {
return $response
->withStatus(302)
->withHeader('Location', (string) $url);
}
/**
* Get the route name for the specified callable.
*
@@ -103,13 +155,13 @@ class util {
* @param string|array|callable $callable the Callable to get the URI for
* @param array $params Any parameters to include in the path
* @param array $queryparams Any parameters to include in the query string
* @return moodle_url
* @return url
*/
public static function get_path_for_callable(
string|array|callable $callable,
array $params,
array $queryparams,
): moodle_url {
array $params = [],
array $queryparams = [],
): url {
global $CFG;
$router = \core\di::get(\core\router::class);
@@ -118,7 +170,7 @@ class util {
$routename = self::get_route_name_for_callable($callable);
return new moodle_url(
return new url(
url: $parser->fullUrlFor(
new Uri($CFG->wwwroot),
$routename,
@@ -149,7 +201,7 @@ class util {
}
/**
* Get the instance of the \route\router\route attribute for the specified callable if one is available.
* Get the instance of the \core\router\route attribute for the specified callable if one is available.
*
* @param callable|array|string $callable
* @return null|route The route if one was found.
@@ -227,4 +279,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);
+4 -1
View File
@@ -4859,7 +4859,10 @@ class settings_navigation extends navigation_node {
}
if (!$adminoptions->update && $adminoptions->tags) {
$url = new moodle_url('/course/tags.php', array('id' => $course->id));
$url = \core\router\util::get_path_for_callable([
\core_course\route\controller\tags_controller::class,
'administer_tags',
], ['course' => $course->id]);
$coursenode->add(get_string('coursetags', 'tag'), $url, self::TYPE_SETTING, null, 'coursetags', new pix_icon('i/settings', ''));
$coursenode->get('coursetags')->set_force_into_more_menu();
}
@@ -0,0 +1,47 @@
<?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\parameters;
use core\tests\router\route_testcase;
use core\url;
use GuzzleHttp\Psr7\ServerRequest;
/**
* Tests for the query_returnurl parameter.
*
* @package core
* @category test
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[\PHPUnit\Framework\Attributes\CoversClass(query_returnurl::class)]
final class query_returnurl_test extends route_testcase {
public function test_returnurl_specified(): void {
$this->resetAfterTest();
$param = new query_returnurl();
$request = $param->add_attributes_for_parameter_value(
new ServerRequest('GET', '/course/edit'),
'/course/view',
);
$this->assertInstanceOf(url::class, $request->getAttribute('returnurl'));
$this->assertTrue(
$request->getAttribute('returnurl')->compare(new url('/course/view')),
);
}
}
+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',
];
}
}