Merge pull request #400 from catalyst/issue-387-m501

Issue 387: Add metadata header to outage pages
This commit is contained in:
Matthew Hilton
2026-01-19 14:09:47 +10:00
committed by GitHub
15 changed files with 97 additions and 24 deletions

View File

@@ -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.

View File

@@ -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(),

View File

@@ -49,6 +49,12 @@ class maintenance_static_page {
} else if (PHPUNIT_TEST || defined('BEHAT_SITE_RUNNING')) {
$html = '<html></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
);

View File

@@ -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.

View File

@@ -279,11 +279,12 @@ 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
*/
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 +338,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 +369,19 @@ 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)];
$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);
}
@@ -389,6 +404,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 +412,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)) {

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="auth/outage/db" VERSION="20160922" COMMENT="XMLDB file for Moodle auth/outage"
<XMLDB PATH="auth/outage/db" VERSION="20260114" COMMENT="XMLDB file for Moodle auth/outage"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@@ -18,6 +18,7 @@
<FIELD NAME="lastmodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="When was this entry last modified."/>
<FIELD NAME="finished" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Timestamp of when the outage really finished."/>
<FIELD NAME="accesskey" TYPE="char" LENGTH="16" NOTNULL="false" SEQUENCE="false" COMMENT="Unique key used to access during outage"/>
<FIELD NAME="metadata" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="metadata to be exposed in outage page headers"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>

View File

@@ -61,5 +61,19 @@ 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;
}

View File

@@ -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';
}

View File

@@ -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";
@@ -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'] = '';
@@ -140,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';
@@ -169,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.<br />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.";

View File

@@ -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'),

View File

@@ -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([

View File

@@ -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 = "<!DOCTYPE html>\n" .
'<html><head><title>Title</title></head>' .
@@ -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('<meta http-equiv="refresh" content="5">', $generated);
}

View File

@@ -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))) {

View File

@@ -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.

View File

@@ -44,14 +44,13 @@ defined('MOODLE_INTERNAL') || die();
<?php if ($viewbag['admin']) : ?>
<?php
$adminlinks = [];
foreach (
[
$params = [
'startofwarning' => -$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',