Compare commits

..
Author SHA1 Message Date
cameron1729 85d0657563 Merge pull request #418 from catalyst/fix-502-forcelogin-tests
Fix outage step test by forcing login default to off
2026-07-16 01:41:52 +08:00
Cameron Ball 88667c51e4 Fix outage step test by forcing login default to off 2026-07-16 01:37:04 +08:00
Sarah Cotton 5dd73b0909 Merge pull request #411 from sarahjcotton/WR489688-security-fix-501
WR489688: Security fix #30
2026-07-13 14:02:23 +01:00
Sarah Cotton 1a94b8e635 WR489688: Security fix #30 2026-06-25 15:10:18 +01:00
23 changed files with 77 additions and 94 deletions
-1
View File
@@ -69,7 +69,6 @@ if (is_callable('auth_outage_bootstrap_callback')) {
}
// 3) Check for allowed scripts or IPs during outages.
$outageinfo = false;
if (!empty($_SERVER['REQUEST_URI'])) {
$rooturl = parse_url($CFG->wwwroot);
$path = '';
+3 -6
View File
@@ -40,18 +40,16 @@ class calendar {
/**
* Create an event on the calendar for this outage.
* @param outage $outage Outage to be added to the calendar.
* @return void
*/
public static function create(outage $outage): void {
public static function create(outage $outage) {
calendar_event::create(self::create_data($outage));
}
/**
* Updates an event on the calendar based on this outage.
* @param outage $outage Outage to be updated in the calendar.
* @return void
*/
public static function update(outage $outage): void {
public static function update(outage $outage) {
$event = self::load($outage->id);
if (is_null($event)) {
@@ -65,9 +63,8 @@ class calendar {
/**
* Removes an event from the calendar related to this outage.
* @param int $outageid Id of outage to be deleted from the calendar.
* @return void
*/
public static function delete(int $outageid): void {
public static function delete($outageid) {
$event = self::load($outageid);
// If not found (was not created before) ignore it.
+1 -2
View File
@@ -48,7 +48,6 @@ class outagedb {
/**
* Gets all outage entries.
* @return outage[]
*/
public static function get_all() {
global $DB;
@@ -362,7 +361,7 @@ class outagedb {
$data = $DB->get_records_select(
'auth_outage',
'starttime <= :datetime1 AND :datetime2 <= stoptime AND finished IS NULL',
['datetime1' => $time, 'datetime2' => $time],
['datetime1' => $time, 'datetime2' => $time, 'datetime3' => $time],
'starttime ASC, stoptime DESC, title ASC',
'*',
0,
-2
View File
@@ -67,7 +67,6 @@ class edit extends moodleform {
$mform->addHelpButton('title', 'title', 'auth_outage');
$mform->addElement('editor', 'description', get_string('description', 'auth_outage'));
$mform->setType('description[text]', PARAM_RAW);
$mform->addHelpButton('description', 'description', 'auth_outage');
$mform->addElement('static', 'usagehints', '', get_string('textplaceholdershint', 'auth_outage'));
@@ -80,7 +79,6 @@ class edit extends moodleform {
get_string('useaccesskey:desc', 'auth_outage'),
0
);
$mform->setType('useaccesskey', PARAM_BOOL);
$mform->addElement('text', 'accesskey', get_string('accesskey', 'auth_outage'));
$mform->setType('accesskey', PARAM_TEXT);
+1 -1
View File
@@ -79,6 +79,6 @@ class cli_exception extends Exception {
* @param Exception|null $previous Another exception as reference or null.
*/
public function __construct($message, $code = 1, ?Exception $previous = null) {
parent::__construct('*ERROR* ' . $message, $code, $previous);
parent::__construct('*ERROR* ' . $message, $code, $previous = null);
}
}
+1
View File
@@ -114,6 +114,7 @@ abstract class clibase {
* Change session to admin user.
*/
protected function become_admin_user() {
global $DB;
$user = get_admin();
unset($user->description);
unset($user->access);
+30 -2
View File
@@ -28,7 +28,8 @@ use coding_exception;
* @copyright 2016 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class create extends clibase {
class create extends clibase
{
/**
* @var mixed[] Defaults to use if given option is null.
*/
@@ -98,7 +99,6 @@ class create extends clibase {
/**
* Executes the CLI.
* @throws cli_exception
*/
public function execute() {
// Help always overrides any other parameter.
@@ -269,4 +269,32 @@ class create extends clibase {
}
return $option;
}
/**
* Ensures the given option is or can be converted to a bool.
* @param mixed $option The parameter to check.
* @param string $param Name of that parameter.
* @return bool The converted parameter.
* @throws cli_exception
*/
private function merge_options_check_parameters_bool($option, $param) {
if (is_bool($option)) {
return $option;
}
if (is_string($option)) {
$option = strtoupper($option);
if (in_array($option, ['0', 'FALSE', 'NO', 'N'])) {
return false;
}
if (in_array($option, ['1', 'TRUE', 'YES', 'Y'])) {
return true;
}
}
throw new cli_exception(
get_string('clierrorinvalidvaluenotbool', 'auth_outage', ['param' => $param]),
cli_exception::ERROR_PARAMETER_INVALID
);
}
}
-1
View File
@@ -56,7 +56,6 @@ class finish extends clibase {
/**
* Executes the CLI.
* @throws cli_exception
*/
public function execute() {
// Help always overrides any other parameter.
-1
View File
@@ -101,7 +101,6 @@ class waitforit extends clibase {
$outage = $this->get_outage();
while ($sleep = $this->wait_for_outage_to_start($outage)) {
$sleep = max(1, $sleep);
if (is_null($this->sleepcallback)) {
$this->verbose('Sleeping for ' . $sleep . ' second(s).');
sleep($sleep);
+14 -15
View File
@@ -47,6 +47,11 @@ class infopage {
* @param array|null $params Parameters to use or null to get from Moodle API (request).
*/
public function __construct(?array $params = null) {
global $CFG;
// Enable SVG support here to make sure all SVG files
// used in the current theme are served properly.
$CFG->svgicons = true;
if (is_null($params)) {
$params = [
'id' => optional_param('id', null, PARAM_INT),
@@ -96,29 +101,21 @@ class infopage {
if (!$this->static && !has_capability('auth/outage:viewinfo', context_system::instance())) {
redirect(new moodle_url('/'));
}
// Enable SVG support here to make sure all SVG files
// used in the current theme are served properly.
$previoussvg = $CFG->svgicons ?? null;
$CFG->svgicons = true;
$PAGE->set_context(context_system::instance());
$PAGE->set_title($this->outage->get_title());
$PAGE->set_heading($this->outage->get_title());
$PAGE->set_url(new moodle_url('/auth/outage/info.php'));
// Inject metadata into the header before any output starts, otherwise header() will
// fail once outagelib::get_inject_code() below has echoed anything.
// No hooks injecting into this page, do it manually.
echo outagelib::get_inject_code();
// Inject metadata into the header before output.
if (!empty($this->outage->metadata)) {
$safemeta = str_replace(["\r", "\n"], '', $this->outage->metadata);
header('X-Outage-Metadata: ' . $safemeta);
header('X-Outage-Metadata: ' . $this->outage->metadata);
header('X-Outage-StartTime: ' . $this->outage->starttime);
header('X-Outage-EndTime: ' . $this->outage->stoptime);
}
// No hooks injecting into this page, do it manually.
echo outagelib::get_inject_code();
echo $OUTPUT->header();
$viewbag = [
'admin' => is_siteadmin(),
@@ -126,8 +123,10 @@ class infopage {
];
require($CFG->dirroot . '/auth/outage/views/info/content.php');
echo $OUTPUT->footer();
$CFG->svgicons = $previoussvg;
// Moodle 2.7 did not check for CLI mode, which was fixed later.
if (!($CFG->branch == '27' && CLI_SCRIPT)) {
echo $OUTPUT->footer();
}
}
/**
@@ -51,8 +51,7 @@ class maintenance_static_page {
} else {
// Inject metadata into the header before output.
if (!empty($outage->metadata)) {
$safemeta = str_replace(["\r", "\n"], '', $outage->metadata);
header('X-Outage-Metadata: ' . $safemeta);
header('X-Outage-Metadata: ' . $outage->metadata);
header('X-Outage-StartTime: ' . $outage->starttime);
header('X-Outage-EndTime: ' . $outage->stoptime);
}
+6 -7
View File
@@ -51,7 +51,6 @@ class outagelib {
/**
* Fetches page.
* @param string $file file to be fetched
* @return array{contents: string|false, mime: string}
*/
public static function fetch_page($file) {
global $CFG;
@@ -70,7 +69,6 @@ class outagelib {
/**
* Resets inject called to allow the code to be regenerated.
* @return void
*/
public static function reset_injectcalled() {
self::$injectcalled = false;
@@ -79,7 +77,6 @@ class outagelib {
/**
* Given a time, usually now, when is the next outage window?
* @param int $time time for next window
* @return int
*/
public static function get_next_window($time = null) {
@@ -180,7 +177,6 @@ class outagelib {
'default_warning_duration' => (string)(60 * 60),
'default_title' => get_string('defaulttitlevalue', 'auth_outage'),
'default_description' => get_string('defaultdescriptionvalue', 'auth_outage'),
'default_metadata' => '',
'remove_selectors' => ".usermenu\n.logininfo\n.homelink",
];
}
@@ -271,6 +267,10 @@ class outagelib {
// single-quotes (and double for the sake of it) are present otherwise it would break the code.
$allowedips = addslashes($allowedips);
// Escape the access key before substitution into the PHP literal to prevent
// code injection via a maliciously crafted access key value.
$accesskey = addslashes((string)$accesskey);
$cookiesecure = is_moodle_cookie_secure();
// Since Moodle 4.3 cookiehttponly is default to true and this CFG is not set.
@@ -288,7 +288,7 @@ if ((time() >= {{STARTTIME}}) && (time() < {{STOPTIME}})) {
require_once($CFG->dirroot.'/lib/classes/ip_utils.php');
}
// 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_ALPHANUM);
$urlaccesskey = optional_param('accesskey', null, PARAM_TEXT);
$isphpunit = defined('PHPUNIT_TEST');
if (!empty($urlaccesskey) && !$isphpunit) {
@@ -332,8 +332,7 @@ if ((time() >= {{STARTTIME}}) && (time() < {{STOPTIME}})) {
}
if ({{USEACCESSKEY}} && $accesskeyblocked) {
$safeaccesskey = htmlspecialchars($useraccesskey ?? '', ENT_QUOTES | ENT_HTML5, 'UTF-8');
echo '<!-- auth_outage blocked by missing or incorrect access key, access key given: ' . $safeaccesskey . ' -->';
echo '<!-- auth_outage blocked by missing or incorrect access key, access key given: '. $useraccesskey .' -->';
}
if (!$isphpunit) {
+2 -2
View File
@@ -187,8 +187,8 @@ class renderer extends plugin_renderer_base {
$outagehtml = html_writer::div(
html_writer::tag(
'blockquote',
html_writer::div(html_writer::tag('b', format_string($outage->get_title()), ['data-id' => $outage->id])) .
html_writer::div(html_writer::tag('i', format_text($outage->get_description(), FORMAT_HTML))) .
html_writer::div(html_writer::tag('b', $outage->get_title(), ['data-id' => $outage->id])) .
html_writer::div(html_writer::tag('i', $outage->get_description())) .
html_writer::div(
html_writer::tag('b', get_string('tableheaderwarnbefore', 'auth_outage') . ': ') .
format_time($outage->get_warning_duration())
+1 -4
View File
@@ -37,10 +37,7 @@ class update_static_page extends scheduled_task {
}
/**
* Executes the task: regenerates the maintenance static page for the next scheduled outage.
*
* @throws \coding_exception
* @throws \file_exception
* Executes the event.
*/
public function execute() {
outagelib::prepare_next_outage();
-5
View File
@@ -21,14 +21,9 @@
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_createdby" TYPE="foreign" FIELDS="createdby" REFTABLE="user" REFFIELDS="id"/>
<KEY NAME="fk_modifiedby" TYPE="foreign" FIELDS="modifiedby" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="start_stop_title" UNIQUE="false" FIELDS="starttime, stoptime, title"/>
<INDEX NAME="ix_stoptime_finished" UNIQUE="false" FIELDS="stoptime, finished"/>
<INDEX NAME="ix_createdby" UNIQUE="false" FIELDS="createdby"/>
<INDEX NAME="ix_modifiedby" UNIQUE="false" FIELDS="modifiedby"/>
</INDEXES>
</TABLE>
</TABLES>
+3 -20
View File
@@ -37,31 +37,14 @@ if (!isset($_GET['file'])) {
die('Missing file parameter.');
}
$rawfile = $_GET['file'];
if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $rawfile)) {
http_response_code(400);
die('Invalid file parameter.');
}
$parts = explode('.', $rawfile);
$parts = explode('.', $_GET['file']);
if (count($parts) != 2) {
http_response_code(400);
die('Invalid file requested.');
}
$extension = strtolower(pathinfo($parts[0], PATHINFO_EXTENSION));
$allowedmimes = [
'css' => 'text/css',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
];
if (!array_key_exists($extension, $allowedmimes)) {
http_response_code(400);
die('Unsupported file type.');
}
$mime = $allowedmimes[$extension];
$mime = base64_decode($parts[1]);
// Detect type, we only support css or PNG images.
header('Content-Type: ' . $mime);
// Use cache.
-2
View File
@@ -31,8 +31,6 @@ use auth_outage\local\controllers\maintenance_static_page;
// @codingStandardsIgnoreStart
require_once(__DIR__.'/../../config.php');
// @codingStandardsIgnoreEnd
require_once($CFG->libdir . '/adminlib.php');
admin_externalpage_setup('auth_outage_manage');
$id = optional_param('id', null, PARAM_INT);
$outage = is_null($id) ? outagedb::get_next_starting() : outagedb::get_by_id($id);
if (is_null($outage)) {
+3
View File
@@ -95,6 +95,9 @@ abstract class base_testcase extends \advanced_testcase {
parent::setUp();
$this->resetAfterTest(true);
// These tests rely on force login being disabled, but Moodle 5.2 enables it by default (MDL-87523).
set_config('forcelogin', 0);
}
/**
+4 -12
View File
@@ -154,23 +154,15 @@ final class infopage_test extends \auth_outage\base_testcase {
}
/**
* Tests that rendering the page enables SVG support and restores the previous value afterwards.
* Tests the constructor enables SVG support.
*/
public function test_svgicons_is_restored_after_output(): void {
public function test_svgicons_is_true(): void {
global $CFG;
$this->assertTrue(has_capability('auth/outage:viewinfo', context_system::instance()));
$outage = $this->get_dummy_outage();
$CFG->svgicons = false;
$info = new infopage(['outage' => $outage, 'static' => false]);
// Constructing the page should not touch $CFG->svgicons.
self::assertFalse($CFG->svgicons);
$info->get_output();
// Svgicons should be restored to its original value once rendering has finished.
self::assertFalse($CFG->svgicons);
new infopage();
self::assertTrue($CFG->svgicons);
}
}
+4 -6
View File
@@ -272,7 +272,7 @@ if ((time() >= 123) && (time() < 456)) {
require_once($CFG->dirroot.'/lib/classes/ip_utils.php');
}
// 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_ALPHANUM);
$urlaccesskey = optional_param('accesskey', null, PARAM_TEXT);
$isphpunit = defined('PHPUNIT_TEST');
if (!empty($urlaccesskey) && !$isphpunit) {
@@ -318,8 +318,7 @@ e.e.e.e/20');
}
if (true && $accesskeyblocked) {
$safeaccesskey = htmlspecialchars($useraccesskey ?? '', ENT_QUOTES | ENT_HTML5, 'UTF-8');
echo '<!-- auth_outage blocked by missing or incorrect access key, access key given: ' . $safeaccesskey . ' -->';
echo '<!-- auth_outage blocked by missing or incorrect access key, access key given: '. $useraccesskey .' -->';
}
if (!$isphpunit) {
@@ -359,7 +358,7 @@ if ((time() >= 123) && (time() < 456)) {
require_once($CFG->dirroot.'/lib/classes/ip_utils.php');
}
// 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_ALPHANUM);
$urlaccesskey = optional_param('accesskey', null, PARAM_TEXT);
$isphpunit = defined('PHPUNIT_TEST');
if (!empty($urlaccesskey) && !$isphpunit) {
@@ -403,8 +402,7 @@ if ((time() >= 123) && (time() < 456)) {
}
if (true && $accesskeyblocked) {
$safeaccesskey = htmlspecialchars($useraccesskey ?? '', ENT_QUOTES | ENT_HTML5, 'UTF-8');
echo '<!-- auth_outage blocked by missing or incorrect access key, access key given: ' . $safeaccesskey . ' -->';
echo '<!-- auth_outage blocked by missing or incorrect access key, access key given: '. $useraccesskey .' -->';
}
if (!$isphpunit) {
+1 -1
View File
@@ -28,7 +28,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = "auth_outage";
$plugin->version = 2026011305; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2026011306; // The current plugin version (Date: YYYYMMDDXX).
$plugin->release = 2026011305; // Human-readable release information.
$plugin->requires = 2025100600; // Moodle 5.1.
$plugin->maturity = MATURITY_STABLE; // Suitable for PRODUCTION environments!
+1 -1
View File
@@ -39,7 +39,7 @@ defined('MOODLE_INTERNAL') || die();
<b><?php echo get_string('infountil', 'auth_outage'); ?></b>
<?php echo userdate($viewbag['outage']->stoptime, get_string('datetimeformat', 'auth_outage')); ?>
</div>
<div class="auth_outage_info_description"><?php echo format_text($viewbag['outage']->get_description(), FORMAT_HTML); ?></div>
<div class="auth_outage_info_description"><?php echo $viewbag['outage']->get_description(); ?></div>
<?php if ($viewbag['admin']) : ?>
<?php
+1 -1
View File
@@ -68,7 +68,7 @@ if (!$viewbag['static']) {
<style>
<?php
readfile($CFG->dirroot . '/auth/outage/views/warningbar/warningbar.css');
echo preg_replace('/<\s*\/\s*style\s*>/i', '', outagelib::get_config()->css);
echo outagelib::get_config()->css;
?>
</style>