Maintenance mode (live check)
local_maintenance_livecheck
A Moodle local plugin that shows Moodle's built-in maintenance-mode announcement to users without requiring a full page reload. It injects an AMD module that periodically polls a lightweight AJAX endpoint and renders a countdown banner when CLI maintenance is scheduled, or an immediate notice when CLI/legacy maintenance is active. Behaviour is fully admin-configurable: an enable flag, the poll interval, a back-off delay, and the weekdays/time window during which polling occurs.
The plugin is exemplary. There are no security findings and no code-quality or compliance violations.
It performs no database, filesystem, SQL, shell, or HTTP operations whatsoever; the only request-handling endpoint (ajax.php) is intentionally public, changes no state, and exposes only non-sensitive maintenance-scheduling status that Moodle core already displays to users on page load. All client-side DOM writes use trusted core language strings, so there is no XSS exposure.
Supporting practices are complete: a null_provider Privacy API implementation, a full English language pack for every setting, a Behat test covering the primary user story, and a compiled AMD bundle that is consistent with its source. No third-party libraries are bundled.
The sole observation is an info-level architectural note that the page-wide JavaScript is injected via the *_extend_navigation() callback rather than the modern output hooks — a deliberate, documented choice that is functional and carries no risk.
local_maintenance_livecheck is a small, mature local plugin that surfaces Moodle's maintenance-mode warning to users without a full page reload. A navigation callback injects an AMD module on most pages; that module polls a lightweight endpoint (ajax.php) and renders a countdown banner when CLI or legacy maintenance is scheduled or active.
The codebase is clean and follows Moodle conventions throughout:
- No database, filesystem, SQL, shell, or HTTP access of any kind — the plugin only reads
$CFGflags and its own admin-set configuration. - No untrusted input reaches a sink.
ajax.phpignores its only request parameter and returns a single integer ornull; the JavaScript writes only core language strings into the DOM. - Privacy API is implemented as a
null_provider(the plugin stores no personal data), language strings are complete, and a Behat test covers the main user story.
The single finding below is an info-level architectural note, not a defect. The unauthenticated ajax.php endpoint was reviewed and found safe: it is intentionally public (documented), performs no state change, and discloses only whether maintenance mode is scheduled or active — information Moodle core already shows to users on page load.
Findings
The plugin makes its banner appear on (nearly) every page by implementing local_maintenance_livecheck_extend_navigation() and calling $PAGE->requires->js_call_amd() from inside it.
The *_extend_navigation() callback exists to add nodes to Moodle's navigation tree; using it purely as a hook to inject page-wide JavaScript is a deliberate workaround. The author documents this candidly in README.md and in the function's own docblock ("by abusing Moodle's *_extend_navigation() hook").
Since Moodle 4.4 there is a dedicated, supported mechanism for emitting markup or JavaScript on every page — the output hooks \core\hook\output\before_standard_head_html_generation and \core\hook\output\before_footer_html_generation, registered via db/hooks.php. These fire more consistently across page types than the navigation callback, which is only invoked when the global navigation is actually built. The README already acknowledges the resulting limitation: the banner cannot appear on some pages (for example a SCORM activity opened in a new window).
This is not a security or correctness defect — the current approach is functional and intentional. Migrating to an output hook would be a modernisation that improves page coverage and removes the documented reliance on the navigation API for an unrelated purpose.
Info only. This is an architectural/maintainability observation, not an exploitable issue or a coding-standard violation. The navigation-callback approach is deliberate, documented, requires no user input, and carries no security risk. The only practical downside is the one the author already acknowledges — the banner is not guaranteed to appear on every page type. No action is required for correctness or security.
local_maintenance_livecheck_extend_navigation() reads the plugin configuration with get_config('local_maintenance_livecheck'), evaluates whether the current server day/time falls within the configured live-check window (using core usergetdate() and make_timestamp()), and, when appropriate, calls $PAGE->requires->js_call_amd('local_maintenance_livecheck/livecheck', 'init', [$jsoptions]) to load the polling module. There is no user input involved — all decision inputs are admin-controlled settings or server time.
function local_maintenance_livecheck_extend_navigation(global_navigation $navigation) {
global $CFG, $PAGE;
Consider registering an output hook instead of relying on the navigation callback. For example, add a db/hooks.php:
$callbacks = [
[
'hook' => \core\hook\output\before_footer_html_generation::class,
'callback' => '\local_maintenance_livecheck\local\hook_callbacks::before_footer_html_generation',
],
];
and move the existing config checks and the $PAGE->requires->js_call_amd(...) call into that callback method. This is the supported way to inject page-wide assets in current Moodle and fires on more page types than *_extend_navigation().
No change is required for correctness or security — this is purely a modernisation suggestion.
Unauthenticated endpoint reviewed and found safe. ajax.php deliberately omits require_login() (documented via a phpcs:disable moodle.Files.RequireLogin.Missing comment). It performs no state change, ignores its only POST parameter (time, a cache-buster), and returns a single value: null (no maintenance), a positive integer (seconds until scheduled maintenance), or 0 (legacy maintenance active). The disclosed information — whether maintenance is scheduled or active — is non-sensitive and is already shown by Moodle core to users on page load, so neither require_login() nor require_sesskey() is warranted here.
No risky APIs anywhere in the plugin. There is no database access, no filesystem access, no raw SQL, no shell/eval, no HTTP/curl calls, and no direct $_GET/$_POST/$_SESSION superglobal use. Server-side input handling is limited to reading admin-controlled $CFG and plugin config. The weekday-matching logic substr($config->livecheckweekdays, $date['wday'], 1) in lib.php correctly matches the 0-to-6 string format produced by admin_setting_configmulticheckbox2 (verified against core), where index 0 is Sunday.
Client-side output is XSS-safe. The AMD module writes to the DOM via boxContent.html(s), but s is always a trusted Moodle core language string (sitemaintenance, maintenancemodeisscheduled, maintenancemodeisscheduledlong from the admin component) whose only placeholders are integers computed with Math.floor(). No attacker-controllable data reaches .html(), and the AJAX response value (timeleftinsec) is only ever used in arithmetic and comparisons, never inserted into markup.
Good engineering hygiene. The plugin ships a null_provider Privacy API implementation, a complete English language pack covering every setting, a Behat test exercising the primary user story, and a compiled AMD bundle (amd/build/livecheck.min.js) that is consistent with its source (amd/src/livecheck.js). No third-party libraries are bundled, so the absence of thirdpartylibs.xml is correct. version.php declares $plugin->supported = [501, 501] and an appropriate requires value for the targeted Moodle 5.1 release.