feat: moodle ws provisioning + real fixture
This commit is contained in:
File diff suppressed because one or more lines are too long
+141
@@ -0,0 +1,141 @@
|
||||
# ops/README.md — Moodle web-service provisioning
|
||||
|
||||
## provision-ws.php
|
||||
|
||||
Idempotent script that bootstraps the read-only WS integration on the live Moodle.
|
||||
|
||||
**What it does:**
|
||||
1. Enables Moodle web services + REST protocol + built-in mobile service
|
||||
(`enablewebservices`, `enablemobilewebservice`, `webserviceprotocols`).
|
||||
2. Creates the service user `wsreader` (WS Reader, wsreader@saillant.cc)
|
||||
if it does not already exist.
|
||||
3. Enrols `wsreader` as a student in every real course (id > 1) so that
|
||||
`core_course_get_contents` returns content for each course.
|
||||
|
||||
Script is safe to re-run: user creation is skipped if the username exists;
|
||||
enrolment is idempotent via the manual enrolment plugin.
|
||||
|
||||
**How to run (from Tower, where Moodle containers live):**
|
||||
|
||||
```bash
|
||||
WSPASS=$(grep WSREADER_PASSWORD ~/formations-app.env | cut -d= -f2-)
|
||||
docker cp ops/provision-ws.php moodle:/tmp/provision-ws.php
|
||||
docker exec -e WSREADER_PASSWORD="$WSPASS" moodle php /tmp/provision-ws.php
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
user created: <id> (or "user exists: <id>" on re-run)
|
||||
enrolled: freertos
|
||||
enrolled: kicad-makers
|
||||
enrolled: esp32-ia
|
||||
enrolled: docker-selfhosting
|
||||
enrolled: iot-az
|
||||
enrolled: llm-locaux
|
||||
OK
|
||||
```
|
||||
|
||||
## Env file — Tower: `/home/clems/formations-app.env`
|
||||
|
||||
```
|
||||
WSREADER_PASSWORD=<generated on first run>
|
||||
MOODLE_WS_TOKEN=<issued by Moodle mobile service>
|
||||
```
|
||||
|
||||
**Never commit this file.** It lives on Tower only.
|
||||
|
||||
## Token renewal
|
||||
|
||||
The mobile-service token does not expire by default in Moodle 4.5.
|
||||
To revoke and renew:
|
||||
|
||||
```bash
|
||||
# Revoke current token via Moodle admin UI:
|
||||
# Site admin → Server → Web services → Manage tokens → delete wsreader token
|
||||
# Then re-issue:
|
||||
WSPASS=$(grep WSREADER_PASSWORD ~/formations-app.env | cut -d= -f2-)
|
||||
TOKEN=$(curl -s "https://moodle.saillant.cc/login/token.php" \
|
||||
--data-urlencode "username=wsreader" \
|
||||
--data-urlencode "password=$WSPASS" \
|
||||
--data-urlencode "service=moodle_mobile_app" \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
|
||||
sed -i "s/^MOODLE_WS_TOKEN=.*/MOODLE_WS_TOKEN=$TOKEN/" ~/formations-app.env
|
||||
echo "New token: $TOKEN"
|
||||
```
|
||||
|
||||
## Fixture provenance — fixtures/course-contents-3.json
|
||||
|
||||
Captured: 2026-06-10 from live Moodle (https://moodle.saillant.cc)
|
||||
Course: id=3 (`kicad-makers` — "KiCad pour Makers")
|
||||
Command:
|
||||
```bash
|
||||
TOKEN=$(grep MOODLE_WS_TOKEN ~/formations-app.env | cut -d= -f2-)
|
||||
curl -s "https://moodle.saillant.cc/webservice/rest/server.php" \
|
||||
--data-urlencode "wstoken=$TOKEN" \
|
||||
--data-urlencode "wsfunction=core_course_get_contents" \
|
||||
--data-urlencode "moodlewsrestformat=json" \
|
||||
--data-urlencode "courseid=3" > fixtures/course-contents-3.json
|
||||
```
|
||||
|
||||
**No tokens are embedded in the fixture.** `pluginfile.php` URLs in the
|
||||
fixture do NOT include the token — authentication is added at fetch-time
|
||||
by appending `?token=<TOKEN>` to the URL.
|
||||
|
||||
## Where chapter titles live (CRITICAL for Task 3 parser)
|
||||
|
||||
The fixture is a JSON array of **sections**. Each section has a `modules[]`
|
||||
array. Book modules have `modname == "book"`.
|
||||
|
||||
Each book module's `contents[]` array contains two entry types:
|
||||
|
||||
### 1. The `structure` entry (type=`"content"`, filename=`"structure"`, filepath=`"/"`)
|
||||
|
||||
`contents[0]` is always a special entry with:
|
||||
- `type: "content"`
|
||||
- `filename: "structure"`
|
||||
- `filepath: "/"`
|
||||
- `content`: a **JSON string** encoding an array of chapter objects:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Introduction",
|
||||
"href": "242/index.html",
|
||||
"level": 0,
|
||||
"hidden": "0",
|
||||
"subitems": [...]
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
Each object has `title` (human-readable chapter title), `href` (chapter
|
||||
id from the path, e.g. `242`), `level` (0=top-level, 1=subchapter),
|
||||
`hidden`, and `subitems` (nested subchapters with the same shape).
|
||||
|
||||
**This is the authoritative TOC with hierarchy** — use this to build
|
||||
the navigation tree.
|
||||
|
||||
### 2. Per-chapter `file` entries (type=`"file"`, filename=`"index.html"`)
|
||||
|
||||
Each subsequent entry has:
|
||||
- `type: "file"`
|
||||
- `filename: "index.html"`
|
||||
- `filepath: "/<chapterId>/"` — e.g. `"/242/"` (chapter id is the path segment)
|
||||
- `content`: the **plain-text chapter title** (e.g. `"Introduction"`)
|
||||
- `fileurl`: pluginfile URL to fetch the chapter HTML, e.g.:
|
||||
`https://moodle.saillant.cc/webservice/pluginfile.php/69/mod_book/chapter/242/index.html`
|
||||
Fetch with `?token=<TOKEN>` appended.
|
||||
|
||||
**Parser recipe for Task 3:**
|
||||
1. Filter `contents` where `type == "file"` and `filename == "index.html"`.
|
||||
2. Extract `chapterId` from `filepath` (strip slashes → integer).
|
||||
3. Use `content` as the chapter title.
|
||||
4. Use `fileurl + "?token=" + TOKEN` to fetch HTML.
|
||||
5. Optionally parse the `structure` entry for hierarchy (level/subitems).
|
||||
|
||||
Example (from module "Module 1 : Découverte de KiCad"):
|
||||
| filepath | content (title) | fileurl (fetch with ?token=…) |
|
||||
|----------|------------------------------------------|--------------------------------------------------------------|
|
||||
| /242/ | Introduction | …/pluginfile.php/69/mod_book/chapter/242/index.html |
|
||||
| /243/ | 1.1 Introduction et installation | …/pluginfile.php/69/mod_book/chapter/243/index.html |
|
||||
| /244/ | Pourquoi KiCad plutôt qu'Eagle, … | …/pluginfile.php/69/mod_book/chapter/244/index.html |
|
||||
| /254/ | Récapitulatif | …/pluginfile.php/69/mod_book/chapter/254/index.html |
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
// Idempotent provisioning of a read-only web-service user for the app BFF.
|
||||
// Run: docker cp ops/provision-ws.php moodle:/tmp/ &&
|
||||
// docker exec -e WSREADER_PASSWORD=... moodle php /tmp/provision-ws.php
|
||||
define('CLI_SCRIPT', true);
|
||||
require('/var/www/html/config.php');
|
||||
require_once($CFG->dirroot.'/user/lib.php');
|
||||
require_once($CFG->dirroot.'/lib/enrollib.php');
|
||||
|
||||
// 1) Enable web services + REST + built-in mobile service
|
||||
// (mobile service already includes core_course_get_contents & file serving)
|
||||
set_config('enablewebservices', 1);
|
||||
set_config('enablemobilewebservice', 1);
|
||||
$protocols = (string)get_config('core', 'webserviceprotocols');
|
||||
if (strpos($protocols, 'rest') === false) {
|
||||
set_config('webserviceprotocols', trim($protocols === '' ? 'rest' : $protocols . ',rest', ','));
|
||||
}
|
||||
|
||||
// 2) Service user
|
||||
$username = 'wsreader';
|
||||
$password = getenv('WSREADER_PASSWORD');
|
||||
if (!$password) { fwrite(STDERR, "WSREADER_PASSWORD required\n"); exit(1); }
|
||||
$user = core_user::get_user_by_username($username);
|
||||
if (!$user) {
|
||||
$u = new stdClass();
|
||||
$u->username = $username;
|
||||
$u->password = $password;
|
||||
$u->firstname = 'WS';
|
||||
$u->lastname = 'Reader';
|
||||
$u->email = 'wsreader@saillant.cc';
|
||||
$u->confirmed = 1;
|
||||
$u->mnethostid = $CFG->mnet_localhost_id;
|
||||
$uid = user_create_user($u, true, false);
|
||||
$user = core_user::get_user($uid);
|
||||
echo "user created: {$user->id}\n";
|
||||
} else {
|
||||
echo "user exists: {$user->id}\n";
|
||||
}
|
||||
|
||||
// 3) Enrol as student in every real course (id > 1) so get_contents works
|
||||
$student = $DB->get_record('role', ['shortname' => 'student'], '*', MUST_EXIST);
|
||||
$manual = enrol_get_plugin('manual');
|
||||
foreach ($DB->get_records_select('course', 'id > 1') as $course) {
|
||||
$minstance = null;
|
||||
foreach (enrol_get_instances($course->id, false) as $i) {
|
||||
if ($i->enrol === 'manual') { $minstance = $i; break; }
|
||||
}
|
||||
if (!$minstance) {
|
||||
$id = $manual->add_instance($course);
|
||||
$minstance = $DB->get_record('enrol', ['id' => $id], '*', MUST_EXIST);
|
||||
}
|
||||
$manual->enrol_user($minstance, $user->id, $student->id);
|
||||
echo "enrolled: {$course->shortname}\n";
|
||||
}
|
||||
echo "OK\n";
|
||||
Reference in New Issue
Block a user