Certifications
tool_mucertify
MuTMS Certifications plugin (`tool_mucertify`) provides a full certification management system for Moodle. It supports creating certifications with configurable periods, multiple assignment sources (manual, self-assignment, approval-based, and cohort-based), recertification workflows, certificate issuance via `tool_certificate`, custom fields, notifications, tagging, multi-tenancy integration via `tool_mutenancy`, and a learner-facing catalogue. The plugin uses Moodle's Report Builder for listing UIs and includes comprehensive Behat and PHPUnit tests.
The plugin is exceptionally well-written and follows Moodle security and coding standards throughout. All pages enforce require_login() and appropriate capability checks (tool/mucertify:view, tool/mucertify:edit, tool/mucertify:assign, tool/mucertify:admin, tool/mucertify:delete). State-changing operations are protected against CSRF via Moodle's moodleform / ajax form infrastructure. All database queries use $DB parameterized methods. File serving is properly gated through tool_mucertify_pluginfile() with access checks. The Privacy API is fully implemented with metadata, export, and delete support.
The only findings are two low-severity code quality issues: one use of the $_REQUEST superglobal (with documented rationale and proper clean_param() sanitization), and one instance of a time() integer being interpolated directly into a SQL string rather than using a named parameter. Neither finding poses a security risk.
No third-party libraries are bundled. No deprecated APIs were identified. The plugin has comprehensive test coverage across unit tests, Behat tests, and a test data generator.
Overview
tool_mucertify is a substantial, well-architected Moodle admin tool plugin providing a complete certification lifecycle management system. It is part of the MuTMS suite and depends on tool_mulib, tool_muprog, enrol_muprog, block_muprog_my, block_muprogmyoverview, and block_mucertify_my.
Key Components
- Certification management — CRUD operations with archive/restore, move between contexts, period settings, recertification configuration, certificate template assignment
- Assignment sources — Manual, self-assignment, approval-based, and cohort-based automatic assignment
- Certification periods — Time-windowed certification periods with configurable due dates, expiration, validity rules, and recertification
- Certificate issuance — Integration with
tool_certificatefor automated PDF certificate generation - Catalogue — Learner-facing browsing UI with search, tagging, visibility controls (public/cohort-based)
- Notifications — Assignment, unassignment, validity, and approval workflow notifications
- Web services — External functions for certifications, assignments, periods, and form autocomplete
- Report Builder — System reports for certifications, assignments, periods, and requests
- Privacy API — Full GDPR compliance with metadata, export, and deletion
- Custom fields — Both certification-level and assignment-level custom field handlers
- Multi-tenancy — Integration with
tool_mutenancyfor tenant-scoped visibility
Security Assessment
The plugin demonstrates strong security practices throughout:
- Every page-level script calls
require_login()and enforces appropriate capabilities - All state-changing operations use Moodle forms (which handle sesskey validation)
- Database access exclusively uses the
$DBAPI with parameterized queries - File serving includes proper access control checks
- User input is consistently sanitized via
required_param(),optional_param(), andclean_param() - Upload processing validates file types and user mappings to prevent injection
- The Privacy API implementation is thorough and handles all user data tables
Findings
The catalogue index page passes $_REQUEST directly to the catalogue class constructor instead of using Moodle's required_param() / optional_param() functions. While the constructor does sanitize each value individually using clean_param(), Moodle coding standards require using the dedicated parameter retrieval functions.
The developer included a comment explaining the rationale: the catalogue is read-only and they want to support URL bookmarking. This mitigates any CSRF concern, and the clean_param() usage prevents injection. However, it remains a coding standards deviation.
Low risk. The values are properly sanitized within the constructor using clean_param(). The page is read-only, so there is no CSRF concern. This is a coding standards violation rather than a security risk. The developer explicitly documented the rationale for this approach.
The catalogue class constructor receives the array and processes each key using clean_param() with appropriate PARAM_* types (PARAM_INT for page/perpage, PARAM_RAW for searchtext). The page is read-only — it only displays certifications visible to the current user. No data modifications occur.
$catalogue = new \tool_mucertify\local\catalogue($_REQUEST);
Use optional_param() for each expected parameter and pass them as an array:
$params = [
'page' => optional_param('page', 0, PARAM_INT),
'perpage' => optional_param('perpage', 10, PARAM_INT),
'searchtext' => optional_param('searchtext', null, PARAM_RAW),
];
$catalogue = new \tool_mucertify\local\catalogue($params);
In the allocation_completed() method, the $now variable (result of time()) is directly interpolated into the SQL string using PHP string interpolation instead of being passed as a named parameter placeholder.
While not a security vulnerability — $now is always an integer produced by time() and cannot contain user input — Moodle coding standards require all variables in SQL to use parameterized placeholders for consistency and to prevent accidental injection if the code is later refactored.
Low risk. The interpolated variable $now is guaranteed to be an integer from time(). There is no user input involved and no realistic exploit path. This is a coding style issue — other queries in the same plugin (e.g., process_recertifications()) correctly use named parameters for the same time() values. Consistent use of parameterized queries is a best practice that guards against future refactoring mistakes.
The allocation_completed() method is called from an event observer when a program allocation is completed. The $now variable is set to time() (always a Unix timestamp integer) and is used to filter certification periods by their window dates. The rest of the query uses proper named parameters.
$now = time();
$sql = "SELECT p.*
FROM {tool_mucertify_period} p
JOIN {tool_mucertify_assignment} a ON a.userid = p.userid AND a.certificationid = p.certificationid
JOIN {tool_mucertify_certification} c ON c.id = p.certificationid
JOIN {user} u ON u.id = p.userid
WHERE a.archived = 0 AND c.archived = 0 AND u.deleted = 0
AND p.timecertified IS NULL AND p.timerevoked IS NULL
AND p.timewindowstart <= $now AND (p.timewindowend IS NULL OR p.timewindowend > $now)
AND p.userid = :userid AND p.programid = :programid";
Use named parameters for the $now values:
$now = time();
$sql = "SELECT p.*
FROM {tool_mucertify_period} p
JOIN {tool_mucertify_assignment} a ON a.userid = p.userid AND a.certificationid = p.certificationid
JOIN {tool_mucertify_certification} c ON c.id = p.certificationid
JOIN {user} u ON u.id = p.userid
WHERE a.archived = 0 AND c.archived = 0 AND u.deleted = 0
AND p.timecertified IS NULL AND p.timerevoked IS NULL
AND p.timewindowstart <= :now1 AND (p.timewindowend IS NULL OR p.timewindowend > :now2)
AND p.userid = :userid AND p.programid = :programid";
$params = [
'now1' => $now,
'now2' => $now,
'userid' => $allocation->userid,
'programid' => $program->id,
];
The plugin has excellent test coverage with 17 Behat feature files covering all major workflows (allocation sources, management, history upload, visibility, certificates) and extensive PHPUnit tests covering business logic classes, event classes, external functions, custom field handlers, and the privacy provider.
The capability model is well-designed with granular permissions: viewcatalogue (learner browsing), viewusercertifications (viewing others' certifications), view (management access), edit (create/update certifications), delete (delete certifications), assign/unassign (user assignment management), admin (advanced operations like history upload and period management), and configurecustomfields (custom field configuration). Risk bitmasks are correctly applied where appropriate.
The plugin properly uses Moodle's File API for all file operations — certification description files, images, and uploaded CSV data are all managed through get_file_storage(), file_prepare_standard_editor(), file_postupdate_standard_editor(), and file_save_draft_area_files(). Temporary upload data is stored in the file system with proper cleanup via a scheduled task.
The web services are properly declared with loginrequired: true for AJAX autocomplete functions, and capabilities declarations for the main external functions. The external function implementations validate context and check capabilities before returning data.
All management pages use the AJAX form pattern (defining AJAX_SCRIPT and using ajax_form_render() / ajax_form_submitted() / ajax_form_cancelled()), which provides CSRF protection via Moodle's form sesskey handling. The forms are rendered in modals for a modern UX.