Skip to content
hn-88 edited this page Sep 9, 2026 · 6 revisions

Adding some documentation generated by Claude during its review of the code.

Installation and upgrading

After placing the files in Moodle's local directory, visit the main administration page and go to Site administration > Notifications to complete the installation process. If there are some changes made, new DB tables, new task registrations, etc, we need to increase the version number in the version.php file of the plugin. If there are minimal changes, we only need to purge the cache -

Site administration → Development → Purge caches, or

php admin/cli/purge_caches.php from the Moodle root.

An OAuth 2 service must exist in Site administration → Server → OAuth 2 services, and its ID must match whatever you set in your plugin's oauth_issuer_id setting (default is '1', which is unlikely to be correct.)

Invalid request error

The most probable cause for such an error would be in launch.php, where the check

if ($iss !== 'https://swayamplus.education.gov.in/oidc') {
    throw new moodle_exception('invalidrequest', 'error');
}

has to be implemented for the correct issuer URL - which is currently hard-coded for testing.

Discovery does not populate the endpoints automatically

If Discovery does not populate the endpoints automatically when the OAuth2 service is added, as was happening with the test python mock server, we need to manually configure the endpoints, following https://docs.moodle.org/405/en/OAuth_2_services , as:

authorization_endpoint = https://our-ngrok-url.ngrok-free.dev/oidc/auth
token_endpoint = https://our-ngrok-url.ngrok-free.dev/oidc/token
userinfo_endpoint = https://our-ngrok-url.ngrok-free.dev/oidc/me
jwks_uri = https://our-ngrok-url.ngrok-free.dev/oidc/jwks

and Base URL seems to be https://our-ngrok-url.ngrok-free.dev/oidc for our test code, but should be replaced with the actual URL of https://swayamplus.api-server.com for the actual Swayam Plus API server.

Various features and nuances of the codebase

The use of get_string

The use of get_string - pulling labels from the lang file instead of hardcoding them, is needed to pass Moodle's official coding-style checks if you ever submit this to the Moodle's plugins directory.

Moodle's XMLDB format

db/install.xml needs to be in Moodle's XMLDB format, which requires uppercase tag and attribute names (

, , , etc. — table/column values stay lowercase, but the XML structure itself is uppercase), and the whole thing needs to be wrapped in an root with a PATH and VERSION attribute, matching the schema at lib/xmldb/xmldb.xsd.

sync_roster.php — three runtime risks

  1. No pagination ceiling. The while ($has_more) loop only stops when a page returns fewer than $limit rows. If the Swayam API ever has a bug (or an off-by-one) that keeps returning exactly 100 rows forever, this becomes an infinite loop that will eventually hit Moodle's cron task timeout and get killed mid-sync — better to fail fast with a sane cap, e.g. if ($page > 500) { mtrace("Safety limit reached, aborting."); break; }.
  2. Unchecked null on API failure. json_decode($response) returns null if the response isn't valid JSON (e.g. the API returns a 500 with an HTML error page, or the bearer token expired and it's a 401 with a plain-text body). The very next line, empty($data->enrollments), will throw a PHP warning on null->enrollments in PHP 8. Worth an explicit if ($data === null) { mtrace("Invalid JSON response, aborting page {$page}."); break; } before that check.
  3. Suspend logic assumes the manual enrol plugin owns the enrolment. In the TERMINATED branch, it calls $enrol_plugin->update_user_enrol($manual_instance, $userid, ENROL_USER_SUSPENDED) as soon as is_enrolled() is true — but is_enrolled() returns true regardless of which enrol method granted access (manual, self, cohort sync, etc.). If this user actually got into the course some other way, there's no user_enrolments row for the manual instance, and update_user_enrol() will either silently no-op or throw, depending on Moodle version — worth checking a specific user_enrolments row exists for $manual_instance->id before calling it here, the same way the ACCESS_PROVISIONED branch already does.

Retry logic

  • report_progress()/report_completion() still attempt the push immediately (so the common case — API is up — stays fast with no added latency).
  • On failure, the attempt is recorded in a new local_swayamplus_pushqueue table instead of being dropped.
  • A new lightweight task, running every 5 minutes, pulls due rows and retries them, doubling the wait each time it fails again — capped at 6 hours so a prolonged outage doesn't stretch retries out indefinitely, while still eventually recovering once SWAYAM+ comes back.
  • A 401 on any attempt clears the cached bearer token, so the next retry doesn't keep hammering the API with a token that's actually the problem.
  • One thing worth deciding, not implemented here: right now a row retries forever (capped at a 6-hour interval) with no ceiling. That's probably correct behaviour — eventual consistency once the API recovers — but if you want visibility into a queue that's been stuck for, say, a week, you may want an admin-facing report page or an alert (e.g. a check in sync_roster's nightly run that mtraces a warning if any queue row's attempts exceeds some threshold). Not urgent, but worth having on your list before this runs unattended for months.

URL generation

A URL generation tool is at Site administration → Local plugins → Swayam Plus URL builder.

Enter a course ID, and it immediately gives you the percent-encoded target_link_uri to hand to the SWAYAM+ team.

If you also fill in a sample enrollment ID (and optionally override the issuer for testing against your ngrok mock), it additionally generates the complete, ready-to-paste test launch URL, saving you from reconstructing that long string by hand every time you test a different course.

The generated URL would be of the form

https://ourdomain.org/local/swayamplus/launch.php?iss=https%3A%2F%2Four-ngrok-url.ngrok-free.dev%2Foidc&swayamEnrollmentId=mock-enrollment-999&target_link_uri=https%3A%2F%2Fourdomain.org%2Fcourse%2Fview.php%3Fid%3D165

for a ngrok test, for our course_id=165 and swayamEnrollmentId=mock-enrollment-999.

New users are dropped in Profile page

This is a long-standing Moodle core limitation, documented in Moodle's own tracker (MDL-61591) — when a new account's profile isn't fully set up on first login, after the user saves the mandatory-fields form, Moodle redirects them to their own profile page instead of honoring $SESSION->wantsurl. This isn't something your plugin caused, and it isn't fixable by changing anything in launch.php or observer.php - enrolment already succeeded correctly before the browser lands anywhere, so functionally nothing is broken, it's purely a navigation/UX gap on first-time signups only. Returning learners who've already completed their profile won't hit this at all.

Workaround:

  1. Go to any page where you can add a block sitewide — the Front Page is the standard choice. Turn editing on.
  2. Add a block → HTML. (This is called a Text Block in Moodle 5.x)
  3. Give it a title (e.g. "Continue learning") and content — something like a large, bold link:
   <p style="text-align:center;">
     <a href="https://devel2.srisathyasaividyavahini.org/my/courses.php"
        style="font-size:1.4em; font-weight:bold; display:inline-block; padding:10px 20px;">
       → Go to My Courses
     </a>
   </p>
  1. On that block's own settings (gear icon → Configure), under "Where this block appears" → Page contexts, set it to "Display throughout the entire site." Save.
  2. Now navigate to any user's profile page — your own is fine. The block should appear there too (since you just made it sitewide). Open that same block's settings again, and this time change "Display on page types" to "Only user profile pages." Save.