MDL Shield

Maintenance mode (live check)

local_maintenance_livecheck

Print Report
Plugin Information

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.

Privacy API
Unit Tests
Behat Tests
Reviewed:2026-05-26
8 files·599 lines
Grade Justification

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.

AI Summary

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 $CFG flags and its own admin-set configuration.
  • No untrusted input reaches a sink. ajax.php ignores its only request parameter and returns a single integer or null; 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

best practiceInfo
Page-wide JavaScript is injected via the navigation callback rather than a dedicated output hook

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.

Risk Assessment

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.

Context

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.

Identified Code
function local_maintenance_livecheck_extend_navigation(global_navigation $navigation) {
    global $CFG, $PAGE;
Suggested Fix

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.

Additional AI Notes

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.

This review was generated by an AI system and may contain inaccuracies. Findings should be verified by a human reviewer before acting on them.