MDL Shield

Additional tools library for MuTMS plugins

tool_mulib

Print Report
Plugin Information

A shared library plugin for the MuTMS suite providing utility classes for notifications, external database connections via PDO, context map caching for fast permission lookups, SQL fragment builder, AJAX modal forms, autocomplete form elements, JSON schema validation, and various helper utilities. It serves as the foundation for other MuTMS plugins (programs, certifications, training credits, etc.).

GitHubmutms/moodle-tool_mulibv4.5.10.06
Version:2026032945
Release:v4.5.10.06
Reviewed for:4.5
Privacy API
Unit Tests
Behat Tests
Reviewed:2026-04-14
339 files·44,909 lines
Grade Justification

The plugin is authored by an experienced Moodle developer and demonstrates strong overall code quality. All web-facing pages enforce require_login() and appropriate capability checks (moodle/site:config for admin pages, custom capability tool/mulib:useextdb for query listing). All forms use Moodle's moodleform with automatic sesskey handling. Output is consistently sanitized using s(), format_string(), and clean_text(). The Privacy API is fully implemented. Third-party libraries are properly declared in thirdpartylibs.xml.

The main findings are all low severity:

  • Direct writes to the core role_assignments table using a magic fake user ID (-999666) for default role assignment hacking. This is a deliberate workaround but modifies a core table the plugin does not own.
  • Missing db/uninstall.php to clean up the magic role assignment records on plugin removal.
  • External database credentials stored in plaintext in the tool_mulib_extdb_server table. This is architecturally unavoidable for PDO connections but increases blast radius of a database compromise.
  • PDO used for external database connections which bypasses Moodle's $DB API. This is the explicit, intentional purpose of the extdb feature, restricted to site administrators, and uses parameterized queries.

No high or critical security vulnerabilities were found. The PDO usage is the most notable pattern but is fully admin-gated and uses prepared statements, making the real-world risk minimal.

AI Summary

Plugin Overview

tool_mulib is a shared library plugin for the MuTMS suite of Moodle plugins. It provides foundational utilities used by sibling plugins (tool_muprog, tool_mucertify, tool_mutrain, tool_murelation, etc.).

Key Features

  • Notification framework — manages email/message notifications with custom templates, supervisor CC support, and import/export between instances
  • External database (extdb) — admin-configurable PDO connections to external databases with parameterized query execution
  • Context map — a cached context hierarchy for fast permission lookups across large Moodle sites, maintained via event observers and scheduled task
  • SQL fragment builder — an immutable sql class for safe composition of SQL queries with named parameters
  • AJAX modal forms — a custom form framework for rendering Moodle forms inside modal dialogs via AJAX
  • Autocomplete form elements — base classes for AJAX-powered autocomplete fields (users, cohorts, categories)
  • JSON schema validation — wrapper around opis/json-schema library
  • Upsert helper — cross-database INSERT ... ON CONFLICT/DUPLICATE KEY implementation

Security Model

All admin pages (extdb server/query management) require moodle/site:config. The query listing page requires tool/mulib:useextdb. Notification management is delegated to consuming plugins via abstract manager::can_manage() checks. All forms use moodleform with sesskey protection. Output is consistently sanitized.

Architecture

The plugin defines 5 database tables (tool_mulib_notification, tool_mulib_notification_user, tool_mulib_extdb_server, tool_mulib_extdb_query, tool_mulib_context_parent, tool_mulib_context_map), one web service, one scheduled task, and 11 event observers for context map maintenance.

Findings

code qualityLow
Direct inserts and deletes on core role_assignments table

The context_map class directly inserts and deletes records in the core role_assignments table to simulate default user role assignments. This uses a magic fake user ID (-999666) and component = 'tool_mulib' to create synthetic role assignment records.

This bypasses Moodle's role_assign() / role_unassign() API and modifies a core table the plugin does not own. While the approach is documented as a deliberate hack for performance optimization of the context map permission lookups, it creates a coupling to the internal structure of role_assignments.

Risk Assessment

Low risk. The records use a clearly non-real user ID and are tagged with component = 'tool_mulib' for identification. The direct table writes do not introduce a security vulnerability — the worst case is stale records if the default role settings change while the plugin is active. The main concern is maintainability: if Moodle changes the role_assignments table structure, this code would need updating.

Context

