Chapitre 9.4 — Créer sa propre fonction externe
Partie 9 : Web Services — Chapitre 4/5 Public : dev ayant fait la Partie 6 (plugins) et les chapitres 9.1–9.3. On expose
local_todolisten web service. Version de référence : Moodle 5.2 (\core_external\external_apiet les classesexternal_*du namespacecore_external).
⏱️ TL;DR
- Une external function est une classe dans
classes/external/étendant\core_external\external_api, avec trois méthodes statiques :execute_parameters()(déclare/valide les entrées),execute()(la logique),execute_returns()(déclare la sortie).- On la déclare dans
db/services.php(nom, classname, capabilities, type read/write,ajax => truepour l’usage interne).- Les types viennent de
core_external:external_function_parameters,external_single_structure,external_multiple_structure,external_value(PARAM_*).execute()refait la sécurité :validate_parameters(), résolution +validate_context(),require_capability()— jamais de confiance aveugle aux entrées.- Sortie sûre :
external_format_string/external_format_textpour tout texte renvoyé (échappement/filtres côté serveur, ch. 7.2/7.4).- Bump
version.phpaprès tout changement dedb/services.php. Tester avec l’API tester.- La même fonction sert l’API externe (token) et le JS interne (
core/ajax, siajax => true).
🎯 Objectifs
- Écrire une external function complète (les trois méthodes).
- La déclarer dans
db/services.phpavec ses capabilities. - Valider les entrées et sécuriser l’exécution (contexte, capabilities).
- Formater la sortie de façon sûre et typée.
- La rendre appelable en REST et par
core/ajax.
1. Le plan : exposer local_todolist
On reprend local_todolist (ch. 6.3 : une todo-list par utilisateur) et on expose trois opérations en web service :
local_todolist_get_tasks— lister ses tâches (lecture).local_todolist_add_task— ajouter une tâche (écriture).local_todolist_toggle_task— basculer l’état d’une tâche (écriture).
Arborescence ajoutée au plugin :
public/local/todolist/
├── db/
│ └── services.php ← déclare les fonctions (+ un service)
└── classes/
└── external/
├── get_tasks.php
├── add_task.php
└── toggle_task.php💡 Pour un dev React : chaque external function ≈ un route handler typé avec un schéma d’entrée (
execute_parameters, votre Zod), une logique (execute, votre handler), et un schéma de sortie (execute_returns, le type de réponse). La validation d’entrée/sortie est déclarative et appliquée par le framework — vous ne parsez pas à la main.
2. La classe : les trois méthodes
Exemple complet, local_todolist_get_tasks :
<?php
// public/local/todolist/classes/external/get_tasks.php
namespace local_todolist\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
defined('MOODLE_INTERNAL') || die();
class get_tasks extends external_api {
/**
* Déclare les PARAMÈTRES d'entrée (ici : aucun ; on lit SES tâches).
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
// ex. un filtre optionnel : 'onlyopen' => new external_value(PARAM_BOOL, '', VALUE_DEFAULT, false)
]);
}
/**
* LA LOGIQUE. S'exécute avec l'identité du token / de la session.
*
* @return array conforme à execute_returns()
*/
public static function execute(): array {
global $USER;
// 1. Valider les paramètres (aucun ici, mais la forme est obligatoire).
self::validate_parameters(self::execute_parameters(), []);
// 2. Contexte + sécurité. On lit ses propres tâches → contexte système.
$context = \context_system::instance();
self::validate_context($context); // OBLIGATOIRE : valide le contexte pour le WS
require_capability('local/todolist:manageentries', $context);
// 3. Logique métier (via le manager du ch. 6.3).
$tasks = \local_todolist\manager::get_user_tasks($USER->id);
// 4. Formatage SÛR de la sortie.
$result = [];
foreach ($tasks as $task) {
$result[] = [
'id' => (int) $task->id,
// external_format_string : échappe/filtre le texte renvoyé.
'name' => \core_external\util::format_string($task->name, $context),
'completed' => (bool) $task->completed,
];
}
return $result;
}
/**
* Déclare la STRUCTURE de retour (typée, validée par le framework).
*/
public static function execute_returns(): external_multiple_structure {
return new external_multiple_structure(
new external_single_structure([
'id' => new external_value(PARAM_INT, 'Task id'),
'name' => new external_value(PARAM_TEXT, 'Task name'),
'completed' => new external_value(PARAM_BOOL, 'Completed?'),
])
);
}
}Les trois méthodes, leur rôle :
execute_parameters(): le schéma d’entrée. Retourne unexternal_function_parametersdécrivant chaque paramètre (typePARAM_*, obligatoire/optionnel/défaut). Le framework valide et nettoie les entrées avantexecute().execute(): la logique. Reçoit les paramètres (via ses arguments), refait la sécurité, appelle le métier, retourne un tableau conforme àexecute_returns().execute_returns(): le schéma de sortie. Le framework valide ce queexecute()renvoie contre ce schéma (une clé manquante ou d’un mauvais type lève une erreur) et le sérialise.
⚠️ Piège n°1 : les trois méthodes sont statiques et leurs noms sont imposés (
execute,execute_parameters,execute_returns). Le framework les appelle par convention. Une faute de nom = fonction non fonctionnelle.
3. Une fonction avec paramètres et écriture
local_todolist_add_task — prend un name, crée la tâche, renvoie la tâche créée :
<?php
// public/local/todolist/classes/external/add_task.php
namespace local_todolist\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
defined('MOODLE_INTERNAL') || die();
class add_task extends external_api {
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'name' => new external_value(PARAM_TEXT, 'The task text', VALUE_REQUIRED),
]);
}
/**
* @param string $name
* @return array
*/
public static function execute(string $name): array {
global $USER;
// 1. Validation typée des paramètres.
['name' => $name] = self::validate_parameters(
self::execute_parameters(),
['name' => $name]
);
// 2. Sécurité.
$context = \context_system::instance();
self::validate_context($context);
require_capability('local/todolist:manageentries', $context);
// 3. Métier (le manager applique aussi la limite maxtasks, ch. 6.3).
$id = \local_todolist\manager::create($USER->id, $name);
$task = \local_todolist\manager::get_own_task($id, $USER->id);
// 4. Sortie sûre.
return [
'id' => (int) $task->id,
'name' => \core_external\util::format_string($task->name, $context),
'completed' => (bool) $task->completed,
];
}
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'id' => new external_value(PARAM_INT, 'Task id'),
'name' => new external_value(PARAM_TEXT, 'Task name'),
'completed' => new external_value(PARAM_BOOL, 'Completed?'),
]);
}
}Points clés du flux d’écriture :
validate_parameters()retourne les valeurs nettoyées et typées — utilisez ces valeurs, pas les arguments bruts (défense en profondeur).- La capability d’écriture (
local/todolist:manageentries) est revérifiée : le service ne remplace jamais le contrôle métier (ch. 9.2 §3). - Le manager (ch. 6.3) porte la logique et ses garde-fous (appartenance via
userid, limitemaxtasks). On ne réécrit pas la logique dans l’external function : on la réutilise. C’est le bénéfice « une logique, deux consommateurs » (ch. 9.1).
⚠️ Piège de sécurité (rappel IDOR, ch. 6.3) :
toggle_task(id)doit charger la tâche viaget_own_task($id, $USER->id)— filtrer paruserid— sinon un token pourrait basculer la tâche d’autrui en passant unidarbitraire. La capability autorise « gérer ses tâches » ; l’appartenance se vérifie dans la requête. Autorisation ≠ appartenance, aussi en web service.
4. Déclarer les fonctions : db/services.php
Rien n’est exposé tant que ce n’est pas déclaré :
<?php
// public/local/todolist/db/services.php
defined('MOODLE_INTERNAL') || die();
// 1. Les fonctions exposées.
$functions = [
'local_todolist_get_tasks' => [
'classname' => 'local_todolist\external\get_tasks',
'description' => 'Get the current user tasks',
'type' => 'read',
'ajax' => true, // appelable aussi par core/ajax (JS interne, ch. 7.3)
'capabilities' => 'local/todolist:manageentries',
],
'local_todolist_add_task' => [
'classname' => 'local_todolist\external\add_task',
'description' => 'Add a task for the current user',
'type' => 'write',
'ajax' => true,
'capabilities' => 'local/todolist:manageentries',
],
'local_todolist_toggle_task' => [
'classname' => 'local_todolist\external\toggle_task',
'description' => 'Toggle a task completion state',
'type' => 'write',
'ajax' => true,
'capabilities' => 'local/todolist:manageentries',
],
];
// 2. (Optionnel) un service pré-défini regroupant ces fonctions.
$services = [
'To-do list API' => [
'functions' => [
'local_todolist_get_tasks',
'local_todolist_add_task',
'local_todolist_toggle_task',
],
'restrictedusers' => 1, // réservé aux utilisateurs autorisés
'enabled' => 0, // désactivé par défaut : l'admin l'active sciemment
'shortname' => 'local_todolist',
],
];Chaque entrée de $functions :
classname: la classe FQCN (namespacelocal_todolist\external\...).type:readouwrite(documentaire + certains contrôles).ajax => true: autorise l’appel viacore/ajax(le JS interne) — indispensable si votre frontend Moodle l’utilise (ch. 7.3). Sansajax => true, la fonction n’est appelable que par token externe.capabilities: les capabilities requises (documentaire ; la vraie vérification est votrerequire_capabilitydansexecute()).
Le bloc $services pré-déclare un service (l’admin peut aussi en créer un à la main, ch. 9.1). On le laisse désactivé par défaut (enabled => 0) : l’activation est une décision d’admin.
⚠️ Piège :
db/services.phpn’est lu qu’à l’installation/upgrade. Après l’avoir créé/modifié, bumpezversion.phpet rejouez l’upgrade — sinon vos fonctions n’existent pas pour Moodle. C’est l’oubli n°1 (« ma fonction n’apparaît pas dans le service »).
5. Sortie sûre : format_string / format_text
Comme pour l’affichage (ch. 7.2), tout texte renvoyé par une external function doit être formaté côté serveur :
\core_external\util::format_string($text, $context)pour un texte simple (nom, titre) — échappe et applique les filtres.\core_external\util::format_text($text, $format, $context)pour du contenu riche (HTML, description) — applique filtres + nettoyage, et renvoie le format.
Pourquoi c’est critique ici : le consommateur (Next.js, ch. 9.5) peut réafficher ce texte dans un navigateur. mustache.js / React échappent le texte, mais les filtres Moodle (liens, médias, nettoyage HTML) ne s’appliquent pas côté client. Le formatage doit donc se faire dans l’external function. C’est la règle « nettoyage en sortie côté serveur » (ch. 7.2/7.4), appliquée aux WS.
⚠️ Piège : renvoyer
$task->namebrut (sansformat_string) expose le consommateur à du contenu non filtré et casse la cohérence des filtres. Toujours passer les textes parformat_string/format_textavec le contexte approprié.
6. Tester
- Bump + upgrade :
version.phpincrémenté,php admin/cli/upgrade.php, purge des caches. - Activer le service (ou créer un service custom) et ajouter les fonctions (ch. 9.1), générer un token (ch. 9.2).
- API tester (Web services → API tester) : sélectionner
local_todolist_add_task, saisirname, exécuter avec le token → voir la réponse réelle. - REST direct :
curl 'https://moodle.example.com/webservice/rest/server.php' \
--data-urlencode 'wstoken=VOTRE_TOKEN' \
--data-urlencode 'wsfunction=local_todolist_add_task' \
--data-urlencode 'moodlewsrestformat=json' \
--data-urlencode 'name=Lire le chapitre 9'
# → {"id":12,"name":"Lire le chapitre 9","completed":false}- Depuis le JS interne (grâce à
ajax => true) :Ajax.call([{methodname:'local_todolist_add_task', args:{name:'…'}}])(ch. 7.3) — la même fonction.
7. Synthèse
Créer une external function = une classe (trois méthodes statiques : schéma d’entrée, logique, schéma de sortie) + une déclaration (db/services.php) + un bump de version. La logique réutilise votre code métier (le manager) et refait la sécurité (validate_context + require_capability + filtre d’appartenance). Les textes sortent formatés (format_string/format_text). Résultat : une fonction, sécurisée et typée, appelable en REST (token) et par core/ajax (JS interne). Reste à la consommer depuis Next.js — le chapitre final.
✏️ Exercices
Exercice 1 — Le rôle des trois méthodes.
Associez chaque méthode à son rôle : execute_parameters, execute, execute_returns. Que fait le framework avec chacune ?
✅ Solution
execute_parameters() = schéma d’entrée : le framework valide et nettoie les paramètres reçus avant d’appeler execute(). execute() = la logique : refait la sécurité, appelle le métier, renvoie un tableau. execute_returns() = schéma de sortie : le framework valide ce que execute() renvoie contre ce schéma (type/clés) et le sérialise en JSON. C’est de la validation déclarative appliquée par le framework (entrée et sortie).
Exercice 2 — Fonction invisible.
Vous avez écrit les classes et db/services.php, mais la fonction n’apparaît nulle part et l’appel renvoie « fonction inconnue ». Cause ?
✅ Solution
db/services.php n’est lu qu’à l’installation/upgrade : sans bump de version.php suivi de php admin/cli/upgrade.php (+ purge des caches), Moodle ne connaît pas vos fonctions. Correctif : incrémenter $plugin->version, rejouer l’upgrade, purger. Vérifier ensuite que la fonction est ajoutée à un service activé et que le token y a accès (via get_site_info.functions, ch. 9.3).
Exercice 3 — IDOR en web service.
Écrivez le squelette sécurisé de toggle_task(id) : quels contrôles empêchent un token de basculer la tâche d’un autre utilisateur ?
✅ Solution
public static function execute(int $id): array {
global $USER;
['id' => $id] = self::validate_parameters(self::execute_parameters(), ['id' => $id]);
$context = \context_system::instance();
self::validate_context($context);
require_capability('local/todolist:manageentries', $context);
// Filtre d'APPARTENANCE : get_own_task($id, $USER->id) lève une exception si pas à nous.
\local_todolist\manager::toggle($id, $USER->id);
$task = \local_todolist\manager::get_own_task($id, $USER->id);
return ['id' => (int)$task->id, 'name' => \core_external\util::format_string($task->name, $context), 'completed' => (bool)$task->completed];
}La capability autorise « gérer ses tâches » ; l’appartenance est garantie par le filtre userid dans toggle/get_own_task (ch. 6.3). Sans ce filtre, un id arbitraire basculerait la tâche d’autrui (IDOR). Autorisation ≠ appartenance, aussi en WS.
Exercice 4 — Sortie brute.
Un dev renvoie 'name' => $task->name sans format_string. Quel est le risque et le correctif ?
✅ Solution
Risque : le texte renvoyé n’est ni échappé ni filtré côté serveur ; le consommateur (Next.js/React) échappe le texte à l’affichage mais n’applique pas les filtres Moodle, et si le nom contient du HTML/contenu spécial, la cohérence (et parfois la sécurité) est compromise. Correctif : 'name' => \core_external\util::format_string($task->name, $context) (ou format_text pour du contenu riche), avec le contexte approprié. Règle : tout texte renvoyé passe par format_string/format_text.
Exercice 5 — ajax => true.
À quoi sert 'ajax' => true dans db/services.php, et que se passe-t-il sans ?
✅ Solution
'ajax' => true autorise l’appel de la fonction via core/ajax (le JavaScript interne de Moodle, avec la session, ch. 7.3). Sans lui, la fonction n’est appelable que par un token externe (REST), et un Ajax.call interne serait refusé. Comme la même fonction sert souvent l’UI interne et l’API externe (ch. 9.1), on met ajax => true dès que le frontend Moodle l’utilise.
🧠 Quiz de révision
Q1. Quelles sont les trois méthodes d’une external function et leur nom imposé ?
Réponse
execute_parameters() (schéma/validation d’entrée), execute() (logique), execute_returns() (schéma/validation de sortie). Statiques, noms imposés, dans une classe étendant \core_external\external_api.Q2. Où et comment déclare-t-on une external function ?
Réponse
db/services.php : entrée dans $functions avec classname, type (read/write), ajax, capabilities ; éventuellement un $services pré-défini. Il faut bumper version.php et rejouer l’upgrade pour que Moodle la prenne en compte.Q3. Quels contrôles de sécurité execute() doit-il refaire ?
Réponse
self::validate_parameters(...) (utiliser les valeurs nettoyées), résoudre + self::validate_context($context), require_capability(...), et le filtre d’appartenance (userid) dans les requêtes. Le service ne remplace jamais ces contrôles.Q4. Pourquoi et comment formate-t-on la sortie ?
Réponse
\core_external\util::format_string / format_text (avec le contexte) sur tout texte renvoyé. Sinon, contenu non filtré et risque de sécurité.Q5. Pourquoi dit-on « une fonction, deux consommateurs » ici ?
Réponse
ajax => true, la même external function est appelable par core/ajax (JS interne, session) et par REST (token externe). La logique et la sécurité sont écrites une fois, dans execute(), et servent les deux — pas de duplication (ch. 9.1).Chapitre suivant : 05 — Consommer l’API depuis Next.js — le client TypeScript typé, le token côté serveur uniquement (route handlers / server actions), la gestion des erreurs Moodle (HTTP 200 trompeur), le typage des réponses, et une mini-app Next.js qui lit et écrit dans Moodle.