Skip to Content

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_todolist en web service. Version de référence : Moodle 5.2 (\core_external\external_api et les classes external_* du namespace core_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 => true pour 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_text pour tout texte renvoyé (échappement/filtres côté serveur, ch. 7.2/7.4).
  • Bump version.php après tout changement de db/services.php. Tester avec l’API tester.
  • La même fonction sert l’API externe (token) et le JS interne (core/ajax, si ajax => true).

🎯 Objectifs

  • Écrire une external function complète (les trois méthodes).
  • La déclarer dans db/services.php avec 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 un external_function_parameters décrivant chaque paramètre (type PARAM_*, obligatoire/optionnel/défaut). Le framework valide et nettoie les entrées avant execute().
  • 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 que execute() 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, limite maxtasks). 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 via get_own_task($id, $USER->id)filtrer par userid — sinon un token pourrait basculer la tâche d’autrui en passant un id arbitraire. 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 (namespace local_todolist\external\...).
  • type : read ou write (documentaire + certains contrôles).
  • ajax => true : autorise l’appel via core/ajax (le JS interne) — indispensable si votre frontend Moodle l’utilise (ch. 7.3). Sans ajax => true, la fonction n’est appelable que par token externe.
  • capabilities : les capabilities requises (documentaire ; la vraie vérification est votre require_capability dans execute()).

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.php n’est lu qu’à l’installation/upgrade. Après l’avoir créé/modifié, bumpez version.php et 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->name brut (sans format_string) expose le consommateur à du contenu non filtré et casse la cohérence des filtres. Toujours passer les textes par format_string/format_text avec le contexte approprié.


6. Tester

  1. Bump + upgrade : version.php incrémenté, php admin/cli/upgrade.php, purge des caches.
  2. Activer le service (ou créer un service custom) et ajouter les fonctions (ch. 9.1), générer un token (ch. 9.2).
  3. API tester (Web services → API tester) : sélectionner local_todolist_add_task, saisir name, exécuter avec le token → voir la réponse réelle.
  4. 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}
  1. 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

Dans 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

Parce que le consommateur (JS/React/Next.js) n’applique pas les filtres Moodle : le formatage/nettoyage doit se faire côté serveur via \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

Avec 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.