diff --git a/.github/workflows/config-template.php b/.github/workflows/config-template.php
index 2227832ee3b..ae49d583954 100644
--- a/.github/workflows/config-template.php
+++ b/.github/workflows/config-template.php
@@ -58,6 +58,8 @@ $CFG->pathtophp = getenv('pathtophp');
$CFG->phpunit_dataroot = realpath(dirname(__DIR__)) . '/phpunitdata';
$CFG->phpunit_prefix = 't_';
+$CFG->routerconfigured = true;
+
define('TEST_EXTERNAL_FILES_HTTP_URL', 'http://localhost:8080');
define('TEST_EXTERNAL_FILES_HTTPS_URL', 'http://localhost:8080');
diff --git a/.upgradenotes/MDL-87648-2026011405492190.yml b/.upgradenotes/MDL-87648-2026011405492190.yml
new file mode 100644
index 00000000000..5e6b109aa42
--- /dev/null
+++ b/.upgradenotes/MDL-87648-2026011405492190.yml
@@ -0,0 +1,11 @@
+issueNumber: MDL-87648
+notes:
+ core:
+ - message: >
+ The Checks API now supports multiple results being returned for a single
+ instance of `\core\check\check`.
+
+
+ To support this a new `get_results(): array;` method has been created
+ which returns an array of `\core\check\result` objects.
+ type: improved
diff --git a/admin/cli/checks.php b/admin/cli/checks.php
index 40deb4d5535..fc434c6de48 100644
--- a/admin/cli/checks.php
+++ b/admin/cli/checks.php
@@ -110,46 +110,49 @@ $exitcode = $exitcodes[result::OK];
foreach ($checks as $check) {
$ref = $check->get_ref();
- $result = $check->get_result();
- $status = $result->get_status();
- $checkexitcode = $exitcodes[$status];
+ foreach ($check->get_results() as $result) {
+ $status = $result->get_status();
+ $checkexitcode = $exitcodes[$status];
- // Summary is treated as html.
- $summary = $result->get_summary();
- $summary = html_to_text($summary, 60, false);
+ // Summary is treated as html.
+ $summary = $result->get_summary();
+ $summary = html_to_text($summary, 60, false);
- if ($checkexitcode > $exitcode) {
- $exitcode = $checkexitcode;
- $header = $exitlabel[$status] . ': ' . $check->get_name() . " (" . $check->get_ref() . ")\n";
- }
+ if ($checkexitcode > $exitcode) {
+ $exitcode = $checkexitcode;
+ $header = $exitlabel[$status] . ': ' . $check->get_name() . " (" . $check->get_ref() . ")\n";
+ }
- if (empty($messages[$status])) {
- $messages[$status] = $result;
- }
+ if (empty($messages[$status])) {
+ $messages[$status] = $result;
+ }
- $len = strlen(get_string('status' . $status));
+ $len = strlen(get_string('status' . $status));
- if ($options['verbose'] ||
- $status == result::WARNING ||
- $status == result::CRITICAL ||
- $status == result::ERROR) {
+ if (
+ $options['verbose']
+ || $status == result::WARNING
+ || $status == result::CRITICAL
+ || $status == result::ERROR
+ ) {
+ $output .= sprintf(
+ $format,
+ $OUTPUT->check_result($result),
+ sprintf('%s (%s)', $check->get_name(), $ref),
+ );
- $output .= sprintf(
- $format,
- $OUTPUT->check_result($result),
- sprintf('%s (%s)', $check->get_name(), $ref)
- );
+ $summary = str_replace("\n", "\n" . $prefix . ' ', $summary);
+ $output .= sprintf($format, '', ' ' . $summary);
+ $output .= sprintf($format, '', ' ' . html_to_text($result->get_details(), width: 0, dolinks: false));
- $summary = str_replace("\n", "\n" . $prefix . ' ', $summary);
- $output .= sprintf( $format, '', ' ' . $summary);
-
- if ($options['verbose']) {
- $actionlink = $check->get_action_link();
- if ($actionlink) {
- $output .= sprintf( $format, '', ' ' . $actionlink->url);
+ if ($options['verbose']) {
+ $actionlink = $check->get_action_link();
+ if ($actionlink) {
+ $output .= sprintf($format, '', ' ' . $actionlink->url);
+ }
+ $output .= sprintf($format, '', '');
}
- $output .= sprintf( $format, '', '');
}
}
}
@@ -168,4 +171,3 @@ if ($output) {
// NRPE shell exit code.
exit($exitcode);
-
diff --git a/public/admin/environment.xml b/public/admin/environment.xml
index 30a56c7abe5..4593b6e3a20 100644
--- a/public/admin/environment.xml
+++ b/public/admin/environment.xml
@@ -5233,6 +5233,7 @@
+
diff --git a/public/lang/en/admin.php b/public/lang/en/admin.php
index 88b7969d33e..c3f4aab139d 100644
--- a/public/lang/en/admin.php
+++ b/public/lang/en/admin.php
@@ -126,6 +126,7 @@ $string['cannotuninstall'] = '{$a} can not be uninstalled.';
$string['categoryemail'] = 'Email';
$string['cfgwwwrootslashwarning'] = '$CFG->wwwroot is defined incorrectly in the config.php file. It includes a \'/\' character at the end which must be removed.';
$string['cfgwwwrootwarning'] = '$CFG->wwwroot is defined incorrectly in the config.php file. It should match the URL you are using to access this page.';
+$string['check_router'] = 'Router configuration';
$string['checkupgradepending'] = 'Upgrade';
$string['cleanup'] = 'Cleanup';
$string['clianswerno'] = 'n';
@@ -1171,6 +1172,21 @@ $string['requiremodintro'] = 'Require activity description';
$string['requiremodintro_desc'] = 'If enabled, users will be forced to enter a description for each activity.';
$string['required'] = 'Required';
$string['requires'] = 'Requires';
+$string['routerconfigurationset'] = 'Moodle has been informed that the web server is correctly configured.';
+$string['routerconfigureddetails'] = 'The router correctly serves {$a->url} with a {$a->expectedstatuscode} ("{$a->expectedstatuscodetitle}") response.';
+$string['routerconfiguredok'] = 'The router appears to be configured correctly.';
+$string['routerconfiguredwithissues'] = 'The router is not correctly configured.';
+$string['routerconfiguredwithissuesdetail'] = 'The router is not correctly configured - Issues were detected in {$a->count} checks.';
+$string['routerdocs'] = 'Router documentation';
+$string['routerexpectedgot'] = 'Expected {$a->expectedstatuscode} ("{$a->expectedstatuscodetitle}") and received {$a->statuscode} ("{$a->statuscodetitle}").';
+$string['routerfailapipage'] = 'An API route did not respond with the expected response.';
+$string['routerfailmissingpage'] = 'The missing page handler did not respond to the test URL correctly.';
+$string['routerfailpage'] = 'A standard page controller could not be loaded correctly. The router may not be configured properly.';
+$string['routerfailroutershim'] = 'A routed page whose path ends in ".php" did not response correctly.';
+$string['routerfailshim'] = 'A shimmed page did not response correctly.';
+$string['routernotconfigured'] = 'The router is not configured.';
+$string['routernotconfigureddetail'] = 'The router has not been configured correctly. Please see the documentation for more information on how to configure it correctly.';
+$string['routernotconfigureddetailwithurl'] = 'The following URL did not return the expected status code: {$a->url}.';
$string['purgecaches'] = 'Purge all caches';
$string['purgecachesconfirm'] = 'Moodle can cache themes, JavaScript, language strings, filtered text, RSS feeds and other pieces of calculated data. Purging caches will delete data from the server and force browsers to re-fetch data, so you can be sure you are seeing the most up-to-date values produced by the current code. There is no danger in purging caches, but your site may appear slower for a while until the server and clients calculate new information and cache it.';
$string['purgecachesfinished'] = 'All caches were purged.';
diff --git a/public/lib/classes/check/check.php b/public/lib/classes/check/check.php
index 6fdb79f4f92..129626c36a6 100644
--- a/public/lib/classes/check/check.php
+++ b/public/lib/classes/check/check.php
@@ -128,5 +128,12 @@ abstract class check {
*/
abstract public function get_result(): result;
+ /**
+ * Return all results for this check
+ *
+ * @return result[]
+ */
+ public function get_results(): array {
+ return [$this->get_result()];
+ }
}
-
diff --git a/public/lib/classes/check/environment/router.php b/public/lib/classes/check/environment/router.php
new file mode 100644
index 00000000000..75f87f8b8f6
--- /dev/null
+++ b/public/lib/classes/check/environment/router.php
@@ -0,0 +1,183 @@
+.
+
+namespace core\check\environment;
+
+use core\check\check;
+use core\check\result;
+use core\output\action_link;
+use GuzzleHttp\HandlerStack;
+
+/**
+ * Checks status of router by making test requests to shimmed and API pages.
+ *
+ * @package core
+ * @category check
+ * @copyright Andrew Lyons
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class router extends check {
+ #[\Override]
+ public function get_name(): string {
+ return get_string('check_router', 'admin');
+ }
+
+ /**
+ * Get the list of tests
+ *
+ * @return \Generator
+ */
+ protected function get_tests(): \Generator {
+ yield [
+ 'url' => '/core/check/controller/test',
+ 'expectedcode' => 200,
+ 'expectedcodetitle' => "OK",
+ 'failfeedbackstr' => 'routerfailpage',
+ 'bookmarkanchor' => 'routerfailpage',
+ ];
+ yield [
+ 'url' => '/api/rest/v2/openapi.json',
+ 'expectedcode' => 200,
+ 'expectedcodetitle' => "OK",
+ 'failfeedbackstr' => 'routerfailapipage',
+ 'bookmarkanchor' => 'routerfailapipage',
+ ];
+ yield [
+ 'url' => '/not/a/valid/request',
+ 'expectedcode' => 404,
+ 'expectedcodetitle' => "Not Found",
+ 'failfeedbackstr' => 'routerfailmissingpage',
+ 'bookmarkanchor' => 'routerfailmissingpage',
+ ];
+ yield [
+ 'url' => \core\router\util::get_path_for_callable([\core\route\shim\test_controller::class, 'real_file_shim']),
+ 'expectedcode' => 302,
+ 'expectedcodetitle' => "Found",
+ 'failfeedbackstr' => 'routerfailshim',
+ 'bookmarkanchor' => 'routerfailshim',
+ ];
+ yield [
+ 'url' => \core\router\util::get_path_for_callable([\core\route\shim\test_controller::class, 'nofile_shim']),
+ 'expectedcode' => 302,
+ 'expectedcodetitle' => "Found",
+ 'failfeedbackstr' => 'routerfailroutershim',
+ 'bookmarkanchor' => 'routerfailroutershim',
+ ];
+ }
+
+ #[\Override]
+ public function get_results(): array {
+ global $CFG;
+
+ $results = [];
+
+ if (empty($CFG->routerconfigured)) {
+ $result = new result(
+ result::ERROR,
+ get_string('routernotconfigured', 'admin'),
+ get_string('routernotconfigureddetail', 'admin', [
+ 'docs' => get_docs_url("Configuring_the_Router#routernotconfigured"),
+ ]),
+ );
+
+ return [$result];
+ } else {
+ $results[] = new result(
+ result::OK,
+ get_string('routerconfigurationset', 'admin'),
+ );
+ }
+
+ // The router is marked as configured. Check if it actually works though.
+ $client = \core\di::get(\core\http_client::class);
+
+ $clientoptions = [
+ 'http_errors' => false,
+ 'allow_redirects' => false,
+ // Override the Handler Stack to ensure that no caching is used, an that the security helper is not applied.
+ 'handler' => HandlerStack::create(),
+ ];
+
+ foreach ($this->get_tests() as $test) {
+ if ($test['url'] instanceof \core\url) {
+ $fullurl = $test['url']->out(false);
+ } else {
+ $fullurl = $CFG->wwwroot . $test['url'];
+ }
+
+ try {
+ $response = $client->get($fullurl, $clientoptions);
+ $code = $response->getStatusCode();
+ $codetitle = $response->getReasonPhrase();
+ } catch (\GuzzleHttp\Exception\GuzzleException $e) {
+ $code = $e->getCode();
+ $codetitle = $e->getMessage();
+ }
+
+ $resultprops = [
+ 'url' => $fullurl,
+ 'docs' => get_docs_url("Configuring_the_Router#{$test['bookmarkanchor']}"),
+ 'expectedstatuscode' => $test['expectedcode'],
+ 'expectedstatuscodetitle' => $test['expectedcodetitle'],
+ 'statuscode' => $code,
+ 'statuscodetitle' => $codetitle,
+ ];
+ if ($code === $test['expectedcode']) {
+ $results[] = new result(
+ result::OK,
+ get_string('routerconfigureddetails', 'admin', $resultprops),
+ );
+ } else {
+ $expectedgot = get_string('routerexpectedgot', 'admin', $resultprops);
+ $results[] = new result(
+ result::ERROR,
+ get_string($test['failfeedbackstr'], 'admin', $resultprops),
+ get_string('routernotconfigureddetailwithurl', 'admin', $resultprops) . " {$expectedgot}",
+ new action_link(
+ new \core\url(get_docs_url("Configuring_the_Router#{$test['bookmarkanchor']}")),
+ get_string('routerdocs', 'admin'),
+ ),
+ );
+ }
+ }
+
+ return $results;
+ }
+
+ #[\Override]
+ public function get_result(): result {
+ // Aggregate the results.
+ $failed = array_filter(
+ $this->get_results(),
+ fn ($result) => $result->get_status() !== result::OK,
+ );
+
+ if (empty($failed)) {
+ // Nothing failed. Return the OK result.
+ return new result(
+ result::OK,
+ get_string('routerconfiguredok', 'admin'),
+ );
+ }
+
+ // Some form of failure. Return the generic failure result.
+ return new result(
+ result::ERROR,
+ get_string('routerconfiguredwithissues', 'admin'),
+ get_string('routerconfiguredwithissuesdetail', 'admin', ['count' => count($failed)]),
+ );
+ }
+}
diff --git a/public/lib/classes/check/manager.php b/public/lib/classes/check/manager.php
index fceea2ff6f9..4b04fc0e03b 100644
--- a/public/lib/classes/check/manager.php
+++ b/public/lib/classes/check/manager.php
@@ -97,6 +97,7 @@ class manager {
new environment\environment(),
new environment\upgradecheck(),
new environment\antivirus(),
+ new environment\router(),
];
// Any plugin can add status checks to this report by implementing a callback
diff --git a/public/lib/classes/check/result.php b/public/lib/classes/check/result.php
index 41b3596500d..2da3b57a2f4 100644
--- a/public/lib/classes/check/result.php
+++ b/public/lib/classes/check/result.php
@@ -16,6 +16,8 @@
namespace core\check;
+use core\output\action_link;
+
/**
* A check object returns a result object
*
@@ -29,7 +31,6 @@ namespace core\check;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class result implements \renderable {
-
/**
* This is used to notify if a check does not apply.
*
@@ -99,7 +100,14 @@ class result implements \renderable {
* This may be a large amount of preformatted html text, possibly describing all the
* different states and actions to address them.
*/
- protected $details = '';
+ protected string $details = '';
+
+ /**
+ * @var null|action_link An action link which is more specific than the general check link, if relvant
+ *
+ * If no specific check link is specified, then the check link will be used.
+ */
+ protected ?action_link $actionlink = null;
/**
* Constructor
@@ -107,11 +115,18 @@ class result implements \renderable {
* @param string $status code
* @param string $summary a 1 liner summary
* @param string $details as a html chunk
+ * @param ?action_link $details An action link which is more specific than the general check link, if relevant
*/
- public function __construct($status, $summary, $details = '') {
+ public function __construct(
+ string $status,
+ string $summary,
+ string $details = '',
+ ?action_link $actionlink = null,
+ ) {
$this->status = $status;
$this->summary = $summary;
$this->details = $details;
+ $this->actionlink = $actionlink;
}
/**
@@ -166,5 +181,13 @@ class result implements \renderable {
public function get_template_name(): string {
return 'core/check/result';
}
-}
+ /**
+ * Get an action link if a more specific one was specified.
+ *
+ * @return null|action_link
+ */
+ public function get_action_link(): ?action_link {
+ return $this->actionlink;
+ }
+}
diff --git a/public/lib/classes/check/table.php b/public/lib/classes/check/table.php
index afe79c60282..669f7d06029 100644
--- a/public/lib/classes/check/table.php
+++ b/public/lib/classes/check/table.php
@@ -14,26 +14,19 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
+namespace core\check;
+
+use core\output\html_writer;
+
/**
- * A table of check results
+ * A table of check results.
*
* @package core
* @category check
* @copyright 2020 Brendan Heywood
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-namespace core\check;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * A table of check results
- *
- * @copyright 2020 Brendan Heywood
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class table implements \renderable {
-
+class table implements \core\output\renderable {
/**
* @var \moodle_url $url
*/
@@ -49,6 +42,13 @@ class table implements \renderable {
*/
public $detail = '';
+ /**
+ * The name of the check that was requested.
+ *
+ * @var string
+ */
+ protected string $checkname = '';
+
/**
* @var array $checks shown in this table
*/
@@ -62,7 +62,6 @@ class table implements \renderable {
* @param string $detail check to focus on
*/
public function __construct($type, $url, $detail = '') {
-
// We may need a bit more memory and this may take a long time to process.
\raise_memory_limit(MEMORY_EXTRA);
\core_php_time_limit::raise();
@@ -70,6 +69,7 @@ class table implements \renderable {
$this->type = $type;
$this->url = $url;
$this->checks = \core\check\manager::get_checks($type);
+ $this->checkname = $detail;
if ($detail) {
$this->checks = array_filter($this->checks, function($check) use ($detail) {
@@ -84,63 +84,91 @@ class table implements \renderable {
/**
* Render a table of checks
*
- * @param renderer $output to use
+ * @param \core\output\renderer $output to use
* @return string html output
*/
public function render($output) {
+ $html = '';
- $table = new \html_table();
+ $table = new \core_table\output\html_table();
$table->data = [];
- $table->head = [
- get_string('status'),
- get_string('check'),
- get_string('summary'),
- get_string('action'),
- ];
- $table->colclasses = [
- 'rightalign status',
- 'leftalign check',
- 'leftalign summary',
- 'leftalign action',
- ];
+ $table->head = [get_string('status')];
+ $table->colclasses = ['rightalign status'];
+
+ if (empty($this->checkname)) {
+ $table->head[] = get_string('check');
+ $table->colclasses[] = 'leftalign check';
+ } else {
+ $html .= html_writer::tag('h3', $this->detail->get_name());
+ }
+
+ $table->head[] = get_string('summary');
+ $table->colclasses[] = 'leftalign summary';
+ $table->head[] = get_string('action');
+ $table->colclasses[] = 'leftalign action';
+
$table->id = $this->type . 'reporttable';
$table->attributes = ['class' => 'admintable ' . $this->type . 'report table generaltable'];
+ $fails = [];
foreach ($this->checks as $check) {
$ref = $check->get_ref();
- $result = $check->get_result();
- $component = $check->get_component();
- $actionlink = $check->get_action_link();
$link = new \moodle_url($this->url, ['detail' => $ref]);
- $row = [];
- $row[] = $output->check_result($result);
- $row[] = $output->action_link($link, $check->get_name());
+ $results = empty($this->checkname)
+ ? [$check->get_result()]
+ : $check->get_results();
- $row[] = $result->get_summary()
- . '
'
- . \html_writer::start_tag('small')
- . $output->action_link($link, get_string('moreinfo'))
- . \html_writer::end_tag('small');
- if ($actionlink) {
- $row[] = $output->render($actionlink);
- } else {
- $row[] = '';
+ foreach ($results as $result) {
+ $row = [];
+ if ($result->get_status() !== result::OK) {
+ $fails[] = $result;
+ }
+ $row[] = $output->check_result($result);
+
+ if (empty($this->checkname)) {
+ $row[] = $output->action_link($link, $check->get_name());
+ }
+
+ $row[] = $result->get_summary()
+ . '
'
+ . \html_writer::start_tag('small')
+ . $output->action_link($link, get_string('moreinfo'))
+ . \html_writer::end_tag('small');
+
+ $actionlink = $result->get_action_link() ?? $check->get_action_link();
+ if ($actionlink) {
+ $row[] = $output->render($actionlink);
+ } else {
+ $row[] = '';
+ }
+
+ $table->data[] = $row;
}
-
- $table->data[] = $row;
}
- $html = \html_writer::table($table);
+ $html .= \html_writer::table($table);
- if ($this->detail && $result) {
- $details = $result->get_details();
+ $details = array_filter(array_map(
+ fn ($result) => $result->get_details(),
+ $fails,
+ ));
+ if (count($details) > 0) {
+ $html .= $output->heading(get_string('details'), 3);
- if (!empty($details)) {
- $html .= $output->heading(get_string('details'), 3);
- $html .= $output->box($details, 'generalbox boxwidthnormal boxaligncenter');
+ if (count($details) === 1) {
+ $result = reset($fails);
+ $html .= $output->box($result->get_details(), 'generalbox boxwidthnormal boxaligncenter');
+ } else {
+ $html .= html_writer::start_tag('ul');
+ foreach ($details as $detail) {
+ $html .= html_writer::tag('li', $detail);
+ }
+ $html .= html_writer::end_tag('ul');
}
+ }
+ if ($this->detail) {
$html .= $output->continue_button($this->url);
}
diff --git a/public/lib/classes/environment.php b/public/lib/classes/environment.php
index c05e756ef4c..403dc7357f4 100644
--- a/public/lib/classes/environment.php
+++ b/public/lib/classes/environment.php
@@ -160,4 +160,23 @@ class environment {
return !empty($CFG->debugdeveloper);
}
+
+ /**
+ * Ensure that the Router is correctly configured.
+ *
+ * @param \environment_results $result
+ * @return \environment_results|null
+ */
+ public static function check_router_configuration(\environment_results $result): ?\environment_results {
+ global $CFG;
+
+ if (empty($CFG->routerconfigured)) {
+ // The router has not been marked as configured.
+ $result->setInfo('Router not configured');
+ $result->setFeedbackStr('routernotconfigured');
+ return $result;
+ }
+
+ return null;
+ }
}
diff --git a/public/lib/classes/route/controller/test_controller.php b/public/lib/classes/route/controller/test_controller.php
new file mode 100644
index 00000000000..f0b972c7d1b
--- /dev/null
+++ b/public/lib/classes/route/controller/test_controller.php
@@ -0,0 +1,45 @@
+.
+
+namespace core\route\controller;
+
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Test controller to provide a route for testing purposes.
+ *
+ * This set of routes is primarily intended for use with the Environment Checks
+ * to help administrators ensure that the Routing system is correctly configured.
+ *
+ * @package core
+ * @copyright Andrew Lyons
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_controller {
+ /**
+ * Test controller route.
+ *
+ * @param ResponseInterface $response
+ * @return ResponseInterface
+ */
+ #[\core\router\route(
+ path: '/check/controller/test',
+ )]
+ public function test_action(ResponseInterface $response): ResponseInterface {
+ $response->getBody()->write('Test controller response');
+ return $response;
+ }
+}
diff --git a/public/lib/classes/route/shim/test_controller.php b/public/lib/classes/route/shim/test_controller.php
new file mode 100644
index 00000000000..c820ae60e89
--- /dev/null
+++ b/public/lib/classes/route/shim/test_controller.php
@@ -0,0 +1,75 @@
+.
+
+namespace core\route\shim;
+
+use core\router\route_controller;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * An example shim route to use for testing.
+ *
+ * This set of routes is primarily intended for use with the Environment Checks
+ * to help administrators ensure that the Routing system is correctly configured.
+ *
+ * @package core
+ * @copyright Andrew Lyons
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_controller {
+ use route_controller;
+
+ /**
+ * An example shim route action for a file which manually shims the request.
+ *
+ * @param ResponseInterface $response
+ * @return ResponseInterface
+ */
+ #[\core\router\route(
+ path: '/lib/exampleshimroute.php',
+ )]
+ public function real_file_shim(
+ RequestInterface $request,
+ ResponseInterface $response,
+ ): ResponseInterface {
+ return self::redirect_to_callable(
+ $request,
+ $response,
+ [\core\route\controller\test_controller::class, 'test_action'],
+ );
+ }
+
+ /**
+ * An example shim route action for a file which no longer exists.
+ *
+ * @param ResponseInterface $response
+ * @return ResponseInterface
+ */
+ #[\core\router\route(
+ path: '/lib/exampleshimroute2.php',
+ )]
+ public function nofile_shim(
+ RequestInterface $request,
+ ResponseInterface $response,
+ ): ResponseInterface {
+ return self::redirect_to_callable(
+ $request,
+ $response,
+ [\core\route\controller\test_controller::class, 'test_action'],
+ );
+ }
+}
diff --git a/public/lib/exampleshimroute.php b/public/lib/exampleshimroute.php
new file mode 100644
index 00000000000..bc48c9353ba
--- /dev/null
+++ b/public/lib/exampleshimroute.php
@@ -0,0 +1,30 @@
+.
+
+/**
+ * An example shim route to use for testing.
+ *
+ * This page is primarily intended for use with the Environment Checks
+ * to help administrators ensure that the Routing system is correctly configured.
+ *
+ * This file simulates a legacy file which has not yet been removed, but which exists in the Routing configuration.
+ *
+ * @package core
+ * @copyright Andrew Lyons
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once("../r.php");
diff --git a/public/lib/phpunit/bootstrap.php b/public/lib/phpunit/bootstrap.php
index 8916ad69765..48dfd049174 100644
--- a/public/lib/phpunit/bootstrap.php
+++ b/public/lib/phpunit/bootstrap.php
@@ -201,6 +201,7 @@ $allowed = ['wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', '
'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword', 'proxybypass',
'altcacheconfigpath', 'pathtogs', 'pathtophp', 'pathtodu', 'aspellpath', 'pathtodot',
'pathtounoconv', 'alternative_file_system_class', 'pathtopython',
+ 'routerconfigured',
];
$productioncfg = (array) $CFG;
$CFG = new stdClass();
diff --git a/public/lib/tests/router_test.php b/public/lib/tests/router_test.php
index d7e8f089c0f..ea3be2793c6 100644
--- a/public/lib/tests/router_test.php
+++ b/public/lib/tests/router_test.php
@@ -93,12 +93,14 @@ final class router_test extends route_testcase {
*/
public function test_basepath(
string $wwwroot,
+ bool $configured,
string $expected,
): void {
global $CFG;
$this->resetAfterTest();
$CFG->wwwroot = $wwwroot;
+ $CFG->routerconfigured = $configured;
$router = di::get(router::class);
@@ -106,8 +108,10 @@ final class router_test extends route_testcase {
}
public static function basepath_provider(): \Iterator {
- yield 'Domain' => ['http://example.com', '/r.php'];
- yield 'Subdirectory' => ['http://example.com/moodle', '/moodle/r.php'];
+ yield 'Domain Router not configured' => ['http://example.com', false, '/r.php'];
+ yield 'Domain Router configured' => ['http://example.com', true, ''];
+ yield 'Subdirectory Router not configured' => ['http://example.com/moodle', false, '/moodle/r.php'];
+ yield 'Subdirectory Router configured' => ['http://example.com/moodle', true, '/moodle'];
}
public function test_basepath_guessed_rphp(): void {