The add_default_role_hacks() method is called from get_contexts_by_capability_join() which is the core permission lookup method for the context map system. The magic user ID -999666 is used to represent Moodle's default user role and default front page role in the role_assignments table, allowing the context map's SQL JOINs to naturally include these implicit role assignments in permission calculations.

This is called frequently during normal operation whenever permission queries use the context map.

Identified Code
protected static function add_default_role_hacks(): void {
    global $DB, $CFG;

    $expected = [];
    if (!empty($CFG->defaultuserroleid)) {
        $syscontext = \context_system::instance();
        $expected[$syscontext->id] = (int)$CFG->defaultuserroleid;
    }
    if (!empty($CFG->defaultfrontpageroleid)) {
        $frontpagecontxt = \context_course::instance(get_site()->id);
        $expected[$frontpagecontxt->id] = (int)$CFG->defaultfrontpageroleid;
    }

    $hacks = $DB->get_records('role_assignments', ['userid' => self::MAGIC_DEFAULT_USER_ID, 'component' => 'tool_mulib']);
    foreach ($hacks as $ra) {
        if (!isset($expected[$ra->contextid]) || $expected[$ra->contextid] != $ra->roleid) {
            $DB->delete_records('role_assignments', ['id' => $ra->id]);
            continue;
        }
        unset($expected[$ra->contextid]);
    }
    foreach ($expected as $contextid => $roleid) {
        $DB->insert_record('role_assignments', [
            'contextid' => $contextid,
            'userid' => self::MAGIC_DEFAULT_USER_ID,
            'roleid' => $roleid,
            'timemodified' => time(),
            'component' => 'tool_mulib',
        ]);
    }
}
Suggested Fix

Consider using Moodle's role_assign() and role_unassign() APIs instead of direct table manipulation, or document this hack thoroughly with a comment explaining why the standard API cannot be used (e.g., because the fake user ID is not a real user).

code qualityLow
Missing db/uninstall.php for cleanup of core table records

