paheko-fork/doc/admin/modules.md

338 lines
14 KiB
Markdown
Raw Permalink Normal View History

2024-01-19 15:39:49 +00:00
Title: Développer des modules pour Paheko
{{{.nav
* **[Modules](modules.html)**
* [Documentation Brindille](brindille.html)
* [Fonctions](brindille_functions.html)
* [Sections](brindille_sections.html)
* [Filtres](brindille_modifiers.html)
}}}
<<toc aside>>
# Introduction
Depuis la version 1.3, Paheko dispose d'extensions modifiables, nommées **Modules**.
Les modules permettent de créer et modifier des formulaires, des modèles de documents simples, à imprimer, mais aussi de créer des "mini-applications" directement dans l'administration de l'association, avec le minimum de code, sans avoir à apprendre à programmer PHP.
Les modules utilisent le langage [Brindille](brindille.html), aussi utilisé pour le site web (qui est lui-même un module). Avec Brindille on parle d'un **squelette** pour un fichier texte contenant du code Brindille.
Les modules ne permettent pas d'exécuter du code PHP, ni de modifier la base de données en dehors des données du module, contrairement aux [plugins](https://fossil.kd2.org/paheko/wiki?name=Documentation/Plugin&p). Grâce à Brindille, les administrateurs de l'association peuvent modifier ou créer de nouveaux modules sans risques pour le serveur, car le code Brindille ne permet pas d'exécuter de fonctions dangereuses. Les **plugins** eux sont écrits en PHP et ne peuvent pas être modifiés par une association. Du fait des risques de sécurité, seuls les plugins officiels sont proposés sur Paheko.cloud.
# Exemples
Paheko fournit quelques modules par défaut, qui peuvent être modifiés ou servir d'inspiration pour de nouveaux modules :
* Reçu de don simple
* Reçu de paiement simple
* Reçu fiscal
* Cartes de membres
* Heures d'ouverture
* Modèles d'écritures comptables
Ces exemples sont développés directement avec Brindille et peuvent être modifiés ou lus depuis le menu **Configuration**, onglet **Extensions**.
Un module fourni dans Paheko peut être modifié, et en cas de problème il peut être remis à son état d'origine.
D'autres exemples d'utilisation sont imaginables :
* Auto-remplissage de la déclaration de la liste des dirigeants à la préfecture
* Compte de résultat et bilan conforme au modèle du plan comptable
* Formulaires partagés entre la partie privée, et le site web (voir par exemple le module "heures d'ouverture")
* Gestion de matériel prêté par l'association
# Pré-requis
Une connaissance de la programmation informatique est souhaitable pour commencer à modifier ou créer des modules, mais cela n'est pas requis, il est possible d'apprendre progressivement.
# Résumé technique
* Utilisation de la syntaxe Brindille
* Les modules peuvent utiliser toutes les fonctions et boucles de Brindille
* Les modules peuvent stocker et récupérer des données dans la base SQLite dans une table clé-valeur spécifique à chaque module
* Les données du module sont stockées en JSON, on peut faire des requêtes complètes avec l'extension [JSON de SQLite](https://www.sqlite.org/json1.html)
* Les données peuvent être validées avant enregistrement en utilisant [JSON Schema](https://json-schema.org/understanding-json-schema/)
* Un module peut également accéder aux données des autres modules
* Un module peut aussi accéder à toutes les données de la base de données, sauf certaines données à risque (voir plus bas)
* Un module ne peut pas modifier les données de la base de données
* Paheko crée automatiquement des index sur les requêtes SQL des modules, permettant de rendre les requêtes rapides
# Structure des répertoires
Chaque module a un nom unique (composé uniquement de lettres minuscules, de tirets bas et de chiffres) et dispose d'un sous-répertoire dans le dossier `modules`. Ainsi le module `recu_don` serait dans le répertoire `modules/recu_don`.
Dans ce répertoire le module peut avoir autant de fichiers qu'il veut, mais certains fichiers ont une fonction spéciale :
* `module.ini` : contient les informations sur le module, voir ci-dessous pour les détails
* `config.html` : si ce squelette existe, un bouton "Configurer" apparaîtra dans la liste des modules (Configuration -> Modules) et affichera ce squelette dans un dialogue
* `icon.svg` : icône du module, qui sera utilisée sur la page d'accueil, si le bouton est activé, et dans la liste des modules. Attention l'élément racine du fichier doit porter l'id `img` pour que l'icône fonctionne (`<svg id="img"...>`), notamment pour que les couleurs du thème s'appliquent à l'icône.
* `README.md` : si ce fichier existe, son contenu sera affiché dans les détails du module
## Snippets
Les modules peuvent également avoir des `snippets`, ce sont des squelettes qui seront inclus à des endroits précis de l'interface, permettant de rajouter des fonctionnalités, ils sont situés dans le sous-répertoire `snippets` du module :
* `snippets/transaction_details.html` : sera inclus en dessous de la fiche d'une écriture comptable
* `snippets/transaction_new.html` : sera inclus au début du formulaire de saisie d'écriture
* `snippets/user_details.html` : sera inclus en dessous de la fiche d'un membre
* `snippets/my_details.html` : sera inclus en dessous de la page "Mes informations personnelles"
* `snippets/my_services.html` : sera inclus en dessous de la page "Mes inscriptions et cotisations"
* `snippets/home_button.html` : sera inclus dans la liste des boutons de la page d'accueil (ce fichier ne sera pas appelé si `home_button` est à `true` dans `module.ini`, il le remplace)
### Snippets MarkDown
Il est également possible, depuis Paheko 1.3.2, d'étendre les fonctionnalités Markdown du site web en créant un snippet dans le répertoire `snippets/markdown/`, par exemple `snippets/markdown/map.html`.
Le snippet sera appelé quand on utilise le tag du même nom dans le contenu du site web. Ici par exemple ça serait `<<map>>`.
Le nom du snippet doit commencer par une lettre minuscule et peut être suivi de lettres minuscules, de chiffres, ou de tirets bas. Exemples : `map2024` `map_openstreetmap`, etc.
Le snippet reçoit ces variables :
* `$params` : les paramètres du tag
* `$block` : booléen, `TRUE` si le tag est seul sur une ligne, ou `FALSE` s'il se situe à l'intérieur d'un texte
* `$content` : le contenu du bloc, si celui-ci est sur plusieurs lignes
Exemple :
```
<<map center="Auckland, New Zealand"
Ceci est la capitale de Nouvelle-Zélande !
>>
Voici un marqueur : <<map marker>>
```
Dans le premier appel, `map.html` recevra ces variables :
```
$params = ['center' => 'Auckland, New Zealand']
$content = "Ceci est la capitale de Nouvelle-Zélande !"
$block = TRUE
```
Dans le second appel, le snippet recevra celles-ci :
```
$params = [0 => 'marker']
$content = NULL
$block = FALSE
```
## Fichier module.ini
Ce fichier décrit le module, au format INI (`clé=valeur`), en utilisant les clés suivantes :
* `name` (obligatoire) : nom du module
* `description` : courte description de la fonctionnalité apportée par le module
* `author` : nom de l'auteur
* `author_url` : adresse web HTTP menant au site de l'auteur
* `home_button` : indique si un bouton pour ce module doit être affiché sur la page d'accueil (`true` ou `false`)
* `menu` : indique si ce module doit être listé dans le menu de gauche (`true` ou `false`)
* `restrict_section` : indique la section auquel le membre doit avoir accès pour pouvoir voir le menu de ce module, parmi `web, documents, users, accounting, connect, config`
* `restrict_level` : indique le niveau d'accès que le membre doit avoir dans la section indiquée pour pouvoir voir le menu de ce module, parmi `read, write, admin`.
Attention : les directives `restrict_section` et `restrict_level` ne contrôlent *que* l'affichage du lien vers le module dans le menu et dans les boutons de la page d'accueil, mais pas l'accès aux pages du module.
# Variables spéciales
Toutes les pages d'un module disposent de la variable `$module` qui contient l'entité du module en cours :
* `$module.name` contient le nom unique (`recu_don` par exemple)
* `$module.label` le libellé du module
* `$module.description` la description
* `$module.config` la configuration du module
* `$module.url` l'adresse URL du module (`https://site-association.tld/m/recu_don/` par exemple)
# Stockage de données
Un module peut stocker des données de deux manières : dans sa configuration, ou dans son stockage de documents JSON.
## Configuration
La première manière est de stocker des informations dans la configuration du module. Pour cela on utilise la fonction `save` et la clé `config` :
```
{{:save key="config" accounts_list="512A,512B" check_boxes=true}}
```
On pourra retrouver ces valeurs dans la variable `$module.config` :
```
{{if $module.config.check_boxes}}
{{$module.config.accounts_list}}
{{/if}}
```
## Stockage de documents JSON
Chaque module peut stocker ses données dans une base de données clé-document qui stockera les données dans des documents au format JSON dans une table SQLite.
Grâce aux [fonctions JSON de SQLite](https://www.sqlite.org/json1.html) on pourra ensuite effectuer des recherches sur ces documents.
Pour enregistrer il suffit d'utiliser la fonction `save` :
```
{{:save key="facture001" type="facture" date="2022-01-01" label="Vente de petits pains au chocolat" total="42"}}
```
Si la clé indiquée (dans le paramètre `key`) n'existe pas, l'enregistrement sera créé, sinon il sera mis à jour avec les valeurs données.
### Validation
On peut utiliser un [schéma JSON](https://json-schema.org/understanding-json-schema/) pour valider que le document qu'on enregistre est valide :
```
{{:save validate_schema="./document.schema.json" type="facture" date="2022-01-01" label="Vente de petits pains au chocolat" total="42"}}
```
Le fichier `document.schema.json` devra être dans le même répertoire que le squelette et devra contenir un schéma valide. Voici un exemple :
```
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"date": {
"description": "Date d'émission",
"type": "string",
"format": "date"
},
"type": {
"description": "Type de document",
"type": "string",
"enum": ["devis", "facture"]
},
"total": {
"description": "Montant total",
"type": "integer",
"minimum": 0
},
"label": {
"description": "Libellé",
"type": "string"
},
"description": {
"description": "Description",
"type": ["string", "null"]
}
},
"required": [ "type", "date", "total", "label"]
}
```
Si le document fourni n'est pas conforme au schéma, il ne sera pas enregistré et une erreur sera affichée.
#### Propriété non requise
Si vous souhaitez utiliser dans votre document une propriété non requise, il ne faut pas la fournir en paramètre de la fonction `save`.
Si elle est fournie mais vide, il faut aussi autoriser le type `null` (en minuscules) au type de votre propriété.
Exemple :
[...]
"description": {
"description": "Description",
"type": ["string", "null"]
}
[...]
### Stockage JSON dans SQLite (pour information)
Explication du fonctionnement technique derrière la fonction `save`.
En pratique chaque enregistrement sera placé dans une table SQL dont le nom commence par `module_data_`. Ici la table sera donc nommée `module_data_factures` si le nom unique du module est `factures`.
Le schéma de cette table est le suivant :
```
CREATE TABLE module_data_factures (
id INTEGER PRIMARY KEY NOT NULL,
key TEXT NULL,
document TEXT NOT NULL
);
CREATE UNIQUE INDEX module_data_factures_key ON module_data_factures (key);
```
Comme on peut le voir, chaque ligne dans la table peut avoir une clé unique (`key`), et un ID ou juste un ID auto-incrémenté. La clé unique n'est pas obligatoire, mais peut être utile pour différencier certains documents.
Par exemple le code suivant :
```
{{:save key="facture_43" nom="Facture de courses"}}
```
Est l'équivalent de la requête SQL suivante :
```
INSERT OR REPLACE INTO module_data_factures (key, document) VALUES ('facture_43', '{"nom": "Facture de courses"}');
```
### Récupération et liste de documents
Il sera ensuite possible d'utiliser la boucle `load` pour récupérer les données :
```
{{#load id=42}}
Ce document est de type {{$type}} créé le {{$date}}.
<h2>{{$label}}</h2>
À payer : {{$total}} €
{{else}}
Le document numéro 42 n'a pas été trouvé.
{{/load}}
```
Cette boucle `load` permet aussi de faire des recherches sur les valeurs du document :
```
<ul>
{{#load where="$$.type = 'facture'" order="date DESC"}}
<li>{{$label}} ({{$total}} €)</li>
{{/load}}
</ul>
```
La syntaxe `$$.type` indique d'aller extraire la clé `type` du document JSON.
C'est un raccourci pour la syntaxe SQLite `json_extract(document, '$.type')`.
# Export et import de modules
Il est possible d'exporter un module modifié. Cela créera un fichier ZIP contenant à la fois le code modifié et le code non modifié.
De la même manière il est possible d'importer un module à partir d'un fichier ZIP d'export. Si vous créez votre fichier ZIP manuellement, attention à respecter le fait que le code du module doit se situer dans le répertoire `modules/nom_du_module` du fichier ZIP. Tout fichier ou répertoire situé en dehors de cette arborescence provoquera une erreur et l'impossibilité d'importer le module.
# Restrictions
* Il n'est pas possible de télécharger ou envoyer des données depuis un autre serveur
* Il n'est pas possible d'écrire un fichier local
## Envoi d'e-mail
Voir [la documentation de la fonction `{{:mail}}`](brindille_functions.html#mail)
## Tables et colonnes de la base de données
Pour des raisons de sécurité, les modules ne peuvent pas accéder à toutes les données de la base de données.
Les colonnes suivantes de la table `users` (liste des membres) renverront toujours `NULL` :
* `password`
* `pgp_key`
* `otp_secret`
Tenter de lire les données des tables suivantes résultera également en une erreur :
* emails
* emails_queue
* compromised_passwords_cache
* compromised_passwords_cache_ranges
* api_credentials
* plugins_signals
* config
* users_sessions
* logs