From 425ef963fb600ea75706d8792c2df8a4c8fbf720 Mon Sep 17 00:00:00 2001 From: Peter Burnett Date: Wed, 31 Dec 2025 10:31:30 +1000 Subject: [PATCH 1/2] Issue 387: Add metadata header to outage pages --- classes/form/outage/edit.php | 6 ++++++ classes/local/controllers/infopage.php | 7 +++++++ .../local/controllers/maintenance_static_page.php | 6 ++++++ classes/local/outage.php | 5 +++++ classes/local/outagelib.php | 14 ++++++++++---- db/install.xml | 3 ++- db/upgrade.php | 15 +++++++++++++++ edit.php | 2 +- lang/en/auth_outage.php | 4 ++++ settings.php | 10 ++++++++++ tests/local/outagelib_test.php | 12 +++++++++++- version.php | 4 ++-- 12 files changed, 79 insertions(+), 9 deletions(-) diff --git a/classes/form/outage/edit.php b/classes/form/outage/edit.php index 3393396..1b13fd8 100644 --- a/classes/form/outage/edit.php +++ b/classes/form/outage/edit.php @@ -90,6 +90,10 @@ class edit extends moodleform { $mform->disabledIf('accesskey', 'useaccesskey'); $mform->addHelpButton('accesskey', 'accesskey', 'auth_outage'); + $mform->addElement('text', 'metadata', get_string('metadata', 'auth_outage')); + $mform->setType('metadata', PARAM_TEXT); + $mform->addHelpButton('metadata', 'metadata', 'auth_outage'); + $this->add_action_buttons(); } @@ -144,6 +148,7 @@ class edit extends moodleform { 'title' => $data->title, 'description' => $data->description['text'], 'accesskey' => $data->useaccesskey ? $data->accesskey : null, + 'metadata' => $data->metadata ?? null, ]; return new outage($outagedata); } @@ -169,6 +174,7 @@ class edit extends moodleform { 'description' => ['text' => $outage->description, 'format' => '1'], 'accesskey' => $outage->accesskey, 'useaccesskey' => !empty($outage->accesskey), + 'metadata' => $outage->metadata, ]); // If the default_autostart is configured in config, then force autostart to be the default value. diff --git a/classes/local/controllers/infopage.php b/classes/local/controllers/infopage.php index a812b7c..acfe126 100644 --- a/classes/local/controllers/infopage.php +++ b/classes/local/controllers/infopage.php @@ -109,6 +109,13 @@ class infopage { // 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)) { + header('X-Outage-Metadata: ' . $this->outage->metadata); + header('X-Outage-StartTime: ' . $this->outage->starttime); + header('X-Outage-EndTime: ' . $this->outage->stoptime); + } + echo $OUTPUT->header(); $viewbag = [ 'admin' => is_siteadmin(), diff --git a/classes/local/controllers/maintenance_static_page.php b/classes/local/controllers/maintenance_static_page.php index 485d640..4a6896d 100644 --- a/classes/local/controllers/maintenance_static_page.php +++ b/classes/local/controllers/maintenance_static_page.php @@ -49,6 +49,12 @@ class maintenance_static_page { } else if (PHPUNIT_TEST || defined('BEHAT_SITE_RUNNING')) { $html = ''; } else { + // Inject metadata into the header before output. + if (!empty($outage->metadata)) { + header('X-Outage-Metadata: ' . $outage->metadata); + header('X-Outage-StartTime: ' . $outage->starttime); + header('X-Outage-EndTime: ' . $outage->stoptime); + } $data = maintenance_static_page_io::file_get_data( $CFG->wwwroot . '/auth/outage/info.php?auth_outage_hide_warning=1&static=1&id=' . $outage->id ); diff --git a/classes/local/outage.php b/classes/local/outage.php index 6a55aaf..b64a309 100644 --- a/classes/local/outage.php +++ b/classes/local/outage.php @@ -113,6 +113,11 @@ class outage { */ public $accesskey = null; + /** + * @var string|null metadata string, or null if not enabled. + */ + public $metadata = 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 0316ef5..93722c2 100644 --- a/classes/local/outagelib.php +++ b/classes/local/outagelib.php @@ -283,7 +283,7 @@ class outagelib { * @return string * @throws invalid_parameter_exception */ - public static function create_climaintenancephp_code($starttime, $stoptime, $allowedips, $accesskey = null) { + public static function create_climaintenancephp_code($starttime, $stoptime, $allowedips, $accesskey = null, $metadata = null) { global $CFG; if (!is_int($starttime) || !is_int($stoptime)) { throw new invalid_parameter_exception('Make sure $startime and $stoptime are integers.'); @@ -337,6 +337,11 @@ if ((time() >= {{STARTTIME}}) && (time() < {{STOPTIME}})) { header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); header('Accept-Ranges: none'); header('X-Moodle-Maintenance: manager'); + if (!empty({{METADATA}})) { + header('X-Outage-Metadata: ' . {{METADATA}}); + } + header('X-Outage-StartTime: ' . '{{STARTTIME}}'); + header('X-Outage-EndTime: ' . '{{STOPTIME}}'); } if (!$isphpunit && ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER))) { @@ -363,10 +368,10 @@ if ((time() >= {{STARTTIME}}) && (time() < {{STOPTIME}})) { } EOT; $search = ['{{STARTTIME}}', '{{STOPTIME}}', '{{USEALLOWEDIPS}}', '{{ALLOWEDIPS}}', '{{USEACCESSKEY}}', '{{ACCESSKEY}}', - '{{YOURIP}}', '{{COOKIESECURE}}', '{{COOKIEHTTPONLY}}']; + '{{YOURIP}}', '{{COOKIESECURE}}', '{{COOKIEHTTPONLY}}', '{{METADATA}}']; // 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)]; + $accesskey, getremoteaddr('n/a'), var_export($cookiesecure, true), var_export($cookiehttponly, true), var_export($metadata, true)]; return str_replace($search, $replace, $code); } @@ -389,6 +394,7 @@ EOT; $config = self::get_config(); $allowedips = trim($config->allowedips); $accesskey = $outage->accesskey ?? null; + $metadata = $outage->metadata ?? null; // 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))) { @@ -396,7 +402,7 @@ EOT; unlink($file); } } else { - $code = self::create_climaintenancephp_code($outage->starttime, $outage->stoptime, $allowedips, $accesskey); + $code = self::create_climaintenancephp_code($outage->starttime, $outage->stoptime, $allowedips, $accesskey, $metadata); $dir = dirname($file); if (!file_exists($dir) || !is_dir($dir)) { diff --git a/db/install.xml b/db/install.xml index 560e44f..8699ef9 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -18,6 +18,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index 8673f61..8d27d7a 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -61,5 +61,20 @@ function xmldb_auth_outage_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2024081900, 'auth', 'outage'); } + if ($oldversion < 2026011301) { + + // Define field metadata to be added to auth_outage. + $table = new xmldb_table('auth_outage'); + $field = new xmldb_field('metadata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'accesskey'); + + // Conditionally launch add field metadata. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Outage savepoint reached. + upgrade_plugin_savepoint(true, 2026011301, 'auth', 'outage'); + } + return true; } diff --git a/edit.php b/edit.php index 4976353..d3f34a3 100644 --- a/edit.php +++ b/edit.php @@ -27,7 +27,6 @@ use auth_outage\dml\outagedb; use auth_outage\form\outage\edit; use auth_outage\local\outage; use auth_outage\local\outagelib; -use auth_outage\output\renderer; require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir . '/adminlib.php'); @@ -90,6 +89,7 @@ if ($clone) { 'warntime' => $time - $config->default_warning_duration, 'title' => $config->default_title, 'description' => $config->default_description, + 'metadata' => $config->default_metadata, ]); $action = 'outagecreate'; } diff --git a/lang/en/auth_outage.php b/lang/en/auth_outage.php index b0f3d89..5d72a9f 100644 --- a/lang/en/auth_outage.php +++ b/lang/en/auth_outage.php @@ -88,6 +88,8 @@ $string['defaultdescriptiondescription'] = 'Default warning message for outages. $string['defaultdescriptionvalue'] = 'There is maintenance scheduled from {{start}} to {{stop}} and our system will not be available during that time.'; $string['defaultlayoutcss'] = 'Layout CSS'; $string['defaultlayoutcssdescription'] = 'This CSS code can be used to override the Outage Warning Bar CSS.'; +$string['defaultmetadata'] = 'Default metadata'; +$string['defaultmetadatadescription'] = 'The default metadata to include in the outage settings, to be used as a template for new outages. E.g. JIRA-{ticketno}.'; $string['defaultoutageautostart'] = 'Outage auto start'; $string['defaultoutageautostartdescription'] = 'If the outage should automatically trigger maintenance mode once it starts, locking down the whole site.'; $string['defaultoutageduration'] = 'Outage duration'; @@ -120,6 +122,8 @@ $string['messageoutagebackonline'] = 'We are back online!'; $string['messageoutagebackonlinedescription'] = 'You may resume browsing safely.'; $string['messageoutageongoing'] = 'Back online at {$a->stop}.'; $string['messageoutagewarning'] = 'Shutting down in {{countdown}}'; +$string['metadata'] = 'Outage metadata'; +$string['metadata_help'] = 'Data here will be output in the outage page as a meta tag in the header of the outage page.'; $string['na'] = 'n/a'; $string['notfound'] = 'No outages found.'; $string['outage:updatenotify'] = ''; diff --git a/settings.php b/settings.php index eeb10f0..dfd5221 100644 --- a/settings.php +++ b/settings.php @@ -83,6 +83,16 @@ if ($hassiteconfig) { $defaults['default_title'], PARAM_TEXT )); + $settings->add( + new admin_setting_configtext( + 'auth_outage/default_metadata', + get_string('defaultmetadata', 'auth_outage'), + get_string('defaultmetadatadescription', 'auth_outage'), + '', + PARAM_TEXT + ) + ); + $settings->add(new admin_setting_configtextarea( 'auth_outage/default_description', get_string('defaultdescription', 'auth_outage'), diff --git a/tests/local/outagelib_test.php b/tests/local/outagelib_test.php index 092725a..12595ef 100644 --- a/tests/local/outagelib_test.php +++ b/tests/local/outagelib_test.php @@ -352,6 +352,11 @@ e.e.e.e/20'); header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); header('Accept-Ranges: none'); header('X-Moodle-Maintenance: manager'); + if (!empty('testmeta')) { + header('X-Outage-Metadata: ' . 'testmeta'); + } + header('X-Outage-StartTime: ' . '123'); + header('X-Outage-EndTime: ' . '456'); } if (!$isphpunit && ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER))) { @@ -377,7 +382,7 @@ e.e.e.e/20'); } } EOT; - $found = outagelib::create_climaintenancephp_code(123, 456, "hey'\"you\na.b.c.d\ne.e.e.e/20", '12345'); + $found = outagelib::create_climaintenancephp_code(123, 456, "hey'\"you\na.b.c.d\ne.e.e.e/20", '12345', 'testmeta'); self::assertSame($expected, $found); } @@ -431,6 +436,11 @@ if ((time() >= 123) && (time() < 456)) { header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); header('Accept-Ranges: none'); header('X-Moodle-Maintenance: manager'); + if (!empty(NULL)) { + header('X-Outage-Metadata: ' . NULL); + } + header('X-Outage-StartTime: ' . '123'); + header('X-Outage-EndTime: ' . '456'); } if (!$isphpunit && ((defined('AJAX_SCRIPT') && AJAX_SCRIPT) || (defined('WS_SERVER') && WS_SERVER))) { diff --git a/version.php b/version.php index 1eb9caa..3f613a3 100644 --- a/version.php +++ b/version.php @@ -28,8 +28,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = "auth_outage"; -$plugin->version = 2026011300; // The current plugin version (Date: YYYYMMDDXX). -$plugin->release = 2026011300; // Human-readable release information. +$plugin->version = 2026011301; // The current plugin version (Date: YYYYMMDDXX). +$plugin->release = 2026011301; // Human-readable release information. $plugin->requires = 2025100600; // Moodle 5.1. $plugin->maturity = MATURITY_STABLE; // Suitable for PRODUCTION environments! $plugin->supported = [501, 501]; // A range of branch numbers of supported moodle versions. From 2a7556eb75f4c816d868f14929164e2d5dfd5240 Mon Sep 17 00:00:00 2001 From: Peter Burnett Date: Mon, 19 Jan 2026 13:45:20 +1000 Subject: [PATCH 2/2] chore: ci Fixes --- classes/local/outagelib.php | 14 ++++++++++++-- db/upgrade.php | 1 - lang/en/auth_outage.php | 8 ++------ tests/local/cli/waitforit_test.php | 4 ++-- .../controllers/maintenance_static_page_test.php | 3 +-- views/info/content.php | 7 +++---- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/classes/local/outagelib.php b/classes/local/outagelib.php index 93722c2..b23f99c 100644 --- a/classes/local/outagelib.php +++ b/classes/local/outagelib.php @@ -279,6 +279,7 @@ class outagelib { * @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. + * @param string|null $metadata Metadata to set in headers, or null if none. * * @return string * @throws invalid_parameter_exception @@ -370,8 +371,17 @@ EOT; $search = ['{{STARTTIME}}', '{{STOPTIME}}', '{{USEALLOWEDIPS}}', '{{ALLOWEDIPS}}', '{{USEACCESSKEY}}', '{{ACCESSKEY}}', '{{YOURIP}}', '{{COOKIESECURE}}', '{{COOKIEHTTPONLY}}', '{{METADATA}}']; // 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), var_export($metadata, true)]; + $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), + var_export($metadata, true)]; return str_replace($search, $replace, $code); } diff --git a/db/upgrade.php b/db/upgrade.php index 8d27d7a..8d28ef6 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -62,7 +62,6 @@ function xmldb_auth_outage_upgrade($oldversion) { } if ($oldversion < 2026011301) { - // Define field metadata to be added to auth_outage. $table = new xmldb_table('auth_outage'); $field = new xmldb_field('metadata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'accesskey'); diff --git a/lang/en/auth_outage.php b/lang/en/auth_outage.php index 5d72a9f..6972aab 100644 --- a/lang/en/auth_outage.php +++ b/lang/en/auth_outage.php @@ -32,9 +32,9 @@ $string['allowedipsnoconfig'] = 'Your config.php does not have the extra setup t $string['auth_outagedescription'] = 'Auxiliary plugin that warns users about a future outage and prevents them from logging in once the outage starts.'; $string['autostart'] = 'Auto start maintenance mode.'; $string['autostart_help'] = 'If selected, when the outage starts it will automatically turn on Moodle maintenance mode.'; +$string['autostartforcedoff'] = 'Force off'; $string['autostartoff'] = 'Default off'; $string['autostarton'] = 'Default on'; -$string['autostartforcedoff'] = 'Force off'; $string['builtinallowediplist'] = 'Builtin Allowed IP List'; $string['builtinallowediplist_desc'] = 'A second allowed IP list which makes it easier to have some IPs forced in config.php and others editable in the UI'; $string['clicreateexamples'] = "Create an outage starting in 10 seconds\n\n> php create.php -s=10"; @@ -144,6 +144,7 @@ $string['outagefinishwarning'] = 'You are about to mark this outage as finished. $string['outageslistfuture'] = 'Planned outages'; $string['outageslistpast'] = 'Outage history'; $string['pluginname'] = 'Outage manager'; +$string["privacy:no_data_reason"] = "The Outage authentication plugin does not store any personal data."; $string['removeselectors'] = 'Remove selectors'; $string['removeselectorsdescription'] = 'CSS selectors to remove when rendering a static themed maintenance page. One selector per line.'; $string['settingssectiondefaults'] = 'Default Outage Parameters'; @@ -173,8 +174,3 @@ $string['warningduration'] = 'Warning duration'; $string['warningduration_help'] = 'How long before the start of the outage should the warning be displayed.'; $string['warningdurationerrorinvalid'] = 'Warning duration must be positive.'; $string['warningreenablemaintenancemode'] = 'Please note that saving this outage will re-enable maintenance mode.
Untick "Auto start maintenance mode" if you want to prevent this.'; - -/* - * Privacy provider (GDPR) - */ -$string["privacy:no_data_reason"] = "The Outage authentication plugin does not store any personal data."; diff --git a/tests/local/cli/waitforit_test.php b/tests/local/cli/waitforit_test.php index 350b79a..03c9d5f 100644 --- a/tests/local/cli/waitforit_test.php +++ b/tests/local/cli/waitforit_test.php @@ -160,7 +160,7 @@ final class waitforit_test extends cli_testcase { /** * Tests the countdown. */ - public function test_countdown() { + public function test_countdown(): void { self::setAdminUser(); $now = time(); outagedb::save(new outage([ @@ -189,7 +189,7 @@ final class waitforit_test extends cli_testcase { /** * Tests if the outage changed while waiting. */ - public function test_outagechanged() { + public function test_outagechanged(): void { self::setAdminUser(); $now = time(); $id = outagedb::save(new outage([ diff --git a/tests/local/controllers/maintenance_static_page_test.php b/tests/local/controllers/maintenance_static_page_test.php index 3155db3..2aa6092 100644 --- a/tests/local/controllers/maintenance_static_page_test.php +++ b/tests/local/controllers/maintenance_static_page_test.php @@ -583,7 +583,7 @@ final class maintenance_static_page_test extends \auth_outage\base_testcase { /** * Test meta refresh maximum 5 minutes. */ - public function test_meta_refresh_maximum_5seconds() { + public function test_meta_refresh_maximum_5seconds(): void { $this->resetAfterTest(true); $html = "\n" . 'Title' . @@ -593,7 +593,6 @@ final class maintenance_static_page_test extends \auth_outage\base_testcase { $page->set_max_refresh_time(5); $page->generate(); $generated = trim(file_get_contents($page->get_io()->get_template_file())); - return $generated; self::assertStringContainsString('', $generated); } diff --git a/views/info/content.php b/views/info/content.php index 8a01cef..0f44cce 100644 --- a/views/info/content.php +++ b/views/info/content.php @@ -44,14 +44,13 @@ defined('MOODLE_INTERNAL') || die(); -$viewbag['outage']->get_warning_duration(), '15secondsbefore' => -15, 'start' => 0, 'endofoutage' => $viewbag['outage']->get_duration_planned() - 1, - ] as $title => $delta - ) { + ]; + foreach ($params as $title => $delta) { $adminlinks[] = html_writer::link( new moodle_url( '/auth/outage/info.php',