Files
formations-app/ops

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):

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:

# 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:

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:

    [
      {
        "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