My programs overview page
block_muprogmyoverview
A block plugin that provides a 'My programs' overview page for users to view, filter, sort, search, favourite, and hide their allocated training programs. It is part of the MuTMS suite and depends on `tool_mulib` and `tool_muprog`. The plugin registers a navigation hook to add a 'My programs' entry to the primary menu, exposes two AJAX web services (fetching programs and toggling favourites), and stores user display preferences.
The plugin is well-written and follows Moodle security and coding standards throughout. All SQL queries use parameterized placeholders or safely interpolated integer values. The index.php page calls require_login() before processing. Both external web services validate parameters via validate_parameters(), validate context, and require login. The AJAX service definitions handle sesskey validation automatically. Output is properly sanitized using format_string(), format_text(), and appropriate PARAM_* return types. The Privacy API is implemented, and the plugin includes comprehensive PHPUnit and Behat tests.
The only findings are low-severity code quality issues: an N+1 query pattern in the get_active_programs external service, hardcoded English strings in JavaScript notification alerts, and an unused variable in the upgrade script. None of these represent security vulnerabilities or significant risks.
Overview
block_muprogmyoverview is a dashboard block plugin that gives users a filterable, sortable overview of their allocated training programs. It is part of the MuTMS plugin suite and depends on tool_mulib and tool_muprog.
Architecture
index.php— standalone page at/blocks/muprogmyoverview/withrequire_login(), capability checks for management links, and block region setup- Two AJAX web services —
get_active_programs(read) andset_favourite_program(write), both requiring login and using Moodle's external API with full parameter validation - Mustache templates — card, list, and description views with proper use of
{{escaped}}and{{{unescaped}}}output (unescaped only for pre-sanitized HTML fromformat_string()/format_text()) - AMD JavaScript — client-side paging, searching, favouriting, and hiding programs via Moodle's AJAX framework
- Navigation hook — adds "My programs" to the primary navigation menu when active programs exist
- Privacy API — implemented with metadata and user preference export
Security Assessment
No security vulnerabilities were identified. The plugin:
- Uses parameterized SQL throughout (via
tool_mulib\local\sqlhelper and$DBmethods) - Properly escapes search input with
$DB->sql_like_escape() - Validates all external API parameters with
PARAM_*types and explicit allowlists - Requires login on all entry points
- Uses Moodle's favourites API for favourite management
- Properly sanitizes output with
format_string(),format_text(), ands()equivalents
Findings Summary
Only low-severity code quality issues were found:
- N+1 database query pattern in the programs listing external service
- Hardcoded English strings in JavaScript notification alerts
- Unused
$dbmanvariable in the upgrade script
Findings
The execute() method fetches program records with a JOIN on the allocation table, but then makes a separate $DB->get_record() call inside the foreach loop for each program to re-fetch the same allocation data. The initial query already JOINs {tool_muprog_allocation} a, but only selects p.* (program columns) and f.id AS favid, discarding the allocation columns (a.timestart, a.timedue, a.timeend, a.timecompleted) that are needed later.
This results in N+1 queries — one initial query plus one per program — where a single query selecting the needed allocation columns would suffice.
Low risk. This is a performance concern, not a security issue. For users with a small number of program allocations (typical case), the overhead is negligible. For users with many allocations (e.g. 50+), the extra queries add measurable latency. The web service already uses $limit and $offset for pagination, which bounds the per-request impact to the page size (typically 12-96 programs).
The get_active_programs web service is called via AJAX every time the user loads, filters, sorts, searches, or pages through their program list. The initial SQL query joins the allocation table (a) and the program table (p) but only selects program columns (p.*). Inside the foreach loop, allocation data (timestart, timedue, timeend, timecompleted) is needed for date formatting, status calculation, and progress computation — leading to a redundant per-program query.
foreach ($programs as $program) {
$allocation = $DB->get_record('tool_muprog_allocation', ['userid' => $USER->id, 'programid' => $program->id]);
Include the required allocation fields in the initial SELECT statement:
"SELECT p.*, a.timestart AS alloc_timestart, a.timedue AS alloc_timedue, a.timeend AS alloc_timeend, a.timecompleted AS alloc_timecompleted, f.id AS favid
FROM {tool_muprog_allocation} a
JOIN {tool_muprog_program} p ON p.id = a.programid
..."
Then use the already-fetched allocation data inside the loop instead of querying again for each program.
$sql = new sql(
"SELECT p.*, f.id AS favid
FROM {tool_muprog_allocation} a
JOIN {tool_muprog_program} p ON p.id = a.programid
LEFT JOIN {favourite} f ON f.itemid = p.id AND f.userid = a.userid AND f.component = 'tool_muprog' AND f.itemtype = 'programs'
WHERE a.userid = :userid AND a.archived = 0 AND p.archived = 0
Two Notification.alert() calls use hardcoded English strings instead of Moodle's get_string() / getString() internationalization API. This means the error messages will always appear in English regardless of the user's language preference.
Low risk. This is a code quality / internationalization issue. It does not affect security or functionality. Users with non-English language packs will see English error messages for this specific failure case, which is a minor UI inconsistency.
These notification alerts are shown when the AJAX call to toggle a program's favourite status succeeds but returns warnings. The addToFavourites and removeFromFavourites functions both contain identical hardcoded strings for the error case.
Notification.alert('Starring program failed', 'Could not change favourite state');
Use Moodle's string API:
import {getString} from 'core/str';
// ...
getString('error_favourite', 'block_muprogmyoverview').then(msg => {
Notification.alert(msg);
});
And add corresponding language strings to lang/en/block_muprogmyoverview.php.
Notification.alert('Starring program failed', 'Could not change favourite state');
Same fix as above — replace with Moodle string API calls.
The upgrade function calls $DB->get_manager() and assigns the result to $dbman, but this variable is never used anywhere in the function. The only upgrade step uses $DB->set_field() which does not require the database manager.
Informational. Dead code with no functional or security impact. The $DB->get_manager() call is safe — it just returns the database schema manager object without performing any operations.
The upgrade function contains a single upgrade step that updates the contextid field in the favourite table. This operation uses $DB->set_field() which is a standard DML method and does not require the DDL manager. The $dbman variable appears to be a leftover from a removed or planned upgrade step.
$dbman = $DB->get_manager();
Remove the unused line:
function xmldb_block_muprogmyoverview_upgrade($oldversion) {
global $DB;
if ($oldversion < 2026022045.02) {
The plugin has comprehensive test coverage with PHPUnit tests for the block class, both external services, the utility class, the event, and the privacy provider, as well as a Behat step definition class for acceptance testing.
The plugin uses a custom SQL builder (tool_mulib\local\sql) with a comment-replacement pattern for dynamic query construction. This approach maintains parameterized queries while allowing conditional SQL fragments. The pattern is safe — user-controlled values always go through $DB->sql_like_escape() and parameterized placeholders.
The Privacy API implementation properly exports all user preferences including dynamically-named hidden program preferences via regex pattern matching. The block_muprogmyoverview_user_preferences() function correctly defines all preference types with PARAM_* validation, choice constraints, and permission callbacks.
The set_favourite_program external service does not verify that the calling user has an active allocation to the target program before toggling the favourite. This is by design — the impact is negligible since favourites only affect the user's own view, and orphaned favourites would simply have no visible effect. The cleanup_hidden_programs() method handles stale hidden-program preferences, but no equivalent cleanup exists for orphaned favourites.
All SQL interpolation of variables into queries uses safe integer values — $now = time() always returns an integer, and $hiddenprograms IDs are extracted via regex matching (\d+) and cast with (int). While using named parameters would be more idiomatic, the current approach is functionally safe.