MDL Shield

Restriction by section completion

availability_sectioncompleted

Print Report
Plugin Information

Availability condition that restricts activities, resources and sections until the completion-tracked activities of another section (optionally including its subsections) are completed, passed or failed, with an optional minimum number of activities.

Privacy API
Unit Tests
Behat Tests
Reviewed:2026-10-14
11 files·1,829 lines
Grade Justification

No security vulnerabilities were found. The plugin has no pages, web services, forms or file handling of its own. It plugs into core's restrict-access framework, so core controls who can edit conditions and when they are evaluated.

  • Input validation: the condition constructor accepts only an integer section id, a whitelisted state and a bounded integer minimum, so tampered availability JSON is rejected before use.
  • Database access: the one direct query (get_records_select_menu() on course_modules_completion) is parameterised through get_in_or_equal().
  • Output: section names pass through format_string() before they reach the YUI form or the description callback.

The only finding is a low-severity logic gap: a stored completion state that core can produce (COMPLETION_COMPLETE_FAIL_HIDDEN) is not counted as completed. This affects restriction behaviour in an edge case and has no security impact. The plugin also has good PHPUnit and Behat coverage and a correct null privacy provider.

AI Summary

availability_sectioncompleted is a small availability condition plugin with two PHP classes (condition and frontend), a null privacy provider, a YUI form module, and PHPUnit and Behat tests. It lets teachers make an item depend on the completion of another section's activities, optionally requiring only a minimum number of them or a pass or fail state.

The security posture is clean. The plugin has no entry points of its own; condition JSON is strictly type-checked in the constructor; the single direct database read is fully parameterised; and the section names injected into the editing form's HTML are escaped server-side with format_string(). Restore handling correctly remaps section ids through core's backup ids table and checks course ownership before keeping an unmapped id.

The one item worth acting on is functional rather than security-related. An activity whose stored completion state is COMPLETION_COMPLETE_FAIL_HIDDEN (a completed activity with a failing grade on a hidden grade item) is not counted as completed. As a result, an "all activities" restriction can stay locked for those students even though they completed the activity.

Findings

code qualityLow
COMPLETION_COMPLETE_FAIL_HIDDEN state is not counted as completed

counts() treats an activity as completed in the default complete mode only when its state is COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS or COMPLETION_COMPLETE_FAIL. Core also stores COMPLETION_COMPLETE_FAIL_HIDDEN (4) in course_modules_completion.completionstate. This happens when an activity requires only a grade, the grade item has a pass mark and is hidden, and the student's grade is below the pass mark. For such an activity the student has completed it, but the plugin counts it as incomplete. An "all activities" restriction then never unlocks, and the "(x of y so far)" progress text undercounts. This contradicts the plugin's own documented rule that activities completed in any way (complete, passed or failed) count.

Risk Assessment

Low risk. This is a functional defect with no security impact. It only shows up in courses that require a grade for completion and use hidden grade items with a pass mark. Affected students stay locked out of content they should have unlocked until a teacher intervenes.

Context

In core completion_info::get_core_completion_state(), completiongrade is set to the result of get_grade_completion(), which returns COMPLETION_COMPLETE_FAIL_HIDDEN for a failing grade on a hidden item. When completionpassgrade is off, internal_get_state() passes that value through its array_reduce() unchanged, and update_state() stores it. Both the current-user path (get_data()) and the other-user path (direct query) in get_states() therefore return 4 for such activities.

Identified Code
default:
    return in_array($completionstate, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL], true);
Suggested Fix
default:
    return in_array($completionstate, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS,
        COMPLETION_COMPLETE_FAIL, COMPLETION_COMPLETE_FAIL_HIDDEN], true);

Leave the fail mode unchanged so that the result of a hidden grade item is not revealed through the restriction.

Additional AI Notes

Input validation is strict. condition::__construct() rejects any non-integer id, any e outside the three whitelisted states, and any m outside 0..1000. Values the YUI form later uses in CSS selectors or option markup (json.id, json.e, json.m) can therefore only come from validated, saved conditions.

Escaping in the YUI form relies on the server. getNode() concatenates section.name into <option> markup without escaping it client-side. This is safe because get_section_title() passes custom names through format_string() and otherwise falls back to language strings and section numbers, but the JavaScript depends on that guarantee. Keep it in mind if section names are ever supplied from another source.

Hidden activities count towards "all activities". As the README explains, a hidden completion-tracked activity can keep an "all activities" restriction from ever being met. This is a documented design choice rather than a defect, but teachers may want to be reminded of it in the UI as well.

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