diff --git a/README.md b/README.md index b50ee46..01db01a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,21 @@ ![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/catalyst/moodle-auth_outage/ci.yml?branch=MOODLE_39_STABLE) # Moodle Outage manager plugin -* [Version Support](#version-support) -* [What is this?](#what-is-this) -* [Moodle Requirements](#moodle-requirements) -* [Screenshots](#screenshots) -* [Installation](#installation) -* [Theme configuration](#theme-configuration) -* [How to use](#how-to-use) -* [Quick Guide](#quick-guide) -* [Why is it an auth plugin?](#why-it-is-an-auth-plugin) -* [Feedback and issues](#feedback-and-issues) +- [Moodle Outage manager plugin](#moodle-outage-manager-plugin) + - [What is this?](#what-is-this) + - [Moodle Requirements](#moodle-requirements) + - [Branches](#branches) + - [Screenshots](#screenshots) + - [Installation](#installation) + - [Theme configuration](#theme-configuration) + - [Custom Theme Additional SCSS](#custom-theme-additional-scss) + - [How to use](#how-to-use) + - [Quick Guide](#quick-guide) + - [Why it is an auth plugin?](#why-it-is-an-auth-plugin) + - [Tester restriction options](#tester-restriction-options) + - [IP restriction](#ip-restriction) + - [Access key](#access-key) + - [Feedback and issues](#feedback-and-issues) What is this? ------------- @@ -178,6 +183,16 @@ Why it is an auth plugin? One of the graduated stages this plugin introduces is a 'tester only' mode which disables login for most normal users. This is conceptually similar to the maintenance mode but enables testers to login and confirm the state after an upgrade without needing full admin privileges. +Tester restriction options +------------ +Two options are available to restrict the site to only let testers in during the tester phase. +Note: these restrictions build on each other; If both are enabled, users must meet both criteria to be allowed in. + +## IP restriction +Only allow users from a certain IP or range of ips to enter. +## Access key +Users provide an access key in the URL params on first page load, which is then stored as a cookie for 24 hours. If the access key matches the one setup for the outage, they are allowed in. + Feedback and issues ------------------- diff --git a/bootstrap.php b/bootstrap.php index 5711215..415c9ea 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -77,13 +77,15 @@ $url = $path.'/auth/outage/info.php'; $outageinfo = strpos($_SERVER['REQUEST_URI'], $url) === 0 ? true : false; } + $allowed = !file_exists($CFG->dataroot.'/climaintenance.php') // Not in maintenance mode. || (defined('ABORT_AFTER_CONFIG') && ABORT_AFTER_CONFIG) // Only config requested. || (defined('CLI_SCRIPT') && CLI_SCRIPT) // Allow CLI scripts. || $outageinfo // Allow outage info requests. || (defined('NO_AUTH_OUTAGE') && NO_AUTH_OUTAGE); // Allow any page should not be blocked by maintenance mode. if (!$allowed) { - // Call the climaintenance.php which will check for allowed IPs. + // Call the climaintenance.php which will check for the conditions + // that have been baked into it from the frontend (ip, accesskey, etc...). $CFG->dirroot = dirname(dirname(dirname(__FILE__))); // It is not defined yet but the script below needs it. require($CFG->dataroot.'/climaintenance.php'); // This call may terminate the script here or not. } diff --git a/classes/form/outage/edit.php b/classes/form/outage/edit.php index 55b4872..d830cf3 100644 --- a/classes/form/outage/edit.php +++ b/classes/form/outage/edit.php @@ -75,6 +75,14 @@ public function definition() { $mform->addElement('static', 'usagehints', '', get_string('textplaceholdershint', 'auth_outage')); $mform->addElement('static', 'warningreenablemaintenancemode', ''); + $mform->addElement('advcheckbox', 'useaccesskey', get_string('useaccesskey', 'auth_outage'), + get_string('useaccesskey:desc', 'auth_outage'), 0); + + $mform->addElement('text', 'accesskey', get_string('accesskey', 'auth_outage')); + $mform->setType('accesskey', PARAM_TEXT); + $mform->disabledIf('accesskey', 'useaccesskey'); + $mform->addHelpButton('accesskey', 'accesskey', 'auth_outage'); + $this->add_action_buttons(); } @@ -128,6 +136,7 @@ public function get_data() { 'warntime' => $data->starttime - $data->warningduration, 'title' => $data->title, 'description' => $data->description['text'], + 'accesskey' => $data->useaccesskey ? $data->accesskey : null, ]; return new outage($outagedata); } @@ -151,6 +160,8 @@ public function set_data($outage) { 'warningduration' => $outage->get_warning_duration(), 'title' => $outage->title, 'description' => ['text' => $outage->description, 'format' => '1'], + 'accesskey' => $outage->accesskey, + 'useaccesskey' => !empty($outage->accesskey), ]); // If the default_autostart is configured in config, then force autostart to be the default value. diff --git a/classes/local/outage.php b/classes/local/outage.php index e426d72..6a55aaf 100644 --- a/classes/local/outage.php +++ b/classes/local/outage.php @@ -108,6 +108,11 @@ class outage { */ public $lastmodified = null; + /** + * @var string|null access key, or null if not enabled. + */ + public $accesskey = null; + /** * outage constructor. * @param stdClass|array|null $data The data for the outage. diff --git a/classes/local/outagelib.php b/classes/local/outagelib.php index 4c718fa..7163926 100644 --- a/classes/local/outagelib.php +++ b/classes/local/outagelib.php @@ -279,57 +279,103 @@ private static function injection_allowed() { * @param int $starttime Outage start time. * @param int $stoptime Outage stop time. * @param string $allowedips List of IPs allowed. + * @param string|null $accesskey access key, or null if no access key set. * * @return string * @throws invalid_parameter_exception */ - public static function create_climaintenancephp_code($starttime, $stoptime, $allowedips) { + public static function create_climaintenancephp_code($starttime, $stoptime, $allowedips, $accesskey = null) { + global $CFG; if (!is_int($starttime) || !is_int($stoptime)) { throw new invalid_parameter_exception('Make sure $startime and $stoptime are integers.'); } - if (!is_string($allowedips) || (trim($allowedips) == '')) { - throw new invalid_parameter_exception('$allowedips must be a valid string.'); - } // I know Moodle validation would clean up this field, but just in case, let's ensure no // single-quotes (and double for the sake of it) are present otherwise it would break the code. $allowedips = addslashes($allowedips); + $cookiesecure = is_moodle_cookie_secure(); + + // Since Moodle 4.3 cookiehttponly is default to true and this CFG is not set. + // so if not set, default to true. + $cookiehttponly = isset($CFG->cookiehttponly) ? (bool) $CFG->cookiehttponly : true; + $code = <<<'EOT' = {{STARTTIME}}) && (time() < {{STOPTIME}})) { - define('MOODLE_INTERNAL', true); + if (!defined('MOODLE_INTERNAL')) { + define('MOODLE_INTERNAL', true); + } require_once($CFG->dirroot.'/lib/moodlelib.php'); if (file_exists($CFG->dirroot.'/lib/classes/ip_utils.php')) { require_once($CFG->dirroot.'/lib/classes/ip_utils.php'); } - if (!remoteip_in_list('{{ALLOWEDIPS}}')) { - header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance'); - header('Status: 503 Moodle under maintenance'); - header('Retry-After: 300'); - header('Content-type: text/html; charset=utf-8'); - header('X-UA-Compatible: IE=edge'); - header('Cache-Control: no-store, no-cache, must-revalidate'); - header('Cache-Control: post-check=0, pre-check=0', false); - header('Pragma: no-cache'); - header('Expires: Mon, 20 Aug 1969 09:23:00 GMT'); - header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - header('Accept-Ranges: none'); - header('X-Moodle-Maintenance: manager'); - if ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER)) { - exit(0); + // Put access key as a cookie if given. This stops the need to put it as a url param on every request. + $urlaccesskey = optional_param('accesskey', null, PARAM_TEXT); + + if (!empty($urlaccesskey)) { + setcookie('auth_outage_accesskey', $urlaccesskey, time() + 86400, '/', '', {{COOKIESECURE}}, {{COOKIEHTTPONLY}}); + } + + // Use url access key if given, else the cookie, else null. + $useraccesskey = $urlaccesskey ?: $_COOKIE['auth_outage_accesskey'] ?? null; + + $ipblocked = !remoteip_in_list('{{ALLOWEDIPS}}'); + $accesskeyblocked = $useraccesskey != '{{ACCESSKEY}}'; + $blocked = ({{USEACCESSKEY}} && $accesskeyblocked) || ({{USEALLOWEDIPS}} && $ipblocked); + $isphpunit = defined('PHPUNIT_TEST'); + + if ($blocked) { + if (!$isphpunit) { + header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance'); + header('Status: 503 Moodle under maintenance'); + header('Retry-After: 300'); + header('Content-type: text/html; charset=utf-8'); + header('X-UA-Compatible: IE=edge'); + header('Cache-Control: no-store, no-cache, must-revalidate'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); + header('Expires: Mon, 20 Aug 1969 09:23:00 GMT'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + header('Accept-Ranges: none'); + header('X-Moodle-Maintenance: manager'); } - echo ''; - if (file_exists($CFG->dataroot.'/climaintenance.template.html')) { - require($CFG->dataroot.'/climaintenance.template.html'); + + if (!$isphpunit && ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER))) { exit(0); } - // The file above should always exist, but just in case... - die('We are currently under maintentance, please try again later.'); + + if ({{USEALLOWEDIPS}} && $ipblocked) { + echo ''; + } + + if ({{USEALLOWEDIPS}} && !$ipblocked) { + echo ''; + } + + if ({{USEACCESSKEY}} && $accesskeyblocked) { + echo ''; + } + + if ({{USEACCESSKEY}} && !$accesskeyblocked) { + echo ''; + } + + if (!$isphpunit) { + if (file_exists($CFG->dataroot.'/climaintenance.template.html')) { + require($CFG->dataroot.'/climaintenance.template.html'); + exit(0); + } + // The file above should always exist, but just in case... + die('We are currently under maintentance, please try again later.'); + } } } EOT; - $search = ['{{STARTTIME}}', '{{STOPTIME}}', '{{ALLOWEDIPS}}', '{{YOURIP}}']; - $replace = [$starttime, $stoptime, $allowedips, getremoteaddr('n/a')]; + $search = ['{{STARTTIME}}', '{{STOPTIME}}', '{{USEALLOWEDIPS}}', '{{ALLOWEDIPS}}', '{{USEACCESSKEY}}', '{{ACCESSKEY}}', + '{{YOURIP}}', '{{COOKIESECURE}}', '{{COOKIEHTTPONLY}}']; + // Note that var_export is required because (string) false == '', not 'false'. + $replace = [$starttime, $stoptime, var_export(!empty($allowedips), true), $allowedips, var_export(!empty($accesskey), true), + $accesskey, getremoteaddr('n/a'), var_export($cookiesecure, true), var_export($cookiehttponly, true)]; return str_replace($search, $replace, $code); } @@ -351,13 +397,15 @@ public static function update_climaintenance_code($outage) { $config = self::get_config(); $allowedips = trim($config->allowedips); + $accesskey = $outage->accesskey ?? null; - if (is_null($outage) || ($allowedips == '')) { + // If no outage, or allowed ips is null and access key is null (i.e. no blocking required). + if (is_null($outage) || ($allowedips == '' && empty($accesskey))) { if (file_exists($file)) { unlink($file); } } else { - $code = self::create_climaintenancephp_code($outage->starttime, $outage->stoptime, $allowedips); + $code = self::create_climaintenancephp_code($outage->starttime, $outage->stoptime, $allowedips, $accesskey); $dir = dirname($file); if (!file_exists($dir) || !is_dir($dir)) { diff --git a/db/install.xml b/db/install.xml index fd3696c..560e44f 100644 --- a/db/install.xml +++ b/db/install.xml @@ -17,6 +17,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index 61b31f5..4e46ec9 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -47,5 +47,20 @@ function xmldb_auth_outage_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2016092200, 'auth', 'outage'); } + if ($oldversion < 2024081900) { + + // Define field accesskey to be added to auth_outage. + $table = new xmldb_table('auth_outage'); + $field = new xmldb_field('accesskey', XMLDB_TYPE_CHAR, '16', null, null, null, null, 'finished'); + + // Conditionally launch add field accesskey. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Outage savepoint reached. + upgrade_plugin_savepoint(true, 2024081900, 'auth', 'outage'); + } + return true; } diff --git a/lang/en/auth_outage.php b/lang/en/auth_outage.php index 2545145..8eb74f5 100644 --- a/lang/en/auth_outage.php +++ b/lang/en/auth_outage.php @@ -103,9 +103,9 @@ $string['infostaticpage'] = 'static page'; $string['infopagestaticgenerated'] = 'This warning was generated on {$a->time}.'; $string['ips_combine'] = 'The IPs listed above will be combined with the IPs listed below.'; -$string['allowedipsempty'] = 'When the allowed IPs list is empty we will not block anyone. You can add your own IP address ({$a->ip}) and block all other IPs.'; -$string['allowedipshasmyip'] = 'Your IP ({$a->ip}) is in the list and you will not be blocked out during an Outage.'; -$string['allowedipshasntmyip'] = 'Your IP ({$a->ip}) is not in the list and you will be blocked out during an outage.'; +$string['allowedipsempty'] = 'No one will be blocked by IP because the list is empty. You can add your own IP address ({$a->ip}) and block all other IPs. IP blocking is in addition to access key blocking (if setup in outage)'; +$string['allowedipshasmyip'] = 'Your IP ({$a->ip}) is in the list and your IP will not be blocked out during an Outage.'; +$string['allowedipshasntmyip'] = 'Your IP ({$a->ip}) is not in the list and your IP will be blocked out during an outage.'; $string['allowedipsnoconfig'] = 'Your config.php does not have the extra setup to allow blocking via IP.
Please refer to our README.md file for more information.'; $string['logformaintmodeconfig'] = 'Update maintenance mode configuration.'; $string['logformaintmodeconfigcomplete'] = 'Updating maintenance mode configuration complete.'; @@ -162,6 +162,10 @@ $string['warningduration'] = 'Warning duration'; $string['warningduration_help'] = 'How long before the start of the outage should the warning be displayed.'; $string['warningreenablemaintenancemode'] = 'Please note that saving this outage will re-enable maintenance mode.
Untick "Auto start maintenance mode" if you want to prevent this.'; +$string['accesskey'] = 'Access key'; +$string['accesskey_help'] = 'Testers should pass the access key initially in the url parameters e.g. ?accesskey=xyz. This will then be stored in a cookie for 24 hours, during which the url parameter will not be necessary.
Note: the access key is in addition to any IP restrictions setup.'; +$string['useaccesskey'] = 'Use access key'; +$string['useaccesskey:desc'] = 'Require testers to access site during outage by providing the access key below'; /* * Privacy provider (GDPR) diff --git a/tests/local/outagelib_test.php b/tests/local/outagelib_test.php index ebf6751..4dcd8d8 100644 --- a/tests/local/outagelib_test.php +++ b/tests/local/outagelib_test.php @@ -308,43 +308,84 @@ public function test_inject_settings() { * Test create maintenance php code */ public function test_createmaintenancephpcode() { + global $CFG; + $CFG->cookiehttponly = false; + $expected = <<<'EOT' = 123) && (time() < 456)) { - define('MOODLE_INTERNAL', true); + if (!defined('MOODLE_INTERNAL')) { + define('MOODLE_INTERNAL', true); + } require_once($CFG->dirroot.'/lib/moodlelib.php'); if (file_exists($CFG->dirroot.'/lib/classes/ip_utils.php')) { require_once($CFG->dirroot.'/lib/classes/ip_utils.php'); } - if (!remoteip_in_list('hey\'\"you + // Put access key as a cookie if given. This stops the need to put it as a url param on every request. + $urlaccesskey = optional_param('accesskey', null, PARAM_TEXT); + + if (!empty($urlaccesskey)) { + setcookie('auth_outage_accesskey', $urlaccesskey, time() + 86400, '/', '', true, false); + } + + // Use url access key if given, else the cookie, else null. + $useraccesskey = $urlaccesskey ?: $_COOKIE['auth_outage_accesskey'] ?? null; + + $ipblocked = !remoteip_in_list('hey\'\"you a.b.c.d -e.e.e.e/20')) { - header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance'); - header('Status: 503 Moodle under maintenance'); - header('Retry-After: 300'); - header('Content-type: text/html; charset=utf-8'); - header('X-UA-Compatible: IE=edge'); - header('Cache-Control: no-store, no-cache, must-revalidate'); - header('Cache-Control: post-check=0, pre-check=0', false); - header('Pragma: no-cache'); - header('Expires: Mon, 20 Aug 1969 09:23:00 GMT'); - header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - header('Accept-Ranges: none'); - header('X-Moodle-Maintenance: manager'); - if ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER)) { - exit(0); +e.e.e.e/20'); + $accesskeyblocked = $useraccesskey != '12345'; + $blocked = (true && $accesskeyblocked) || (true && $ipblocked); + $isphpunit = defined('PHPUNIT_TEST'); + + if ($blocked) { + if (!$isphpunit) { + header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance'); + header('Status: 503 Moodle under maintenance'); + header('Retry-After: 300'); + header('Content-type: text/html; charset=utf-8'); + header('X-UA-Compatible: IE=edge'); + header('Cache-Control: no-store, no-cache, must-revalidate'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); + header('Expires: Mon, 20 Aug 1969 09:23:00 GMT'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + header('Accept-Ranges: none'); + header('X-Moodle-Maintenance: manager'); } - echo ''; - if (file_exists($CFG->dataroot.'/climaintenance.template.html')) { - require($CFG->dataroot.'/climaintenance.template.html'); + + if (!$isphpunit && ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER))) { exit(0); } - // The file above should always exist, but just in case... - die('We are currently under maintentance, please try again later.'); + + if (true && $ipblocked) { + echo ''; + } + + if (true && !$ipblocked) { + echo ''; + } + + if (true && $accesskeyblocked) { + echo ''; + } + + if (true && !$accesskeyblocked) { + echo ''; + } + + if (!$isphpunit) { + if (file_exists($CFG->dataroot.'/climaintenance.template.html')) { + require($CFG->dataroot.'/climaintenance.template.html'); + exit(0); + } + // The file above should always exist, but just in case... + die('We are currently under maintentance, please try again later.'); + } } } EOT; - $found = outagelib::create_climaintenancephp_code(123, 456, "hey'\"you\na.b.c.d\ne.e.e.e/20"); + $found = outagelib::create_climaintenancephp_code(123, 456, "hey'\"you\na.b.c.d\ne.e.e.e/20", '12345'); self::assertSame($expected, $found); } @@ -357,44 +398,84 @@ public function test_createmaintenancephpcode() { public function test_createmaintenancephpcode_withoutage($configkey) { global $CFG; $this->resetAfterTest(true); + $CFG->cookiehttponly = false; $expected = <<<'EOT' = 123) && (time() < 456)) { - define('MOODLE_INTERNAL', true); + if (!defined('MOODLE_INTERNAL')) { + define('MOODLE_INTERNAL', true); + } require_once($CFG->dirroot.'/lib/moodlelib.php'); if (file_exists($CFG->dirroot.'/lib/classes/ip_utils.php')) { require_once($CFG->dirroot.'/lib/classes/ip_utils.php'); } - if (!remoteip_in_list('127.0.0.1')) { - header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance'); - header('Status: 503 Moodle under maintenance'); - header('Retry-After: 300'); - header('Content-type: text/html; charset=utf-8'); - header('X-UA-Compatible: IE=edge'); - header('Cache-Control: no-store, no-cache, must-revalidate'); - header('Cache-Control: post-check=0, pre-check=0', false); - header('Pragma: no-cache'); - header('Expires: Mon, 20 Aug 1969 09:23:00 GMT'); - header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - header('Accept-Ranges: none'); - header('X-Moodle-Maintenance: manager'); - if ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER)) { - exit(0); + // Put access key as a cookie if given. This stops the need to put it as a url param on every request. + $urlaccesskey = optional_param('accesskey', null, PARAM_TEXT); + + if (!empty($urlaccesskey)) { + setcookie('auth_outage_accesskey', $urlaccesskey, time() + 86400, '/', '', true, false); + } + + // Use url access key if given, else the cookie, else null. + $useraccesskey = $urlaccesskey ?: $_COOKIE['auth_outage_accesskey'] ?? null; + + $ipblocked = !remoteip_in_list('127.0.0.1'); + $accesskeyblocked = $useraccesskey != '5678'; + $blocked = (true && $accesskeyblocked) || (true && $ipblocked); + $isphpunit = defined('PHPUNIT_TEST'); + + if ($blocked) { + if (!$isphpunit) { + header($_SERVER['SERVER_PROTOCOL'] . ' 503 Moodle under maintenance'); + header('Status: 503 Moodle under maintenance'); + header('Retry-After: 300'); + header('Content-type: text/html; charset=utf-8'); + header('X-UA-Compatible: IE=edge'); + header('Cache-Control: no-store, no-cache, must-revalidate'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); + header('Expires: Mon, 20 Aug 1969 09:23:00 GMT'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + header('Accept-Ranges: none'); + header('X-Moodle-Maintenance: manager'); } - echo ''; - if (file_exists($CFG->dataroot.'/climaintenance.template.html')) { - require($CFG->dataroot.'/climaintenance.template.html'); + + if (!$isphpunit && ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER))) { exit(0); } - // The file above should always exist, but just in case... - die('We are currently under maintentance, please try again later.'); + + if (true && $ipblocked) { + echo ''; + } + + if (true && !$ipblocked) { + echo ''; + } + + if (true && $accesskeyblocked) { + echo ''; + } + + if (true && !$accesskeyblocked) { + echo ''; + } + + if (!$isphpunit) { + if (file_exists($CFG->dataroot.'/climaintenance.template.html')) { + require($CFG->dataroot.'/climaintenance.template.html'); + exit(0); + } + // The file above should always exist, but just in case... + die('We are currently under maintentance, please try again later.'); + } } } EOT; $outage = new outage([ 'starttime' => 123, 'stoptime' => 456, + 'accesskey' => '5678', ]); $file = $CFG->dataroot.'/climaintenance.php'; set_config($configkey, '127.0.0.1', 'auth_outage'); @@ -414,15 +495,16 @@ public static function createmaintenancephpcode_withoutage_provider(): array { } /** - * Test create maintenance php code without IPs + * Test create maintenance php code without IPs or accesskey */ - public function test_createmaintenancephpcode_withoutips() { + public function test_createmaintenancephpcode_withoutips_or_accesskey() { global $CFG; $this->resetAfterTest(true); $outage = new outage([ 'starttime' => 123, 'stoptime' => 456, + 'accesskey' => null, ]); $file = $CFG->dataroot.'/climaintenance.php'; set_config('allowedips', '', 'auth_outage'); @@ -591,4 +673,140 @@ private function create_outage() { \core\session\manager::gc(); // Remove stale sessions. \core_plugin_manager::reset_caches(); } + + /** + * Provides values to test_evaluation_maintenancepage + * @return array + */ + public static function evaluation_maintenancepage_provider(): array { + $allowedipout = '