Programs
tool_muprog
Programs management plugin (tool_muprog) for MuTMS suite. Provides program creation, allocation of users to programs through multiple sources (manual, self-allocation, cohort sync, approval workflow, external database, certification-based, and program-completion-based), program content structure management (courses, sets, attendance, credits), notifications, export/import in CSV and JSON formats, catalogue browsing for learners, and integration with calendar, certificates, custom fields, report builder, and multi-tenancy.
This plugin is very well written by an experienced Moodle developer. Every page properly calls require_login() and require_capability() with appropriate capability checks. All state-changing operations go through Moodle forms (which handle sesskey validation automatically) or through web service external functions (which validate parameters and contexts). The $DB API is used exclusively with parameterized queries throughout — no SQL injection vectors were found. Output is consistently sanitized via format_string(), format_text(), s(), and Mustache auto-escaping. File operations use the Moodle File API and make_request_directory() for temp files. The upgrade script correctly confines DDL to db/upgrade.php. The Privacy API is fully implemented. No third-party libraries are bundled. The only findings are two low-severity code quality issues: direct use of $_REQUEST in catalogue/index.php and $_POST in management/export.php, both of which are mitigated by subsequent sanitization via clean_param() and form API processing respectively.
Plugin Overview
tool_muprog is a comprehensive Programs management plugin for the MuTMS suite for Moodle. It allows administrators and managers to create structured learning programs composed of courses, sets (groups of courses), attendance items, and credit framework items. Users can be allocated to programs through seven different allocation sources: manual, self-allocation, cohort sync, approval workflow, external database queries, certification-based, and program-completion-based.
Security Assessment
The plugin demonstrates excellent security practices throughout:
- Authentication & Authorization: Every page calls
require_login()andrequire_capability()with granular capabilities (tool/muprog:view,tool/muprog:edit,tool/muprog:allocate,tool/muprog:admin, etc.) - CSRF Protection: All state-changing operations use Moodle forms (
moodleform/dynamic_form) which handle sesskey validation automatically. AJAX pages usedefine('AJAX_SCRIPT', true)with form processing - SQL Safety: All database queries use parameterized
$DBAPI methods. Theget_programsweb service validates field names against a strict whitelist before building SQL - Output Escaping: Consistent use of
format_string(),format_text(),s(), and Mustache auto-escaping for user-facing output - File Handling: Uses Moodle File API (
get_file_storage(),send_stored_file()) for persistent files andmake_request_directory()+ native PHP functions for temporary file operations - Web Services: All external functions validate parameters, validate context, and check capabilities
Code Quality
The codebase is well-structured with clear separation between:
- Page scripts (
catalogue/,management/,my/) - Business logic (
classes/local/) - External API (
classes/external/) - Report builder entities (
classes/reportbuilder/) - Event classes, task classes, and custom field handlers
Extensive test coverage with both PHPUnit and Behat tests. Privacy API is fully implemented. Uninstall handler properly cleans up all plugin data.
Findings
The catalogue index page passes the raw $_REQUEST superglobal directly to the catalogue class constructor. While the constructor properly sanitizes each value using clean_param(), Moodle coding standards recommend using required_param() / optional_param() instead of accessing PHP superglobals directly.
The developer has included a comment explaining the rationale: the catalogue is read-only and they want to allow bookmarkable URLs. Despite this, the standard Moodle parameter functions (optional_param()) also support GET parameters for bookmarking.
Low risk. All values from $_REQUEST are sanitized via clean_param() before use. The searchtext parameter uses PARAM_RAW but is subsequently escaped with $DB->sql_like_escape() for SQL queries, and output is handled through format_string() and Mustache auto-escaping. There is no exploitable vulnerability here — this is a coding standards issue only.
The catalogue index page is a read-only listing page for learners to browse available programs. The $_REQUEST array is passed to the catalogue class constructor, which extracts page, perpage, and searchtext parameters. All values are sanitized with clean_param() before use. The searchtext value is later used safely with $DB->sql_like_escape() for database queries.
$catalogue = new \tool_muprog\local\catalogue($_REQUEST);
Replace with individual optional_param() calls and pass the values to the constructor:
$page = optional_param('page', 0, PARAM_INT);
$perpage = optional_param('perpage', 10, PARAM_INT);
$searchtext = optional_param('searchtext', null, PARAM_RAW);
$catalogue = new \tool_muprog\local\catalogue(['page' => $page, 'perpage' => $perpage, 'searchtext' => $searchtext]);
public function __construct(array $request) {
// NOTE: we do not care about CSRF here, because there are no data modifications in Catalogue,
// we DO want to allow and encourage bookmarking of catalogue URLs.
if (isset($request['page'])) {
$page = clean_param($request['page'], PARAM_INT);
if ($page > 0) {
$this->page = $page;
}
}
if (isset($request['perpage'])) {
$perpage = clean_param($request['perpage'], PARAM_INT);
if ($perpage > 0) {
$this->perpage = $perpage;
}
}
if (isset($request['searchtext'])) {
$searchtext = clean_param($request['searchtext'], PARAM_RAW);
if (\core_text::strlen($searchtext) > 1) {
$this->searchtext = $searchtext;
}
}
}
The export page accesses $_POST['format'] directly before config.php is fully loaded. This is used to conditionally define NO_DEBUG_DISPLAY to suppress debug output when sending a file download.
While this is a common Moodle pattern for download pages and the actual form value is validated through the Moodle forms API later, it technically violates the Moodle coding standard of using required_param() / optional_param() exclusively.
Low risk. The $_POST['format'] access is only used as a boolean check (non-empty) to define a debug suppression constant. The value itself is not used in any output, query, or file operation. The actual export format is validated later through Moodle's form API. This is a coding standards observation, not a security vulnerability.
The export page sends a file download (ZIP containing CSV or JSON data). When the form is submitted with a format value, debug output must be suppressed to avoid corrupting the binary file download. The NO_DEBUG_DISPLAY constant must be defined before config.php is loaded. The actual format value is later validated through the Moodle form API ($form->get_data()) which ensures sesskey validation and proper parameter typing.
if (!empty($_POST['format'])) {
define('NO_DEBUG_DISPLAY', true);
}
This is a known pattern in Moodle core for download pages where NO_DEBUG_DISPLAY must be defined before config.php processes output. No practical alternative exists since optional_param() is not available before config.php is loaded. However, if this constant definition can be moved after config.php, the standard parameter functions could be used instead.
The plugin has a comprehensive test suite with extensive PHPUnit tests covering all major features (allocations, content types, sources, notifications, course reset, certificate integration, upload/export) and Behat acceptance tests covering user workflows. This is commendable for a plugin of this complexity.
The plugin properly implements the Privacy API (GDPR provider) with full get_metadata(), get_contexts_for_userid(), get_users_in_context(), export_user_data(), and all three deletion methods (delete_data_for_all_users_in_context, delete_data_for_user, delete_data_for_users). The export covers allocations, completions, attendance records, evidence, and approval requests.
The plugin includes a proper db/uninstall.php that cleanly removes all plugin data (attendance, evidence, completions, allocations, requests, sources, cohorts, prerequisites, items, files, tags, programs, and enrolment instances) within a transaction.
All web service functions are properly defined with loginrequired => true, appropriate capability declarations, and AJAX-only flags for form autocomplete helpers. The external functions validate parameters, validate context, and check capabilities before performing operations.
The external database allocation source (source/extdb.php) uses the tool_mulib external database query framework rather than making direct database connections, which is the correct architectural approach.