Comme annoncé, voici la documentation d’installation locale de la v3, suivie de bout en bout sur mon poste, et un correctif des trois importeurs qui remplace celui de mon message précédent. Les deux sont repliés en bas.
Faute d’export réel, j’ai testé chatons et services sur un jeu d’essai reconstitué depuis les pages publiques : 84 chatons, 671 services. Avec le correctif, tous sont importés et rattachés au bon chaton, et 63 chatons ont leurs coordonnées. Les services ont tous leur adresse si le champ de l’export s’appelle bien field_service_url (question 3).
Documentation d'installation locale
Cette documentation décrit l’installation, sur un poste de travail, de YesWiki avec la
personnalisation du dépôt framagit.org/chatons/chatons.org et l’extension d’import. Elle a
été suivie de bout en bout le 6 octobre 2026 sur macOS 26.7 avec Homebrew, PHP 8.4.26 et
MariaDB 13.0.2. Sur Debian ou Ubuntu, les étapes sont les mêmes avec les paquets PHP et MariaDB
de la distribution ; je ne les ai pas testées.
Ce que cette installation ne contient pas : les pages, les listes de valeurs et les réglages
faits à la main sur la démo chatons.yeswiki.pro. Ils ne sont que dans sa base de données, pas
dans le dépôt. Sans un export de cette base, on obtient les formulaires et les fiches importées,
mais ni les pages de recherche, ni le thème configuré.
1. Prérequis
- PHP 8.3 ou plus récent, avec les extensions ctype, curl, fileinfo, filter, gd, iconv, json,
mbstring, mysqli, pcre et zip (exigées par le composer.json de YesWiki 4.6.9 ; le README
de YesWiki indique encore PHP 7.3, il n’est plus à jour).
- MySQL ou MariaDB.
- git et curl.
Sur macOS, php@8.4 n’est pas lié par défaut dans Homebrew :
brew install php@8.4 mariadb
brew link --force php@8.4
php -v && php -m # vérifier la version et la liste des extensions ci-dessus
2. Base de données
Démarrer MariaDB pour la session, sans l’inscrire au démarrage du poste :
/opt/homebrew/opt/mariadb/bin/mariadbd-safe --datadir=/opt/homebrew/var/mysql --bind-address=127.0.0.1 &
Créer un dossier de travail, la base et un utilisateur dédié. Le mot de passe se garde dans un
fichier lisible par vous seul, jamais dans un message ni dans le dépôt :
mkdir -p ~/yeswiki-chatons && cd ~/yeswiki-chatons
MDP=$(openssl rand -hex 16)
( umask 077 && printf 'DB_PASS=%s\n' "$MDP" > .env.local )
mariadb -e "CREATE DATABASE yeswiki_chatons CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'yeswiki'@'127.0.0.1' IDENTIFIED BY '$MDP';
GRANT ALL ON yeswiki_chatons.* TO 'yeswiki'@'127.0.0.1';"
unset MDP
3. YesWiki
Prendre l’archive de repository.yeswiki.net, qui contient déjà les dépendances (vendor/),
et vérifier son empreinte :
R=https://repository.yeswiki.net/doryphore
curl -O $R/yeswiki-doryphore-4.6.9.zip
echo "$(curl -s $R/yeswiki-doryphore-4.6.9.zip.sha256 | cut -c1-64) yeswiki-doryphore-4.6.9.zip" | shasum -a 256 -c
unzip yeswiki-doryphore-4.6.9.zip && mv doryphore site
cd site
php -S 127.0.0.1:8099
Le serveur occupe ce terminal : les étapes suivantes se font dans un second terminal, depuis
~/yeswiki-chatons/site.
Ouvrir http://127.0.0.1:8099/ : l’installateur demande le nom du site, les accès à la base
(hôte 127.0.0.1, base yeswiki_chatons, utilisateur yeswiki, mot de passe de .env.local)
et le compte administrateur. Il écrit wakka.config.php.
4. Extension d’import
R=https://repository.yeswiki.net/doryphore
curl -O $R/extension-importer-4.1.3.zip
echo "$(curl -s $R/extension-importer-4.1.3.zip.sha256 | cut -c1-64) extension-importer-4.1.3.zip" | shasum -a 256 -c
unzip -o extension-importer-4.1.3.zip -d tools/
php includes/commands/console list # la commande importer:sync doit apparaître
5. Personnalisation CHATONS
Le dépôt Framagit est le contenu du dossier custom/ de YesWiki :
rm custom/.gitkeep && rmdir custom
git clone https://framagit.org/chatons/chatons.org.git custom
Il apporte trois importeurs (Chatons, Service, Software), une action de recherche et le
thème de couleurs css-presets/chatons.css. Appliquer ensuite le correctif replié en bas du message, sans lequel
l’import est incomplet (voir la partie 7) :
git -C custom apply ../correctif-importeurs.patch # le correctif replié plus bas, enregistré dans ce fichier
6. Sources de données
Ajouter à la fin du tableau de wakka.config.php (le fichier est modifiable tant qu’on n’a pas
retiré son droit d’écriture) :
'dataSources' => array (
'logiciels' => array (
'importer' => 'Software',
'url' => 'https://apps.yunohost.org/default/v3/apps.json',
'formId' => '101',
),
'chatons' => array (
'importer' => 'Chatons',
'url' => 'À COMPLÉTER',
'formId' => '100',
),
'services' => array (
'importer' => 'Service',
'url' => 'À COMPLÉTER',
'formId' => '102',
),
),
Les numéros 100 et 101 sont imposés : l’importeur des services cherche les chatons dans le
formulaire 100 et les logiciels dans le 101. Pour les services, la démo utilise le formulaire 5 ;
j’ai pris 102 parce que les formulaires 1 à 4 sont ceux de l’installation. L’importeur des
chatons lit trois adresses construites sur url : url/organization, url/structure et
url/location.
L’adresse de l’export JSON du site actuel n’est pas publique : je ne l’ai trouvée ni dans le
dépôt, ni sur le forum, ni par essai. Elle est à demander à qui administre le site Drupal.
Puis, dans cet ordre :
php includes/commands/console importer:sync -s logiciels
php includes/commands/console importer:sync -s chatons
php includes/commands/console importer:sync -s services
Une fois la configuration terminée, retirer le droit d’écriture sur wakka.config.php, comme
l’installateur le recommande : chmod a-w wakka.config.php.
7. Ce que j’ai constaté, et ce que corrige le correctif
Le correctif joint remplace celui de mon message précédent : il y ajoute l’importeur des
logiciels, les coordonnées et le filtre par formulaire.
Le filtre par formulaire est ignoré. Les trois importeurs appellent la recherche de YesWiki
avec la clé formIds ; YesWiki attend formsIds. La recherche porte donc sur toutes les fiches
du wiki, et la comparaison des titres déborde du formulaire visé. Sur une installation neuve,
Framasoft entre en collision avec une fiche d’exemple du formulaire « Ressources », Paheko avec le
logiciel Paheko : les deux chatons ne sont pas importés, et leurs services sont rattachés à la
mauvaise fiche. Framasoft manque aussi sur la démo ; je n’ai pas vérifié si c’est pour la même
raison.
Logiciels. Le catalogue YunoHost relevé le 6 octobre compte 696 applications. L’import
s’arrête au premier logiciel incomplet, après 11 fiches : 25 applications n’ont pas de
description en français et 17 pas d’adresse de code source, deux champs obligatoires. Avec le
correctif, la description anglaise remplace la française absente, et une fiche incomplète est
écartée sans arrêter l’import : 679 fiches importées, les 17 écartées nommées en fin de commande.
« Find My Device » figure deux fois dans le catalogue et donne deux fiches.
Coordonnées. Le formulaire du dépôt nomme le champ carte bf_latitude ; YesWiki 4.6.9 et la
démo le nomment bf_geolocation. L’importeur écrit en plus deux champs séparés que ce champ ne
lit pas, et son expression régulière refuse les longitudes négatives (18 chatons sont à l’ouest
du méridien de Greenwich). Le correctif reprend la ligne du champ carte de la démo et écrit
bf_geolocation.
Chatons et services. Faute d’export réel, j’ai reconstitué un jeu d’essai depuis les pages
publiques du 5 octobre : 84 chatons, 671 services. Il reproduit les défauts de la démo, que le
correctif lève :
|
Code actuel |
Avec le correctif |
| Chatons importés |
82 sur 84 |
84 sur 84 |
| Services avec leur adresse |
0 sur 671 |
671 sur 671 |
| Modalité d’accès renseignée |
0 sur 671 |
671 sur 671 |
| « Mutualisé / dédié » converti |
0 |
650 (21 vides à la source) |
| Codes postaux français sans leur zéro |
4 |
0 |
Chatons avec coordonnées dans bf_geolocation |
0 |
63 |
Trois de ces résultats dépendent d’hypothèses du jeu d’essai, à confirmer sur l’export réel :
le nom du champ field_service_url, le code postal stocké en nombre entier, et la modalité
d’accès, que les pages publiques ne donnent pas et que j’ai remplie en rotation pour exercer les
quatre cas.
Le jeu d’essai ne dit rien des dates de 1970 de la démo : je ne connais pas leur cause, le
format du champ created de l’export réel est à vérifier.
Ce qui reste hors du correctif.
- 359 services sur 671 (99 logiciels) ne sont liés à aucune fiche logiciel. Pour 63 d’entre
eux, seule la casse diffère (« Wordpress » et « WordPress ») ; les autres logiciels manquent
au catalogue YunoHost ou y portent un autre nom (Matrix, Nextcloud Calendar, postfix…).
- 127 services à la fois mutualisés et dédiés reçoivent
mutu,dedie, alors que le champ est une
liste à choix unique. C’est un choix de modèle de données pour le groupe de travail.
- Le type de service n’est toujours pas repris.
- L’option
--wipe de importer:sync est déclarée dans l’extension 4.1.3 mais le code ne
l’utilise pas : elle n’efface rien. Comme chaque importeur saute toute fiche dont le titre
existe déjà, une réimportation ne corrige aucune fiche. Il faut effacer les fiches avant, ce
qui perd les retouches faites dans le wiki (14 fiches de services sur la démo).
8. Ce qui reste à obtenir
- L’adresse de l’export JSON du site Drupal, pour importer les vraies données.
- Un export de la base de la démo, pour retrouver les pages, les listes et les réglages.
Correctif des importeurs (ChatonsImporter.php, ServiceImporter.php, SoftwareImporter.php)
diff --git a/services/ChatonsImporter.php b/services/ChatonsImporter.php
index 99cd66d..d6f7f02 100644
--- a/services/ChatonsImporter.php
+++ b/services/ChatonsImporter.php
@@ -62,7 +62,7 @@ texte***bf_adresse***Adresse*** *** *** *** ***text***0*** *** *** * *** * *** *
texte***bf_codepostal***Code postal*** *** *** *** ***text***0*** *** *** * *** * *** *** *** ***
texte***bf_ville***Ville*** *** *** *** ***text***0*** *** *** * *** * *** *** *** ***
texte***bf_pays***Pays*** *** *** *** ***text***0*** *** *** * *** * *** *** *** ***
-map***bf_latitude***bf_longitude*** *** *** *** ***0***0***
+map***bf_geolocation***Géolocalisation de l'adresse*** *** *** *** ***1***0*** *** *** *** *** *** *** *** *** ***
EOT,
"bn_ce_i18n" => "fr-FR",
"bn_only_one_entry" => "N",
@@ -148,7 +148,7 @@ EOT,
unset($item[$field]);
}
- $re = '/POINT.*\((\d*\.\d*) (\d*\.\d*)\)/m';
+ $re = '/POINT.*\((-?\d+(?:\.\d+)?) (-?\d+(?:\.\d+)?)\)/m';
preg_match_all($re, $item['field_structure_hq'] ?? '', $matches, PREG_SET_ORDER, 0);
$lat = $matches[0][2] ?? '';
$lon = $matches[0][1] ?? '';
@@ -165,20 +165,30 @@ EOT,
'bf_description_' => $item['field_presentation'] ?? '',
'url_website' => $item['field_website_url'] ?? '',
'date_created' => $dateCreated,
- 'bf_codepostal' => $item['field_zip_code'] ?? '',
+ 'bf_codepostal' => $this->fixZipCode($item['field_zip_code'] ?? '', $item['field_country'] ?? ''),
'bf_pays' => $item['field_country'] ?? '',
'bf_ville' => $item['field_city'] ?? '',
'geo_data' => $item['field_geo_area'] ?? '',
- 'bf_latitude' => $lat,
- 'bf_longitude' => $lon
+ // the map field stores both coordinates under bf_geolocation
+ 'bf_geolocation' => ($lat !== '' && $lon !== '') ? ['latitude' => (string) $lat, 'longitude' => (string) $lon] : '',
];
}
return $preparedData;
}
+ // the former website stores zip codes as integers, french leading zeros are lost
+ protected function fixZipCode($zip, $country): string
+ {
+ $zip = trim((string) $zip);
+ if (in_array(mb_strtolower(trim((string) $country)), ['france', 'fr'], true) && preg_match('/^\d{4}$/', $zip)) {
+ return '0'.$zip;
+ }
+ return $zip;
+ }
+
public function syncData($data)
{
- $existingEntries = $this->entryManager->search(['formIds' => [$this->config['formId']]]);
+ $existingEntries = $this->entryManager->search(['formsIds' => [$this->config['formId']]]);
foreach ($data as $entry) {
$res = multiArraySearch($existingEntries, 'bf_titre', $entry['bf_titre']);
if (!$res) {
diff --git a/services/ServiceImporter.php b/services/ServiceImporter.php
index ee0c997..d167cc3 100644
--- a/services/ServiceImporter.php
+++ b/services/ServiceImporter.php
@@ -104,7 +104,7 @@ EOT,
foreach ($jsonorga as $i => $item) {
$service = trim(strip_tags($item['field_software'] ?? '')) ;
$chaton = trim($item['src_name'] ?? '');
- $url = strip_tags($item['field_software_url'] ?? '') ;
+ $url = strip_tags((string) (($item['field_service_url'] ?? '') ?: ($item['field_software_url'] ?? ''))) ;
if (empty($chaton)) {
echo 'Warn: Skipping entry without chaton (index: '.$i.')'."\n";
continue;
@@ -125,20 +125,20 @@ EOT,
["mutu", "dedie"],
$item["field_is_shared"] ?? ''
);
- $mutu_dedie = str_replace([", "], [","], $item["field_is_shared"]);
+ $mutu_dedie = str_replace([", "], [","], $mutu_dedie);
switch ($item["field_access_type"] ?? '') {
case 'Cercle restreint':
$acces = "cercle_restreint";
- // no break
+ break;
case 'Fermé':
$acces = "ferme";
- // no break
+ break;
case 'Inscription obligatoire':
$acces = "manuel";
- // no break
+ break;
case "Ouvert à tou⋅te⋅s":
$acces = "open";
- // no break
+ break;
default:
$acces = '';
}
@@ -159,8 +159,8 @@ EOT,
public function mapData($data): array
{
$preparedData = [];
- $chatons = $this->entryManager->search(['formIds' => [100]]);
- $soft = $this->entryManager->search(['formIds' => [101]]);
+ $chatons = $this->entryManager->search(['formsIds' => [100]]);
+ $soft = $this->entryManager->search(['formsIds' => [101]]);
foreach ($data as $i => $entry) {
$title = $entry['software'].' chez '.$entry['chaton'];
$chaton = $software = null;
@@ -186,7 +186,7 @@ EOT,
public function syncData($data)
{
- $existingEntries = $this->entryManager->search(['formIds' => [$this->config['formId']]]);
+ $existingEntries = $this->entryManager->search(['formsIds' => [$this->config['formId']]]);
foreach ($data as $entry) {
$res = multiArraySearch($existingEntries, 'bf_titre', $entry['bf_titre']);
$res2 = multiArraySearch($existingEntries, 'chaton', $entry['chaton']);
diff --git a/services/SoftwareImporter.php b/services/SoftwareImporter.php
index 72b236a..a0b6414 100644
--- a/services/SoftwareImporter.php
+++ b/services/SoftwareImporter.php
@@ -84,7 +84,7 @@ EOT,
$data = [];
foreach ($jsondata['apps'] as $item) {
$data[$item['id']]['bf_titre'] = $item['manifest']['name'];
- $data[$item['id']]['bf_description'] = $item['manifest']['description']['fr'] ?? '';
+ $data[$item['id']]['bf_description'] = $item['manifest']['description']['fr'] ?? $item['manifest']['description']['en'] ?? '';
$data[$item['id']]['bf_site_officiel'] = $item['manifest']['upstream']['website'] ?? '';
$data[$item['id']]['bf_code_source'] = $item['manifest']['upstream']['code'] ?? '';
$data[$item['id']]['bf_documentation_utilisateurice'] = $item['manifest']['upstream']['userdoc'] ?? '';
@@ -102,17 +102,26 @@ EOT,
public function syncData($data)
{
- $existingEntries = $this->entryManager->search(['formIds' => [$this->config['formId']]]);
+ $existingEntries = $this->entryManager->search(['formsIds' => [$this->config['formId']]]);
+ $rejected = [];
foreach ($data as $entry) {
$res = multiArraySearch($existingEntries, 'bf_titre', $entry['bf_titre']);
if (!$res) {
$entry['antispam'] = 1;
- $this->entryManager->create($this->config['formId'], $entry, false, $entry['bf_titre']);
- echo 'Import de la fiche "'.$entry['bf_titre'].'".'."\n";
+ try {
+ $this->entryManager->create($this->config['formId'], $entry, false, $entry['bf_titre']);
+ echo 'Import de la fiche "'.$entry['bf_titre'].'".'."\n";
+ } catch (\Exception $e) {
+ // one incomplete entry must not stop the whole import
+ $rejected[] = $entry['bf_titre'].' ('.$e->getMessage().')';
+ }
} else {
echo 'La fiche "'.$entry['bf_titre'].'" existe déja.'."\n";
}
}
+ if (!empty($rejected)) {
+ throw new \Exception(count($rejected).' fiche(s) ignorée(s) : '.implode(' ; ', $rejected));
+ }
return;
}
Merci et bonne journée.
Arnaud, ALTERNATECH