diff --git a/config-dist.php b/config-dist.php
index 214f36ff1b2..9bc9b7256ca 100644
--- a/config-dist.php
+++ b/config-dist.php
@@ -164,6 +164,12 @@ $CFG->dboptions = array(
$CFG->wwwroot = 'http://example.com/moodle';
+// Generally it is not advisable to use a wwwroot that ends in 'public'.
+// This is because the 'public' directory is used to serve web-accessible content.
+// Moodle looks for any URL which ends in 'public' and assumes that it is a misconfiguration.
+// In the event that there is a need to have a wwwroot that ends in 'public', the
+// following setting can be used to override this check.
+$CFG->wwwrootendsinpublic = false;
//=========================================================================
// 3. DATA FILES LOCATION
diff --git a/public/lang/en/error.php b/public/lang/en/error.php
index 39e2def2638..a0f3f5b9ee6 100644
--- a/public/lang/en/error.php
+++ b/public/lang/en/error.php
@@ -648,6 +648,7 @@ $string['wrongusernamepassword'] = 'Wrong user/password';
$string['wrongzipfilename'] = 'Wrong ZIP file name';
$string['wscouldnotcreateecoursenopermission'] = 'WS - Could not create course - No permission';
$string['wwwrootmismatch'] = 'Incorrect access detected, this server may be accessed only through "{$a}" address, sorry.
Please notify server administrator.';
+$string['wwwrootpublic'] = 'Detected incorrect $CFG->wwwroot in config.php, it should not end in /public. See MDL-85816 for further information.';
$string['wwwrootslash'] = 'Detected incorrect $CFG->wwwroot in config.php, it must not contain trailing slash.
Please notify server administrator.';
$string['xmldberror'] = 'XMLDB error!';
$string['alreadyloggedin'] = 'You are already logged in as {$a}, you need to log out before logging in as different user.';
diff --git a/public/lib/classes/setup.php b/public/lib/classes/setup.php
new file mode 100644
index 00000000000..72cab6d40dc
--- /dev/null
+++ b/public/lib/classes/setup.php
@@ -0,0 +1,83 @@
+.
+
+namespace core;
+
+use core\exception\moodle_exception;
+
+/**
+ * Core setup functionality for Moodle.
+ *
+ * @package core
+ * @copyright Andrew Lyons
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class setup {
+ /**
+ * Check the validity of the wwwroot, throwing a Moodle exception if invalid.
+ *
+ * @throws moodle_exception
+ * @return true
+ */
+ public function validate_wwwroot(): bool {
+ if ($this->does_wwwroot_end_in_slash()) {
+ // The wwwroot should not end in a slash as this may suggest a misconfiguration.
+ throw new moodle_exception('wwwrootslash', 'error');
+ }
+
+ if (!$this->can_wwwroot_end_in_public() && $this->does_wwwroot_end_in_public()) {
+ // The wwwroot should not end in /public as this may suggest a misconfiguration.
+ // There may be legitimate sites out there that currently do this but it is not recommended.
+ // Where a site _does_ need to do this, then they can set the $CFG->wwwrootendsinpublic var to true.
+ throw new moodle_exception('wwwrootpublic', 'error');
+ }
+
+ return true;
+ }
+
+ /**
+ * Whether the wwwroot is allowed to end in public.
+ *
+ * @return bool
+ */
+ protected function can_wwwroot_end_in_public(): bool {
+ global $CFG;
+
+ return property_exists($CFG, 'wwwrootendsinpublic') && $CFG->wwwrootendsinpublic;
+ }
+
+ /**
+ * Detect whether the wwwroot ends in /public.
+ *
+ * @return bool
+ */
+ protected function does_wwwroot_end_in_public(): bool {
+ global $CFG;
+
+ return substr($CFG->wwwroot, -7) == '/public';
+ }
+
+ /**
+ * Detect whether the wwwroot ends in /.
+ *
+ * @return bool
+ */
+ protected function does_wwwroot_end_in_slash(): bool {
+ global $CFG;
+
+ return str_ends_with($CFG->wwwroot, '/');
+ }
+}
diff --git a/public/lib/setuplib.php b/public/lib/setuplib.php
index 9ebddda6b9d..3f191ebbe4d 100644
--- a/public/lib/setuplib.php
+++ b/public/lib/setuplib.php
@@ -634,10 +634,10 @@ function hash_local_config_cache() {
function initialise_fullme() {
global $CFG, $FULLME, $ME, $SCRIPT, $FULLSCRIPT;
- // Detect common config error.
- if (substr($CFG->wwwroot, -1) == '/') {
- throw new \moodle_exception('wwwrootslash', 'error');
- }
+ $setuphelper = \core\di::get(\core\setup::class);
+
+ // Detect common config errors in the wwwroot.
+ $setuphelper->validate_wwwroot();
if (CLI_SCRIPT) {
initialise_fullme_cli();
diff --git a/public/lib/tests/setup_test.php b/public/lib/tests/setup_test.php
new file mode 100644
index 00000000000..ea6786b37a8
--- /dev/null
+++ b/public/lib/tests/setup_test.php
@@ -0,0 +1,110 @@
+.
+
+namespace core;
+
+/**
+ * Tests for the \core\setup class.
+ *
+ * @package core
+ * @category test
+ * @copyright Andrew Lyons
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+#[\PHPUnit\Framework\Attributes\CoversClass(\core\setup::class)]
+final class setup_test extends \advanced_testcase {
+ #[\PHPUnit\Framework\Attributes\DataProvider('wwwroot_validity_provider')]
+ public function test_wwwroot_ends_in_slash(
+ string $wwwroot,
+ bool $mayendinpublic,
+ bool $valid,
+ ?string $exceptionstring = null,
+ ): void {
+ global $CFG;
+
+ $this->resetAfterTest(true);
+ $CFG->wwwroot = $wwwroot;
+ $CFG->wwwrootendsinpublic = $mayendinpublic;
+
+ if (!$valid) {
+ $this->expectException(\core\exception\moodle_exception::class);
+ $this->expectExceptionMessage($exceptionstring);
+ }
+
+ $this->assertTrue(di::get(setup::class)->validate_wwwroot());
+ }
+
+ /**
+ * Data provider for test_wwwroot_ends_in_slash.
+ *
+ * @return \Generator
+ */
+ public static function wwwroot_validity_provider(): \Generator {
+ foreach (['https', 'http'] as $protocol) {
+ yield "Valid {$protocol} wwwroot on root domain" => [
+ "{$protocol}://example.com",
+ false,
+ true,
+ ];
+
+ yield "Valid {$protocol} wwwroot on sub domain" => [
+ "{$protocol}://moodle.example.com",
+ false,
+ true,
+ ];
+
+ yield "Valid {$protocol} wwwroot on directory" => [
+ "{$protocol}://example.com/moodle",
+ false,
+ true,
+ ];
+ }
+
+ yield "Root domain ends in slash" => [
+ "https://example.com/",
+ false,
+ false,
+ get_string('wwwrootslash', 'error'),
+ ];
+
+ yield "Sub domain ends in slash" => [
+ "https://moodle.example.com/",
+ false,
+ false,
+ get_string('wwwrootslash', 'error'),
+ ];
+
+ yield "Directory ends in slash" => [
+ "https://example.com/moodle/",
+ false,
+ false,
+ get_string('wwwrootslash', 'error'),
+ ];
+
+ yield "Directory ends in public and should not" => [
+ "https://example.com/public",
+ false,
+ false,
+ get_string('wwwrootpublic', 'error'),
+ ];
+
+ yield "Directory ends in public and is allowed to" => [
+ "https://example.com/public",
+ true,
+ true,
+ ];
+ }
+}