The plugin inserts records into the core role_assignments table (finding #1) with userid = -999666 and component = 'tool_mulib'. Without a db/uninstall.php script, these synthetic records will persist after plugin uninstallation, leaving orphaned data in the core table.

Additionally, the plugin maintains two cache tables (tool_mulib_context_parent and tool_mulib_context_map) whose data references core context IDs. While these tables would be dropped on uninstall, the role_assignments records would not.

Risk Assessment

Low risk. The orphaned records are small in number (at most 2 records — one for default user role, one for front page role) and tagged with component = 'tool_mulib'. They would not cause functional issues but represent leftover data that should be cleaned up on uninstall.

Context

Moodle automatically drops plugin-owned tables on uninstall, but records inserted into core tables by the plugin are not automatically cleaned up. The role_assignments records with the magic user ID would remain indefinitely after uninstall, potentially causing confusion or minor performance overhead in role assignment queries.

best practiceLow
External database credentials stored in plaintext

The tool_mulib_extdb_server table stores database connection credentials (dbuser and dbpass fields) as plaintext in the Moodle database. Anyone with direct database access to the Moodle database can read all external database passwords.

While this is architecturally unavoidable for PDO connections (the credentials must be available in cleartext to establish connections), it increases the blast radius of a Moodle database compromise — an attacker who gains read access to the Moodle database also gains credentials to all configured external databases.

Risk Assessment

Low risk. Only site administrators can create and view server configurations. The plaintext storage follows Moodle's own pattern for database credentials. The main concern is that a database-level breach (e.g., SQL injection in another plugin, database backup exposure) would expose credentials to external systems. Using Moodle's encrypt() would add a layer of defense-in-depth.

Context

The external database server configuration is managed exclusively by site administrators (moodle/site:config capability). The credentials are used by the pdb class to establish PDO connections. The DSN, username, and password are all needed in cleartext at connection time.

Moodle core stores its own database password in config.php as plaintext, so this follows the same pattern. However, the external DB credentials represent additional attack surface beyond the Moodle database itself.

Identified Code
<FIELD NAME="dbuser" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="DB user name"/>
<FIELD NAME="dbpass" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="DB user password"/>
Suggested Fix

Consider encrypting the dbpass field at rest using Moodle's encrypt() / decrypt() functions (available since Moodle 3.11). This would not prevent access by someone with Moodle config.php access (since the encryption key is there), but it would protect against database-only breaches.

// When storing:
$data->dbpass = encrypt($data->dbpass);

// When reading:
$server->dbpass = decrypt($server->dbpass);
code qualityLow
PDO used for direct external database connections

The pdb class creates direct PDO connections to external databases, bypassing Moodle's $DB API entirely. This is the explicit, intended purpose of the extdb feature — connecting to databases outside of Moodle.

The implementation properly uses PDO prepared statements with named parameter binding via $this->pdo->prepare($sql, []) and $statement->execute($pdoparams). SQL queries are stored in the database and managed exclusively by site administrators.

Risk Assessment

Low risk. While PDO usage formally bypasses Moodle's $DB API (which is listed as a critical-category pattern), the actual risk is minimal because:

  • Only site administrators can configure connections and queries
  • Administrators already have full system access including the ability to execute arbitrary PHP code
  • PDO prepared statements with parameter binding prevent SQL injection
  • The connections are to external databases, not the Moodle database
  • The DSN, credentials, and SQL are all admin-configured, not user-supplied

This is the intentional core functionality of the plugin, not a security bypass.

Context

The entire extdb subsystem is gated behind moodle/site:config capability for all CRUD operations (server creation, query creation, query execution). The SQL queries stored in tool_mulib_extdb_query.sqlquery are written by administrators. The pdb::query() method uses PDO prepared statements with proper parameter binding.

The fix_pdo_params() method extracts named parameters from SQL using regex and maps them to the provided parameter array, ensuring only expected parameters are bound.

Identified Code
try {
    $this->pdo = new PDO($this->dsn, $this->dbuser, $this->dbpass, $this->dboptions);
    return;
} catch (PDOException $ex) {
    $this->pdo = false;
    throw $ex;
}
Identified Code
public function query(string $sql, array $params = []): rs {
    $this->require_connection();
    $pdoparams = $this->fix_pdo_params($sql, $params);
    $statement = $this->pdo->prepare($sql, []);
    $statement->execute($pdoparams);
    return new rs($statement);
}
best practiceInfo
Duplicated upsert_record implementation across two classes

The mulib class and the mudb class contain identical implementations of upsert_record(), upsert_record_pgsql(), upsert_record_mysql(), and validate_upsert_record_arguments(). This appears to be a refactoring artifact where the code was moved to a dedicated mudb helper class but the original copy in mulib was retained.

The internal code (context_map_builder) uses mudb::upsert_record(), while mulib::upsert_record() may be used by other MuTMS suite plugins.

Risk Assessment

No risk. This is purely a code maintainability observation. Both implementations are correct and use parameterized queries. The duplication means a bug fix would need to be applied in two places.

Context

Both mulib and mudb are final classes. The mudb class was likely created as a dedicated database helper to separate concerns, but the original methods in mulib were not removed — possibly because external MuTMS plugins depend on them.

Suggested Fix

If both classes need to expose this functionality, consider having mulib::upsert_record() delegate to mudb::upsert_record() to eliminate code duplication.

Third-Party Libraries (4)
LibraryVersionLicenseDeclared
opis/json-schema
JSON Schema validation used by the json_schema utility class for validating structured data
2.6.0Apache-2.0
opis/string
Unicode string handling dependency of opis/json-schema
2.1.0Apache-2.0
opis/uri
URI handling dependency of opis/json-schema
1.1.0Apache-2.0
Composer
Autoloader for the bundled opis libraries
2.8.8MIT
Additional AI Notes

The plugin is authored by Petr Skoda, a long-time Moodle core developer, and the code quality reflects deep expertise with Moodle internals. The sql class for immutable SQL fragment composition with safe parameter merging is a particularly well-designed pattern that prevents SQL injection at the structural level.

The context map system (context_map, context_map_builder) is a sophisticated caching layer that pre-computes context hierarchies for fast permission lookups. It maintains two cache tables via event observers and a nightly scheduled task. The MAGIC_DEFAULT_USER_ID hack is the primary code quality concern — it works correctly but couples tightly to role_assignments internals.

The notification framework properly delegates access control to consuming plugins via abstract manager::can_manage() and manager::can_view() methods. Notification text is sanitized through s() for subjects and clean_text() for bodies. The filter_multilang method applies only safe multilang filters and explicitly avoids other text filters.

All Mustache templates use double-curly-brace syntax ({{variable}}) for auto-escaping. The {{{icon}}} triple-curly usages are for pre-rendered icon HTML from Moodle's icon system, which is the standard Moodle pattern.

The plugin has comprehensive test coverage including PHPUnit tests for most classes and Behat test infrastructure with custom generators and step definitions.

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