Version in base suite: 4.4.21+dfsg-0+deb13u1 Version in overlay suite: 4.4.28+dfsg-0+deb13u1 Base version: spip_4.4.28+dfsg-0+deb13u1 Target version: spip_4.4.30+dfsg-0+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/s/spip/spip_4.4.28+dfsg-0+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/s/spip/spip_4.4.30+dfsg-0+deb13u1.dsc CHANGELOG.md | 8 composer.lock | 95 +- debian/changelog | 12 debian/patches/0001-Fix-created-directories-and-files-default-rights.patch | 2 debian/patches/0003-Fix-displayed-version-in-the-private-interface.patch | 2 ecrire/CHANGELOG.md | 45 + ecrire/action/editer_logo.php | 2 ecrire/balise/formulaire_.php | 6 ecrire/base/connect_sql.php | 4 ecrire/genie/optimiser.php | 15 ecrire/genie/optimiser_contextes.php | 46 + ecrire/inc/acces.php | 2 ecrire/inc/admin.php | 9 ecrire/inc/config.php | 2 ecrire/inc/cvt_multietapes.php | 5 ecrire/inc/distant.php | 16 ecrire/inc/editer.php | 4 ecrire/inc/filtres.php | 159 --- ecrire/inc/genie.php | 3 ecrire/inc/headers.php | 14 ecrire/inc/meta.php | 13 ecrire/inc/queue.php | 10 ecrire/inc/texte_mini.php | 104 -- ecrire/inc/utils.php | 29 ecrire/inc_version.php | 6 ecrire/paquet.xml | 2 ecrire/public.php | 28 ecrire/public/aiguiller.php | 17 ecrire/public/assembler.php | 29 ecrire/public/cacher.php | 15 ecrire/public/criteres.php | 68 + ecrire/public/interfaces.php | 2 ecrire/src/Calcul/AjaxContext.php | 400 ++++++++++ ecrire/src/Chiffrer/SpipCles.php | 19 ecrire/src/Texte/Collecteur/AbstractCollecteur.php | 173 ++++ ecrire/src/Texte/Collecteur/Idiomes.php | 5 ecrire/src/Texte/Collecteur/Modeles.php | 2 ecrire/src/Texte/Collecteur/Multis.php | 16 ecrire/src/Texte/Collecteur/QuotedStrings.php | 93 +- plugins-dist.json | 18 plugins-dist/bigup/.editorconfig | 2 plugins-dist/bigup/CHANGELOG.md | 8 plugins-dist/bigup/inc/Bigup/Identifier.php | 11 plugins-dist/bigup/paquet.xml | 2 plugins-dist/bigup/phpstan-baseline.neon | 7 plugins-dist/medias/CHANGELOG.md | 12 plugins-dist/medias/inc/documents.php | 2 plugins-dist/medias/metadata/html.php | 43 - plugins-dist/medias/metadata/svg.php | 41 + plugins-dist/medias/paquet.xml | 2 plugins-dist/medias/prive/objets/editer/document_popup.html | 10 plugins-dist/mots/.editorconfig | 2 plugins-dist/mots/CHANGELOG.md | 6 plugins-dist/mots/action/supprimer_mot.php | 9 plugins-dist/mots/paquet.xml | 2 plugins-dist/porte_plume/CHANGELOG.md | 13 plugins-dist/porte_plume/javascript/jquery.markitup_pour_spip.js | 7 plugins-dist/porte_plume/paquet.xml | 2 plugins-dist/porte_plume/porte_plume_fonctions.php | 24 plugins-dist/porte_plume/tests/BarreOutilsMarkitupTest.php | 2 plugins-dist/porte_plume/tests/TraitementsPrevisuTest.php | 46 + plugins-dist/revisions/.editorconfig | 2 plugins-dist/revisions/CHANGELOG.md | 6 plugins-dist/revisions/inc/revisions.php | 6 plugins-dist/revisions/inc/suivi_versions.php | 6 plugins-dist/revisions/paquet.xml | 2 plugins-dist/revisions/prive/objets/contenu/revision.html | 6 plugins-dist/safehtml/CHANGELOG.md | 13 plugins-dist/safehtml/paquet.xml | 2 plugins-dist/safehtml/sanitizer/html.php | 11 plugins-dist/safehtml/sanitizer/svg.php | 3 plugins-dist/safehtml/src/Texte/Collecteur/MetasMicroData.php | 13 plugins-dist/sites/.editorconfig | 2 plugins-dist/sites/CHANGELOG.md | 11 plugins-dist/sites/action/editer_site.php | 31 plugins-dist/sites/formulaires/editer_site.html | 6 plugins-dist/sites/formulaires/editer_site.php | 7 plugins-dist/sites/formulaires/site.php | 12 plugins-dist/sites/inc/site.php | 13 plugins-dist/sites/paquet.xml | 2 plugins-dist/sites/phpstan-baseline.neon | 18 plugins-dist/statistiques/CHANGELOG.md | 13 plugins-dist/statistiques/genie/visites.php | 28 plugins-dist/statistiques/paquet.xml | 2 plugins-dist/statistiques/prive/squelettes/contenu/stats_visites.html | 2 plugins-dist/statistiques/public/stats.php | 9 plugins-dist/statistiques/tests/StatsNettoyerRefererTest.php | 12 plugins-dist/svp/CHANGELOG.md | 6 plugins-dist/svp/formulaires/admin_plugin.php | 22 plugins-dist/svp/formulaires/charger_plugin.php | 11 plugins-dist/svp/inc/svp_decider.php | 12 plugins-dist/svp/paquet.xml | 2 plugins-dist/svp/phpstan-baseline.neon | 18 prive/CHANGELOG.md | 20 prive/formulaires/editer_auteur.html | 7 prive/formulaires/editer_auteur.php | 12 prive/formulaires/editer_liens.php | 139 ++- prive/formulaires/inc-instituer_auteur.html | 6 prive/formulaires/login.php | 10 prive/identifiants.html | 2 prive/javascript/SearchHighlight.js | 82 +- prive/login.html | 2 prive/objets/liste/jobs.html | 4 prive/objets/liste/jobs_fonctions.php | 30 prive/spip_pass.html | 2 vendor/composer/installed.json | 75 + vendor/composer/installed.php | 36 vendor/symfony/filesystem/Filesystem.php | 14 vendor/symfony/filesystem/Path.php | 24 vendor/symfony/polyfill-mbstring/Mbstring.php | 16 vendor/symfony/polyfill-mbstring/Resources/unidata/eastAsianWidthRegexp.php | 5 vendor/symfony/polyfill-mbstring/Resources/unidata/titleCase.php | 139 +++ 112 files changed, 1945 insertions(+), 742 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpl8c4ltg8/spip_4.4.28+dfsg-0+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpl8c4ltg8/spip_4.4.30+dfsg-0+deb13u1.dsc: no acceptable signature found diff -Nru spip-4.4.28+dfsg/CHANGELOG.md spip-4.4.30+dfsg/CHANGELOG.md --- spip-4.4.28+dfsg/CHANGELOG.md 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/CHANGELOG.md 2026-11-05 13:48:46.000000000 +0000 @@ -1,5 +1,13 @@ # Changelog +## 4.4.30 - 2026-11-05 + +- Mise à jour des dépendances (ecrire, prive, medias, porte-plume, safehtml, stats) + +## 4.4.29 - 2026-11-04 + +- Mise à jour des dépendances (ecrire, prive, bigup, medias, mots, porte-plume, revisions, safehtml, sites, stats, svp) + ## 4.4.28 - 2026-10-07 - Mise à jour des dépendances (ecrire, safehtml) diff -Nru spip-4.4.28+dfsg/composer.lock spip-4.4.30+dfsg/composer.lock --- spip-4.4.28+dfsg/composer.lock 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/composer.lock 2026-11-05 13:48:46.000000000 +0000 @@ -60,7 +60,8 @@ "issues": "https://github.com/algo26-matthias/idna-convert/issues", "source": "https://github.com/algo26-matthias/idna-convert/tree/v3.2.1" }, - "time": "2025-10-10T11:40:12+00:00" + "time": "2025-10-10T11:40:12+00:00", + "published-time": "2026-01-20T07:57:26+00:00" }, { "name": "jakeasmith/http_build_url", @@ -99,7 +100,8 @@ "source": "https://github.com/jakeasmith/http_build_url" }, "abandoned": true, - "time": "2026-09-15T07:48:37+00:00" + "time": "2026-09-15T07:48:37+00:00", + "published-time": "2026-09-15T07:51:45+00:00" }, { "name": "psr/container", @@ -147,7 +149,8 @@ "issues": "https://github.com/php-fig/container/issues", "source": "https://github.com/php-fig/container/tree/1.1.2" }, - "time": "2021-11-05T16:50:12+00:00" + "time": "2021-11-05T16:50:12+00:00", + "published-time": "2021-11-05T16:53:52+00:00" }, { "name": "spip-league/composer-installer", @@ -319,16 +322,16 @@ }, { "name": "spip/ecrire", - "version": "4.4.28", + "version": "4.4.30", "source": { "type": "git", "url": "https://git.spip.net/spip/ecrire.git", - "reference": "c313201ded053f6f3d08f7867e62c740ba315540" + "reference": "44245dbbc28cf11f409104d296e44413763a80f5" }, "dist": { "type": "zip", - "url": "https://git.spip.net/api/v4/projects/spip%2Fecrire/repository/archive.zip?sha=c313201ded053f6f3d08f7867e62c740ba315540", - "reference": "c313201ded053f6f3d08f7867e62c740ba315540", + "url": "https://git.spip.net/api/v4/projects/spip%2Fecrire/repository/archive.zip?sha=44245dbbc28cf11f409104d296e44413763a80f5", + "reference": "44245dbbc28cf11f409104d296e44413763a80f5", "shasum": "" }, "require": { @@ -385,23 +388,23 @@ ], "description": "Noyau de SPIP", "support": { - "source": "https://git.spip.net/spip/ecrire/-/tree/4.4.28", + "source": "https://git.spip.net/spip/ecrire/-/tree/4.4.30", "issues": "https://git.spip.net/spip/ecrire/-/issues" }, - "time": "2026-10-07T15:47:13+02:00" + "time": "2026-11-05T14:44:28+01:00" }, { "name": "spip/prive", - "version": "1.1.7", + "version": "1.1.9", "source": { "type": "git", "url": "https://git.spip.net/spip/prive.git", - "reference": "b68902b53cb29ac8d3ec7c3b12c5e43b23281c40" + "reference": "fae2860c67924cca5a66e7b4120a059201f1dd54" }, "dist": { "type": "zip", - "url": "https://git.spip.net/api/v4/projects/spip%2Fprive/repository/archive.zip?sha=b68902b53cb29ac8d3ec7c3b12c5e43b23281c40", - "reference": "b68902b53cb29ac8d3ec7c3b12c5e43b23281c40", + "url": "https://git.spip.net/api/v4/projects/spip%2Fprive/repository/archive.zip?sha=fae2860c67924cca5a66e7b4120a059201f1dd54", + "reference": "fae2860c67924cca5a66e7b4120a059201f1dd54", "shasum": "" }, "require-dev": { @@ -439,10 +442,10 @@ ], "description": "Squelettes de l'espace privé", "support": { - "source": "https://git.spip.net/spip/prive/-/tree/1.1.7", + "source": "https://git.spip.net/spip/prive/-/tree/1.1.9", "issues": "https://git.spip.net/spip/prive/-/issues" }, - "time": "2026-10-06T11:58:29+02:00" + "time": "2026-11-05T14:36:42+01:00" }, { "name": "spip/security", @@ -573,7 +576,8 @@ "type": "tidelift" } ], - "time": "2024-11-20T10:51:57+00:00" + "time": "2024-11-20T10:51:57+00:00", + "published-time": "2024-11-27T12:45:18+00:00" }, { "name": "symfony/deprecation-contracts", @@ -640,20 +644,21 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2024-09-25T14:11:13+00:00", + "published-time": "2024-11-27T12:25:26+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.45", + "version": "v5.4.56", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "57c8294ed37d4a055b77057827c67f9558c95c54" + "reference": "97768a0b5c3495c2a2469dfc1f3db00e006fc5d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/57c8294ed37d4a055b77057827c67f9558c95c54", - "reference": "57c8294ed37d4a055b77057827c67f9558c95c54", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/97768a0b5c3495c2a2469dfc1f3db00e006fc5d5", + "reference": "97768a0b5c3495c2a2469dfc1f3db00e006fc5d5", "shasum": "" }, "require": { @@ -691,7 +696,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.45" + "source": "https://github.com/symfony/filesystem/tree/v5.4.56" }, "funding": [ { @@ -703,11 +708,16 @@ "type": "github" }, { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-22T13:05:35+00:00" + "time": "2026-11-03T06:18:36+00:00", + "published-time": "2026-11-04T10:51:37+00:00" }, { "name": "symfony/polyfill-ctype", @@ -790,20 +800,21 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-04-10T16:19:22+00:00", + "published-time": "2026-04-26T14:44:50+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.43.0", + "version": "v1.43.3", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", - "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/122889ed24dc000165bcbfd77ec412f15cbdfc6a", + "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a", "shasum": "" }, "require": { @@ -855,7 +866,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.3" }, "funding": [ { @@ -875,7 +886,8 @@ "type": "tidelift" } ], - "time": "2026-09-24T15:19:04+00:00" + "time": "2026-10-28T09:35:49+00:00", + "published-time": "2026-10-28T10:07:45+00:00" }, { "name": "symfony/polyfill-php80", @@ -959,7 +971,8 @@ "type": "tidelift" } ], - "time": "2026-09-25T15:50:05+00:00" + "time": "2026-09-25T15:50:05+00:00", + "published-time": "2026-09-30T10:23:35+00:00" }, { "name": "symfony/polyfill-php81", @@ -1039,7 +1052,8 @@ "type": "tidelift" } ], - "time": "2026-05-26T12:45:58+00:00" + "time": "2026-05-26T12:45:58+00:00", + "published-time": "2026-05-26T13:40:34+00:00" }, { "name": "symfony/polyfill-php82", @@ -1119,7 +1133,8 @@ "type": "tidelift" } ], - "time": "2026-09-25T15:50:05+00:00" + "time": "2026-09-25T15:50:05+00:00", + "published-time": "2026-09-30T10:23:32+00:00" }, { "name": "symfony/polyfill-php83", @@ -1199,7 +1214,8 @@ "type": "tidelift" } ], - "time": "2026-09-25T15:50:05+00:00" + "time": "2026-09-25T15:50:05+00:00", + "published-time": "2026-09-30T10:23:43+00:00" }, { "name": "symfony/polyfill-php84", @@ -1279,7 +1295,8 @@ "type": "tidelift" } ], - "time": "2026-09-23T22:01:09+00:00" + "time": "2026-09-23T22:01:09+00:00", + "published-time": "2026-09-30T10:23:33+00:00" }, { "name": "symfony/polyfill-php85", @@ -1359,7 +1376,8 @@ "type": "tidelift" } ], - "time": "2026-09-25T15:46:47+00:00" + "time": "2026-09-25T15:46:47+00:00", + "published-time": "2026-09-30T10:23:32+00:00" }, { "name": "symfony/service-contracts", @@ -1442,7 +1460,8 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:11:13+00:00" + "time": "2024-09-25T14:11:13+00:00", + "published-time": "2024-11-27T12:25:27+00:00" } ], "packages-dev": [], @@ -1458,5 +1477,5 @@ "platform-overrides": { "php": "7.4.33" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.11.0" } diff -Nru spip-4.4.28+dfsg/debian/changelog spip-4.4.30+dfsg/debian/changelog --- spip-4.4.28+dfsg/debian/changelog 2026-10-08 17:15:42.000000000 +0000 +++ spip-4.4.30+dfsg/debian/changelog 2026-11-05 16:35:24.000000000 +0000 @@ -1,3 +1,15 @@ +spip (4.4.30+dfsg-0+deb13u1) trixie-security; urgency=medium + + [ Matthieu Marcillaud ] + * build: up dependencies + * build: Version 4.4.30 + + Fix execution of arbitrary code [CVE-2026-122032] + + [ David Prévot ] + * Refresh patches + + -- David Prévot Thu, 05 Nov 2026 17:35:24 +0100 + spip (4.4.28+dfsg-0+deb13u1) trixie-security; urgency=medium [ Matthieu Marcillaud ] diff -Nru spip-4.4.28+dfsg/debian/patches/0001-Fix-created-directories-and-files-default-rights.patch spip-4.4.30+dfsg/debian/patches/0001-Fix-created-directories-and-files-default-rights.patch --- spip-4.4.28+dfsg/debian/patches/0001-Fix-created-directories-and-files-default-rights.patch 2026-10-08 17:14:45.000000000 +0000 +++ spip-4.4.30+dfsg/debian/patches/0001-Fix-created-directories-and-files-default-rights.patch 2026-11-05 16:35:10.000000000 +0000 @@ -13,7 +13,7 @@ 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecrire/inc_version.php b/ecrire/inc_version.php -index 7405d1a..aea3d46 100644 +index a014cc5..0f0b8ba 100644 --- a/ecrire/inc_version.php +++ b/ecrire/inc_version.php @@ -443,7 +443,7 @@ $liste_des_authentifications = [ diff -Nru spip-4.4.28+dfsg/debian/patches/0003-Fix-displayed-version-in-the-private-interface.patch spip-4.4.30+dfsg/debian/patches/0003-Fix-displayed-version-in-the-private-interface.patch --- spip-4.4.28+dfsg/debian/patches/0003-Fix-displayed-version-in-the-private-interface.patch 2026-10-08 17:14:45.000000000 +0000 +++ spip-4.4.30+dfsg/debian/patches/0003-Fix-displayed-version-in-the-private-interface.patch 2026-11-05 16:35:10.000000000 +0000 @@ -14,7 +14,7 @@ 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecrire/inc_version.php b/ecrire/inc_version.php -index aea3d46..4f73442 100644 +index 0f0b8ba..1274b0d 100644 --- a/ecrire/inc_version.php +++ b/ecrire/inc_version.php @@ -468,7 +468,7 @@ $spip_sql_version = 1; diff -Nru spip-4.4.28+dfsg/ecrire/CHANGELOG.md spip-4.4.30+dfsg/ecrire/CHANGELOG.md --- spip-4.4.28+dfsg/ecrire/CHANGELOG.md 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/CHANGELOG.md 2026-11-05 13:48:46.000000000 +0000 @@ -2,6 +2,51 @@ Changelog de SPIP 4.4 +## 4.4.30 - 2026-11-05 + +### Fixed + +- #206 Corrections des critères `par num` et `par sinum` dynamiques + +## 4.4.29 - 2026-11-04 + +### Security + +- spip-security/securite#4961 gérer l'échappement des tri par expression dynamiques +- spip-security/securite#4956 les echappements base64 génèrent des nom de class non prédictibles +- spip-security/securite#4954#note_235999 quand le redirect d'un formulaire provient de la QS il faut que l'URL soit sécurisée +- spip-security/securite#4966 `spip_sanitize_from_request()` ne préjuge pas de la provenance de `_request()` +- spip-security/securite#4966 coherence de `calculer_contexte()` avec `_request()` +- spip-security/securite#4954#note_235984 quand un webmestre affiche un squelette d'un sous répertoire on sanitize `$_GET` +- spip-security/securite#4954#note_235979 des attribut_html() manquaient sur inc/admin +- spip-security/securite#4954#note_235958 pas de config_fonc venant de la requete + +### Added + +- #196 on peut supprimer le stockage d'un env de variable qui serait sensible +- #196 une class AjaxContext pour encoder/decoder les contextes Ajax selon les cas d'utilisation +- #205 `SpipCles::secret_perso('monplugin')` permet de générer une clé secrète composite pour un plugin + +### Fixed + +- #163 Éviter warning sur get_dns_records +- #178 pas prendre noajax pour ajax +- !446 retablir le fonctionnement des alea cassé par !446 +- #64 les nouveaux mots de passe doivent avoir 12 caractères minimum +- #196 une méthode `contextFileName` pour ne calculer le nom du fichier contexte qu'à un seul endroit +- #196#note_235280 si la constante `_MEMOIZE_CONTEXTES_AJAX` est définie à true, on stocke les contextes via memoization +- #196 ne pas memoriser les `$_POST` de formulaire dans les contextes ajax +- #196 on distingue la stratégie de stockage des arguments des formulaires selon la nature du visiteur +- #196 Contextes ajax stockés en base64 dans des sous-répertoires `tmp/cache/contextes/ab/`, et hash vérifié au décodage +- #196 Purge des contextes ajax dans une tâche cron horaire dédiée (optimiser_contextes), jusqu'à 50000 fichiers par passage +- #196 Exclure les dates par défaut du nom des fichiers de contextes ajax (un seul fichier par contexte, réécrit avec la dernière date) +- #196 Ne pas générer de bloc ajax dans `recuperer_fond()` pour les robots +- #196 Ne pas stocker de contexte ajax pour un bloc vide ou un contexte contenant du PHP +- !444 réparer la migation des logos +- #176 base64_encode les arguments des fonctions de job_queue +- #202 ne pas renvoyer un chemin vide si la création du sous-repertoire cache a échoué +- #195 Pas de numéro sur les noms d’auteurs + ## 4.4.28 - 2026-10-07 ### Fixed diff -Nru spip-4.4.28+dfsg/ecrire/action/editer_logo.php spip-4.4.30+dfsg/ecrire/action/editer_logo.php --- spip-4.4.28+dfsg/ecrire/action/editer_logo.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/action/editer_logo.php 2026-11-05 13:48:46.000000000 +0000 @@ -227,7 +227,9 @@ // logo_modifier commence par supprimer le logo existant, donc on le deplace pour pas le perdre @rename($d, $dir_logos . $nom); // et on le declare comme nouveau logo + autoriser_exception('iconifier', $objet, $id_objet); logo_modifier($objet, $id_objet, $mode, $dir_logos . $nom); + autoriser_exception('iconifier', $objet, $id_objet, false); if ($date_modif) { sql_updateq($table, ['date_modif' => $date_modif], "$_id_objet=$id_objet"); } diff -Nru spip-4.4.28+dfsg/ecrire/balise/formulaire_.php spip-4.4.30+dfsg/ecrire/balise/formulaire_.php --- spip-4.4.28+dfsg/ecrire/balise/formulaire_.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/balise/formulaire_.php 2026-11-05 13:48:46.000000000 +0000 @@ -192,7 +192,8 @@ ($post_form = _request('formulaire_action')) && $post_form == $form && ($p = _request('formulaire_action_args')) - && is_array($p = decoder_contexte_ajax($p, $post_form)) + && ($ajaxContext = new \Spip\Calcul\AjaxContext()) + && is_array($p = $ajaxContext->decodeFormContext($p, $post_form, !empty($GLOBALS['visiteur_session']['id_auteur']))) ) { // enlever le faux attribut de langue masque array_shift($p); @@ -274,7 +275,8 @@ // pour permettre de la restaurer au moment du Verifier et du Traiter array_unshift($args, $GLOBALS['spip_lang']); - $valeurs['formulaire_args'] = encoder_contexte_ajax($args, $form); + $ajaxContext = new \Spip\Calcul\AjaxContext(); + $valeurs['formulaire_args'] = $ajaxContext->encodeFormContext($args, $form, !empty($GLOBALS['visiteur_session']['id_auteur']))->getEnv(); $valeurs['erreurs'] = $erreurs; $valeurs['action'] = $action; $valeurs['form'] = $form; diff -Nru spip-4.4.28+dfsg/ecrire/base/connect_sql.php spip-4.4.30+dfsg/ecrire/base/connect_sql.php --- spip-4.4.28+dfsg/ecrire/base/connect_sql.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/base/connect_sql.php 2026-11-05 13:48:46.000000000 +0000 @@ -428,7 +428,7 @@ * @deprecated */ function query_echappe_textes($query, $uniqid = null) { - trigger_deprecation('spip', '4.4', 'Using "%s" is deprecated, use "%s" instead', __FUNCTION__, 'Spip\Texte\Collecteur\QuotedStrings'); + trigger_deprecation('spip', '4.4', 'Using "%s" is deprecated, use "%s" instead', __FUNCTION__, \Spip\Texte\Collecteur\QuotedStrings::class); static $codeEchappements = null; if ($codeEchappements === null || $uniqid) { $uniqid ??= uniqid(); @@ -508,7 +508,7 @@ * @deprecated */ function query_reinjecte_textes($query, $textes) { - trigger_deprecation('spip', '4.4', 'Using "%s" is deprecated, use "%s" instead', __FUNCTION__, 'Spip\Texte\Collecteur\QuotedStrings'); + trigger_deprecation('spip', '4.4', 'Using "%s" is deprecated, use "%s" instead', __FUNCTION__, \Spip\Texte\Collecteur\QuotedStrings::class); // recuperer les codes echappements $codeEchappements = query_echappe_textes(null); diff -Nru spip-4.4.28+dfsg/ecrire/genie/optimiser.php spip-4.4.30+dfsg/ecrire/genie/optimiser.php --- spip-4.4.28+dfsg/ecrire/genie/optimiser.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/genie/optimiser.php 2026-11-05 13:48:46.000000000 +0000 @@ -33,7 +33,6 @@ optimiser_base_une_table(); optimiser_base(); - optimiser_caches_contextes(); // la date souhaitee pour le tour suivant = apres-demain a 4h du mat ; // sachant qu'on a un delai de 48h, on renvoie aujourd'hui a 4h du mat @@ -43,20 +42,6 @@ } /** - * Vider les contextes ajax de plus de 48h - */ -function optimiser_caches_contextes() { - sous_repertoire(_DIR_CACHE, 'contextes'); - if (is_dir($d = _DIR_CACHE . 'contextes')) { - include_spip('inc/invalideur'); - purger_repertoire($d, [ - 'mtime' => time() - 48 * 3600, - 'limit' => 10000, - ]); - } -} - -/** * Optimise la base de données * * Supprime les relicats d'éléments qui ont disparu diff -Nru spip-4.4.28+dfsg/ecrire/genie/optimiser_contextes.php spip-4.4.30+dfsg/ecrire/genie/optimiser_contextes.php --- spip-4.4.28+dfsg/ecrire/genie/optimiser_contextes.php 1970-01-01 00:00:00.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/genie/optimiser_contextes.php 2026-11-05 13:48:46.000000000 +0000 @@ -0,0 +1,46 @@ + time() - 48 * 3600, + 'limit' => $limit, + ]); + } +} diff -Nru spip-4.4.28+dfsg/ecrire/inc/acces.php spip-4.4.30+dfsg/ecrire/inc/acces.php --- spip-4.4.28+dfsg/ecrire/inc/acces.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/acces.php 2026-11-05 13:48:46.000000000 +0000 @@ -136,7 +136,7 @@ ecrire_meta('alea_ephemere_ancien', @$GLOBALS['meta']['alea_ephemere'], 'non'); $GLOBALS['meta']['alea_ephemere'] = md5(creer_uniqid()); ecrire_meta('alea_ephemere', $GLOBALS['meta']['alea_ephemere'], 'non'); - ecrire_meta('alea_ephemere_date', time(), 'non'); + ecrire_meta('date_alea_ephemere', time(), 'non'); spip_log("renouvellement de l'alea_ephemere"); } diff -Nru spip-4.4.28+dfsg/ecrire/inc/admin.php spip-4.4.30+dfsg/ecrire/inc/admin.php --- spip-4.4.28+dfsg/ecrire/inc/admin.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/admin.php 2026-11-05 13:48:46.000000000 +0000 @@ -193,6 +193,7 @@ return $minipage->page(); } else { + include_spip('inc/filtres'); $dir = dir_admin(); $signal = fichier_admin($script); if (@file_exists($dir . $signal)) { @@ -212,7 +213,7 @@ return ''; } - $corps .= ''; + $corps .= ''; $suivant = _T('bouton_valider'); $js = ''; } else { @@ -229,7 +230,7 @@ . "\n" . "" . $signal . '' . "" . _T('info_creer_repertoire_2', ['repertoire' => joli_repertoire($dir)]) . ''; @@ -293,8 +294,8 @@ include_spip('inc/filtres'); foreach (array_merge($_POST, $_GET) as $n => $c) { if (!in_array($n, ['fichier', 'exec', 'validation_admin']) && !is_array($c)) { - $suite .= "\n"; } } diff -Nru spip-4.4.28+dfsg/ecrire/inc/config.php spip-4.4.30+dfsg/ecrire/inc/config.php --- spip-4.4.28+dfsg/ecrire/inc/config.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/config.php 2026-11-05 13:48:46.000000000 +0000 @@ -428,7 +428,7 @@ 'adresse_site', 'alea_ephemere_ancien', 'alea_ephemere', - 'alea_ephemere_date', + 'date_alea_ephemere', 'langue_site', 'langues_proposees', 'date_calcul_rubriques', diff -Nru spip-4.4.28+dfsg/ecrire/inc/cvt_multietapes.php spip-4.4.30+dfsg/ecrire/inc/cvt_multietapes.php --- spip-4.4.28+dfsg/ecrire/inc/cvt_multietapes.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/cvt_multietapes.php 2026-11-05 13:48:46.000000000 +0000 @@ -79,7 +79,8 @@ if ( $form && ($c = _request('cvtm_prev_post')) - && ($c = decoder_contexte_ajax($c, $form)) + && is_string($c) + && ($c = (new \Spip\Calcul\AjaxContext())->decodeVarValue($c, $form)) ) { # var_dump($c); @@ -139,7 +140,7 @@ } } include_spip('inc/filtres'); - $c = encoder_contexte_ajax($post, $form); + $c = (new \Spip\Calcul\AjaxContext())->encodeVarValue($post, $form)->getEnv(); $valeurs['_hidden'] ??= ''; $valeurs['_hidden'] .= ""; // marquer comme fait, pour eviter double encodage (unipotence) diff -Nru spip-4.4.28+dfsg/ecrire/inc/distant.php spip-4.4.30+dfsg/ecrire/inc/distant.php --- spip-4.4.28+dfsg/ecrire/inc/distant.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/distant.php 2026-11-05 13:48:46.000000000 +0000 @@ -274,13 +274,25 @@ if (!$is_known_host) { $host = trim($parsed_url['host'], '.'); - if (! $ip = filter_var($host, FILTER_VALIDATE_IP)) { + $ip = filter_var($host, FILTER_VALIDATE_IP); + if (!$ip) { $ip = gethostbyname($host); if ($ip === $host) { // Error condition for gethostbyname() $ip = false; } - if ($records = dns_get_record($host)) { + set_error_handler(static function (int $severity, string $message, string $file, int $line): bool { + throw new ErrorException($message, 0, $severity, $file, $line); + }); + try { + $records = dns_get_record($host); + } catch (ErrorException $e) { + $records = false; + spip_log("Warning dns_get_record($host) : " . $e->getMessage(), 'distant' . _LOG_ERREUR); + } finally { + restore_error_handler(); + } + if ($records) { foreach ($records as $record) { // il faut que le TTL soit suffisant afin d'etre certain que le copie_locale eventuel qui suit // se fasse sur la meme IP diff -Nru spip-4.4.28+dfsg/ecrire/inc/editer.php spip-4.4.30+dfsg/ecrire/inc/editer.php --- spip-4.4.28+dfsg/ecrire/inc/editer.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/editer.php 2026-11-05 13:48:46.000000000 +0000 @@ -208,7 +208,7 @@ $row = []; } - // on accepte pas une fonction de config inconnue si elle vient d'un modele + // on accepte pas une fonction de config inconnue si elle vient d'un modele ou d'une request if ( $config_fonc && !in_array($config_fonc, ['articles_edit_config', 'rubriques_edit_config', 'auteurs_edit_config']) @@ -219,6 +219,8 @@ && in_array($config_fonc, $args) ) { $config_fonc = ''; + } elseif (in_array($config_fonc, $_POST) || in_array($config_fonc, $_GET)) { + $config_fonc = ''; } } diff -Nru spip-4.4.28+dfsg/ecrire/inc/filtres.php spip-4.4.30+dfsg/ecrire/inc/filtres.php --- spip-4.4.28+dfsg/ecrire/inc/filtres.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/filtres.php 2026-11-05 13:48:46.000000000 +0000 @@ -9,6 +9,8 @@ * Ce programme est un logiciel libre distribué sous licence GNU/GPL. */ +use Spip\Texte\Collecteur\HtmlTag as CollecteurHtmlTag; + /** * Déclaration de filtres pour les squelettes */ @@ -801,7 +803,9 @@ include_spip('inc/texte'); $flags = ($quote ? ENT_QUOTES : ENT_NOQUOTES); $flags |= ENT_HTML401; - $texte = spip_htmlspecialchars(echappe_retour(echappe_html($texte, '', true), '', 'proteger_amp'), $flags); + $texte = echappe_html($texte, '', true); + $texte = CollecteurHtmlTag::retablir_depuisHtmlBase64($texte, '', 'proteger_amp'); + $texte = spip_htmlspecialchars($texte, $flags); if ($tout) { return corriger_toutes_entites_html($texte); } else { @@ -1632,7 +1636,7 @@ $texte = $collecteurModeles->retablir($texte); } - $texte = echappe_retour($texte); + $texte = CollecteurHtmlTag::retablir_depuisHtmlBase64($texte); return $texte . $fin; } @@ -1648,8 +1652,6 @@ * * @filtre * @uses inc_traduire_dist() - * @uses code_echappement() - * @uses echappe_retour() * * @param string $letexte * @param string $lang @@ -3930,9 +3932,6 @@ * * L'entrée peut-être sérialisée (le `#ENV**` des fonds ajax et ajax_stat) * - * @see decoder_contexte_ajax() - * @uses calculer_cle_action() - * * @param string|array $c * contexte, peut etre un tableau serialize * @param string $form @@ -3943,100 +3942,32 @@ * ajaxid pour cibler le bloc et forcer sa mise a jour * @return string * hash du contexte + * @deprecated */ function encoder_contexte_ajax($c, $form = '', $emboite = null, $ajaxid = '') { - $env = null; - if ( - is_string($c) && @unserialize($c) !== false - ) { - $c = unserialize($c); - } - // supprimer les parametres debut_x - // pour que la pagination ajax ne soit pas plantee - // si on charge la page &debut_x=1 : car alors en cliquant sur l'item 0, - // le debut_x=0 n'existe pas, et on resterait sur 1 - if (is_array($c)) { - foreach ($c as $k => $v) { - if (str_starts_with($k, 'debut_')) { + if ($emboite !== null && !trim($emboite)) { + return ''; + } + $ajaxContext = new \Spip\Calcul\AjaxContext(); + if ($form) { + $ajaxContext->encodeFormContext($c, $form, !empty($GLOBALS['visiteur_session']['id_auteur'])); + } else { + $sensitiveContext = []; + foreach (['fond', 'connect'] as $k) { + if (array_key_exists($k, $c)) { + $sensitiveContext[$k] = $c[$k]; unset($c[$k]); } } - } - - if (!function_exists('calculer_cle_action')) { - include_spip('inc/securiser_action'); - } - - $c = serialize($c); - $cle = calculer_cle_action($form . $c); - $c = "$cle:$c"; - - // Par défaut, on stocke les contextes dans des fichiers en cache - // Exception : si la constante '_CACHE_CONTEXTES_AJAX' == false ET - // que la longueur de l’argument géneré n'est PAS plus long - // que ce qui est toléré. - $cache_contextes_ajax = (!defined('_CACHE_CONTEXTES_AJAX') || _CACHE_CONTEXTES_AJAX); - if (!$cache_contextes_ajax) { - // uniquement si _CACHE_CONTEXTES_AJAX == false - $env = $c; - if (function_exists('gzdeflate') && function_exists('gzinflate')) { - $env = gzdeflate($env); - } - $env = _xor($env); - $env = base64_encode($env); - $len = strlen($env); - // Si l’url est trop longue pour le navigateur - $max_len = _CACHE_CONTEXTES_AJAX_SUR_LONGUEUR; - if ($len > $max_len) { - $cache_contextes_ajax = true; - spip_log( - 'Contextes AJAX forces en fichiers !' - . ' Cela arrive lorsque la valeur du contexte' - . " depasse la longueur maximale autorisee ($max_len). Ici : $len.", - _LOG_AVERTISSEMENT - ); - } - // Sinon si Suhosin est actif et a une la valeur maximale des variables en GET... - elseif ( - $max_len = @ini_get('suhosin.get.max_value_length') - and $max_len < $len - ) { - $cache_contextes_ajax = true; - spip_log('Contextes AJAX forces en fichiers !' - . ' Cela arrive lorsque la valeur du contexte' - . ' depasse la longueur maximale autorisee par Suhosin' - . " ($max_len) dans 'suhosin.get.max_value_length'. Ici : $len." - . ' Vous devriez modifier les parametres de Suhosin' - . ' pour accepter au moins 1024 caracteres.', _LOG_AVERTISSEMENT); - } - } - - if ($cache_contextes_ajax) { - $dir = sous_repertoire(_DIR_CACHE, 'contextes'); - // stocker les contextes sur disque et ne passer qu'un hash dans l'url - $md5 = md5($c); - ecrire_fichier("$dir/c$md5", $c); - $env = $md5; + $ajaxContext->encodeIncludeContext($c, $sensitiveContext); } if ($emboite === null) { - return $env; - } - if (!trim($emboite)) { - return ''; - } - // toujours encoder l'url source dans le bloc ajax - $r = self(); - $r = ' data-origin="' . $r . '"'; - $class = 'ajaxbloc'; - if ($ajaxid && is_string($ajaxid)) { - // ajaxid est normalement conforme a un nom de classe css - // on ne verifie pas la conformite, mais on passe entites_html par dessus par precaution - $class .= ' ajax-id-' . entites_html($ajaxid); + return $ajaxContext->getEnv(); } - return "
\n$emboite
\n"; + return $ajaxContext->emboiteAjaxEnv($emboite, $ajaxid); } /** @@ -4044,9 +3975,6 @@ * * Précude inverse de `encoder_contexte_ajax()` * - * @see encoder_contexte_ajax() - * @uses calculer_cle_action() - * * @param string $c * hash du contexte * @param string $form @@ -4054,48 +3982,15 @@ * @return array|string|bool * - array|string : contexte d'environnement, possiblement sérialisé * - false : erreur de décodage + * @deprecated */ function decoder_contexte_ajax($c, $form = '') { - if (!function_exists('calculer_cle_action')) { - include_spip('inc/securiser_action'); - } - $cache_contextes_ajax = (!defined('_CACHE_CONTEXTES_AJAX') || _CACHE_CONTEXTES_AJAX); - // si $cache_contextes_ajax on accepte que des md5(contexte) et on doit trouver un fichier sur le disque avec ce md5 - if ($cache_contextes_ajax || strlen($c) == 32) { - if (!is_dir($dir = _DIR_CACHE . 'contextes')) { - return false; - } - if (!file_exists("$dir/c$c")) { - return false; - } - $contexte = file_get_contents("$dir/c$c"); - if (!$contexte) { - return false; - } - $c = $contexte; - } else { - // uniquement si _CACHE_CONTEXTES_AJAX == false - $c = @base64_decode($c); - $c = _xor($c); - if (function_exists('gzdeflate') && function_exists('gzinflate')) { - $c = @gzinflate($c); - } - } - // extraire la signature en debut de contexte - // et la verifier avant de deserializer - // format : signature:donneesserializees - if ($p = strpos($c, ':')) { - $cle = substr($c, 0, $p); - $c = substr($c, $p + 1); - - if ($cle == calculer_cle_action($form . $c)) { - $env = @unserialize($c); - return $env; - } + $ajaxContext = new \Spip\Calcul\AjaxContext(); + if ($form) { + return $ajaxContext->decodeFormContext($c, $form, !empty($GLOBALS['visiteur_session']['id_auteur'])) ?? false; } - - return false; + return $ajaxContext->decodeIncludeContext($c, ['fond', 'connect']) ?? false; } /** @@ -4323,7 +4218,7 @@ // Liens : préparer les classes ajax $ajax = ''; if ($type === 'lien') { - if (str_contains($class_lien, 'ajax')) { + if (str_contains($class_lien, 'ajax') && !str_contains($class_lien, 'noajax')) { $ajax = 'ajax'; if (str_contains($class_lien, 'preload')) { $ajax .= ' preload'; diff -Nru spip-4.4.28+dfsg/ecrire/inc/genie.php spip-4.4.30+dfsg/ecrire/inc/genie.php --- spip-4.4.28+dfsg/ecrire/inc/genie.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/genie.php 2026-11-05 13:48:46.000000000 +0000 @@ -114,6 +114,9 @@ // Optimisation de la base $taches_generales['optimiser'] = 3600 * 48; + // Purge des contextes ajax en cache + $taches_generales['optimiser_contextes'] = 3600; + // nouveautes if ( isset($GLOBALS['meta']['adresse_neuf']) diff -Nru spip-4.4.28+dfsg/ecrire/inc/headers.php spip-4.4.30+dfsg/ecrire/inc/headers.php --- spip-4.4.28+dfsg/ecrire/inc/headers.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/headers.php 2026-11-05 13:48:46.000000000 +0000 @@ -131,6 +131,20 @@ '', ]; } else { + // jamais de pseudo-protocoles + if (preg_match(',^(javascript|data|mailto):,i', trim($url))) { + return ''; + } + // est-ce que l'url vient de la QS ? + // dans ce cas on accepte que des urls sécurisées + foreach ($_GET as $value) { + if ($value === $url) { + if (!securiser_redirect_action($url, false)) { + return ''; + } + break; + } + } include_spip('inc/filtres'); // ne pas laisser passer n'importe quoi dans l'url $url = str_replace(['<', '"'], ['<', '"'], $url); diff -Nru spip-4.4.28+dfsg/ecrire/inc/meta.php spip-4.4.30+dfsg/ecrire/inc/meta.php --- spip-4.4.28+dfsg/ecrire/inc/meta.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/meta.php 2026-11-05 13:48:46.000000000 +0000 @@ -52,7 +52,7 @@ if ( (test_espace_prive() || isset($_GET['renouvelle_alea'])) && $GLOBALS[$table] - && time() > _RENOUVELLE_ALEA + ($GLOBALS['meta']['alea_ephemere_date'] ?? 0) + && time() > _RENOUVELLE_ALEA + ($GLOBALS['meta']['date_alea_ephemere'] ?? 0) ) { // si on n'a pas l'acces en ecriture sur le cache, // ne pas renouveller l'alea sinon le cache devient faux @@ -122,10 +122,13 @@ if (!$antidate || !@touch($file, $antidate)) { $r = $GLOBALS[$table] ?? []; if ($table == 'meta') { - unset($r['alea_ephemere']); - unset($r['alea_ephemere_ancien']); - unset($r['secret_du_site']); - unset($r['secret_des_actions']); + $secret_keys = array_filter( + array_keys($r), + fn ($k) => str_starts_with($k, 'secret_') || str_starts_with($k, 'alea_') + ); + foreach ($secret_keys as $k) { + unset($r[$k]); + } if ($antidate) { $r['touch'] = $antidate; } diff -Nru spip-4.4.28+dfsg/ecrire/inc/queue.php spip-4.4.30+dfsg/ecrire/inc/queue.php --- spip-4.4.28+dfsg/ecrire/inc/queue.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/queue.php 2026-11-05 13:48:46.000000000 +0000 @@ -75,7 +75,7 @@ $set_job = [ 'fonction' => $function, 'descriptif' => $description, - 'args' => $arguments, + 'args' => base64_encode($arguments), 'md5args' => $md5args, 'inclure' => $file, 'priorite' => max(-10, min(10, intval($priority))), @@ -261,7 +261,13 @@ } // deserialiser les arguments - $args = unserialize($row['args']); + $args = $row['args']; + if (!str_contains($args, ':')) { + $args = base64_decode($args, true); + } + if ($args !== false) { + $args = unserialize($args); + } if (!is_array($args)) { spip_log('arguments job errones ' . var_export($row, true), 'queue.' . _LOG_ERREUR); return false; diff -Nru spip-4.4.28+dfsg/ecrire/inc/texte_mini.php spip-4.4.30+dfsg/ecrire/inc/texte_mini.php --- spip-4.4.28+dfsg/ecrire/inc/texte_mini.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/texte_mini.php 2026-11-05 13:48:46.000000000 +0000 @@ -10,6 +10,7 @@ */ use Spip\Texte\Collecteur\HtmlPhp as CollecteurPhp; +use Spip\Texte\Collecteur\HtmlTag as CollecteurHtmlTag; /** * Gestion des textes et échappements (fonctions d'usages fréquents) @@ -93,46 +94,24 @@ // XHTML - Preserver les balises-bloc : on liste ici tous les elements // dont on souhaite qu'ils provoquent un saut de paragraphe +defined('_BALISES_BLOCS') || define('_BALISES_BLOCS', implode('|', CollecteurHtmlTag::$listeBalisesBloc)); +defined('_BALISES_BLOCS_REGEXP') || define('_BALISES_BLOCS_REGEXP', ',[:space:]],iS'); -if (!defined('_BALISES_BLOCS')) { - define( - '_BALISES_BLOCS', - 'address|applet|article|aside|blockquote|button|center|d[ltd]|div|fieldset|fig(ure|caption)|footer|form|h[1-6r]|hgroup|head|header|iframe|li|map|marquee|nav|noscript|object|ol|pre|section|t(able|[rdh]|body|foot|extarea)|ul|script|style' - ); -} - -if (!defined('_BALISES_BLOCS_REGEXP')) { - define('_BALISES_BLOCS_REGEXP', ',[:space:]],iS'); -} - -// -// Echapper les elements perilleux en les passant en base64 -// - -// Creer un bloc base64 correspondant a $rempl ; au besoin en marquant -// une $source differente ; le script detecte automagiquement si ce qu'on -// echappe est un div ou un span +/** + * Echapper les elements perilleux en les passant en base64 + * + * Creer un bloc base64 correspondant a $rempl ; au besoin en marquant + * une $source differente ; le script detecte automagiquement si ce qu'on + * echappe est un div ou un span + * + * @return string + */ function code_echappement($rempl, $source = '', $no_transform = false, $mode = null) { - if (!strlen($rempl)) { + if (!is_string($rempl) or !strlen($rempl)) { return ''; } - // Tester si on echappe en span ou en div - if ($mode === null || !in_array($mode, ['div', 'span'])) { - $mode = preg_match(',[:space:]],iS', $rempl) ? 'div' : 'span'; - } - - // Decouper en morceaux, base64 a des probleme selon la taille de la pile - $taille = 30000; - $return = ''; - for ($i = 0; $i < strlen($rempl); $i += $taille) { - // Convertir en base64 et cacher dans un attribut - // utiliser les " pour eviter le re-encodage de ' et ’ - $base64 = base64_encode(substr($rempl, $i, $taille)); - $return .= "<$mode class=\"base64$source\" title=\"$base64\">"; - } - - return $return; + return CollecteurHtmlTag::echappementHtmlBase64((string) $rempl, (string) $source, in_array($mode, ['div', 'span']) ? $mode === 'div' : null); } // Echapper les ... @@ -257,7 +236,7 @@ } $p = strpos($letexte, (string) $regs[0]); - $letexte = substr_replace($letexte, code_echappement($echap, $source, $no_transform), $p, strlen($regs[0])); + $letexte = substr_replace($letexte, CollecteurHtmlTag::echappementHtmlBase64($echap, $source), $p, strlen($regs[0])); } } @@ -274,7 +253,7 @@ && str_contains($letexte, 'remplacer($letexte, fn ($raw) => code_echappement(highlight_string($raw, true), $source)); + $letexte = $collecteurPhp->remplacer($letexte, fn ($raw) => CollecteurHtmlTag::echappementHtmlBase64(highlight_string($raw, true), $source)); } return $letexte; @@ -311,49 +290,19 @@ * @return array|mixed|string|string[] */ function echappe_retour($letexte, $source = '', $filtre = '') { - if (strpos($letexte, (string) "base64$source")) { - # spip_log(spip_htmlspecialchars($letexte)); ## pour les curieux - $max_prof = 5; - while ( - str_contains($letexte, '<') - && preg_match_all( - ',<(span|div)\sclass=[\'"]base64' . $source . '[\'"]\s(.*)>\s*,UmsS', - $letexte, - $regs, - PREG_SET_ORDER - ) - && $max_prof-- - ) { - foreach ($regs as $reg) { - $rempl = base64_decode(extraire_attribut($reg[0], 'title')); - // recherche d'attributs supplementaires - $at = []; - foreach (['lang', 'dir'] as $attr) { - if ($a = extraire_attribut($reg[0], $attr)) { - $at[$attr] = $a; - } - } - if ($at) { - $rempl = '<' . $reg[1] . '>' . $rempl . ''; - foreach ($at as $attr => $a) { - $rempl = inserer_attribut($rempl, $attr, $a); - } - } - if ($filtre) { - $rempl = $filtre($rempl); - } - $letexte = str_replace($reg[0], $rempl, $letexte); - } - } + if (!is_string($letexte) or !strlen($letexte)) { + return $letexte; } - - return $letexte; + return CollecteurHtmlTag::retablir_depuisHtmlBase64((string) $letexte, (string) $source, (string) $filtre); } // Reinserer le javascript de confiance (venant des modeles) function echappe_retour_modeles($letexte, $interdire_scripts = false) { - $letexte = echappe_retour($letexte); + if (!is_string($letexte) or !strlen($letexte)) { + return $letexte; + } + $letexte = CollecteurHtmlTag::retablir_depuisHtmlBase64((string) $letexte); // Dans les appels directs hors squelette, sécuriser aussi ici if ($interdire_scripts) { @@ -473,13 +422,14 @@ function protege_js_modeles($texte) { if (isset($GLOBALS['visiteur_session'])) { - if (preg_match_all(',remplacer($texte, fn ($raw) => code_echappement($raw, 'php' . _PROTEGE_PHP_MODELES)); + $texte = $collecteurPhp->remplacer($texte, fn ($raw) => CollecteurHtmlTag::echappementHtmlBase64($raw, 'php' . _PROTEGE_PHP_MODELES)); } } } diff -Nru spip-4.4.28+dfsg/ecrire/inc/utils.php spip-4.4.30+dfsg/ecrire/inc/utils.php --- spip-4.4.28+dfsg/ecrire/inc/utils.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc/utils.php 2026-11-05 13:48:46.000000000 +0000 @@ -552,8 +552,10 @@ if (\is_array($value)) { return spip_sanitize_value_recursive($value, $sanitize_function); } - // si valeur réécrite, on considère safe - if (_request($key) !== $value) { + // si valeur ni dans $_GET ni dans $_POST, on considère safe + if (_request($key) !== $value + && ($_GET[$key] ?? '') !== $value + && ($_POST[$key] ?? '') !== $value) { return $value; } return $sanitize_function($value); @@ -2887,7 +2889,7 @@ # if (!defined('_IMG_MAX_HEIGHT')) define('_IMG_MAX_HEIGHT', 0); # hauteur en pixels if (!defined('_PASS_LONGUEUR_MINI')) { - define('_PASS_LONGUEUR_MINI', 6); + define('_PASS_LONGUEUR_MINI', 12); } // largeur maximale des images dans l'administration @@ -3501,20 +3503,15 @@ 'args' => ['fond' => $f, 'contexte' => $contexte, 'options' => $options, 'connect' => $connect], 'data' => $page, ]); - if (isset($options['ajax']) && $options['ajax']) { - if (!function_exists('encoder_contexte_ajax')) { - include_spip('inc/filtres'); - } - $page['texte'] = encoder_contexte_ajax( - array_merge( + // pas de bloc ajax pour les robots : ils n'executent pas le JS, et leurs pages ne sont pas mises en cache + if (!empty($options['ajax']) && !_IS_BOT) { + $ajaxContext = new \Spip\Calcul\AjaxContext(); + $page['texte'] = $ajaxContext + ->encodeIncludeContext( $contexte, - ['fond' => $f], - ($connect ? ['connect' => $connect] : []) - ), - '', - $page['texte'], - $options['ajax'] - ); + array_merge(['fond' => $f], ($connect ? ['connect' => $connect] : [])) + ) + ->emboiteAjaxEnv($page['texte'], self(), $options['ajax']); } if (isset($options['raw']) && $options['raw']) { diff -Nru spip-4.4.28+dfsg/ecrire/inc_version.php spip-4.4.30+dfsg/ecrire/inc_version.php --- spip-4.4.28+dfsg/ecrire/inc_version.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/inc_version.php 2026-11-05 13:48:46.000000000 +0000 @@ -449,8 +449,8 @@ // pour specifier les versions de SPIP necessaires // il faut s'en tenir a un nombre de decimales fixe // ex : 2.0.0, 2.0.0-dev, 2.0.0-beta, 2.0.0-beta2 -$spip_version_branche = '4.4.28'; -define('_SPIP_VERSION_ID', 40428); +$spip_version_branche = '4.4.30'; +define('_SPIP_VERSION_ID', 40430); define('_SPIP_EXTRA_VERSION', ''); // cette version dev accepte tous les plugins compatible avec la version ci-dessous @@ -458,7 +458,7 @@ # define('_DEV_VERSION_SPIP_COMPAT', '4.1.99'); // version des signatures de fonctions PHP // (= date de leur derniere modif cassant la compatibilite et/ou necessitant un recalcul des squelettes) -$spip_version_code = 2026_10_06; +$spip_version_code = 2026_11_04; // version de la base SQL (= Date + numero incremental a 2 chiffres YYYYMMDDXX) $spip_version_base = 2026_09_24_00; diff -Nru spip-4.4.28+dfsg/ecrire/paquet.xml spip-4.4.30+dfsg/ecrire/paquet.xml --- spip-4.4.28+dfsg/ecrire/paquet.xml 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/paquet.xml 2026-11-05 13:48:46.000000000 +0000 @@ -1,7 +1,7 @@ decodeFormContext($args, $form, !empty($GLOBALS['visiteur_session']['id_auteur'])) !== null ) { // on est bien dans le contexte de traitement d'un formulaire en ajax // mais traiter ne veut pas @@ -190,9 +191,10 @@ && $v !== 'form' && ($args = _request('var_ajax_env')) ) { - include_spip('inc/filtres'); include_spip('inc/actions'); - if (!($args = decoder_contexte_ajax($args)) + $ajaxContext = new \Spip\Calcul\AjaxContext(); + $args = $ajaxContext->decodeIncludeContext($args, ['fond', 'connect']); + if ($args === null || !is_array($args) || empty($args['fond'])) { include_spip('inc/headers'); @@ -279,8 +281,9 @@ } } - include_spip('inc/filtres'); - if (($args = decoder_contexte_ajax($args, $form)) === false) { + $ajaxContext = new \Spip\Calcul\AjaxContext(); + $args = $ajaxContext->decodeFormContext($args, $form, !empty($GLOBALS['visiteur_session']['id_auteur'])); + if ($args === null) { spip_log("signature ajax form incorrecte : $form (encodage corrompu)", 'formulaires' . _LOG_ERREUR); return false; // continuons le hit comme si de rien etait diff -Nru spip-4.4.28+dfsg/ecrire/public/assembler.php spip-4.4.30+dfsg/ecrire/public/assembler.php --- spip-4.4.28+dfsg/ecrire/public/assembler.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/public/assembler.php 2026-11-05 13:48:46.000000000 +0000 @@ -199,8 +199,11 @@ * lors du calcul d'une page spip etablit le contexte a partir * des variables $_GET et $_POST, purgees des fausses variables var_* * + * Les valeurs de $_GET écrasent les valeurs de $_POST avec une clé identique, + * comme pour _request() + * * Note : pour hacker le contexte depuis le fichier d'appel (page.php), - * il est recommande de modifier $_GET['toto'] (meme si la page est + * il est recommandé de modifier $_GET['toto'] (meme si la page est * appelee avec la methode POST). * * @return array Un tableau du contexte de la page @@ -208,12 +211,12 @@ function calculer_contexte() { $contexte = []; - foreach ($_GET as $var => $val) { + foreach ($_POST as $var => $val) { if (!preg_match(_CONTEXTE_IGNORE_VARIABLES, $var)) { $contexte[$var] = $val; } } - foreach ($_POST as $var => $val) { + foreach ($_GET as $var => $val) { if (!preg_match(_CONTEXTE_IGNORE_VARIABLES, $var)) { $contexte[$var] = $val; } @@ -698,17 +701,17 @@ $compteur--; - return (isset($arg_list['ajax']) && $arg_list['ajax'] === 'ajax') - ? encoder_contexte_ajax( - array_merge( + if (isset($arg_list['ajax']) && $arg_list['ajax'] === 'ajax') { + $ajaxContext = new \Spip\Calcul\AjaxContext(); + $retour = $ajaxContext + ->encodeIncludeContext( $contexte, - ['fond' => $fond], - ($connect ? ['connect' => $connect] : []) - ), - '', - $retour - ) - : $retour; + array_merge(['fond' => $fond], ($connect ? ['connect' => $connect] : [])) + ) + ->emboiteAjaxEnv($retour, self()); + } + + return $retour; } /** diff -Nru spip-4.4.28+dfsg/ecrire/public/cacher.php spip-4.4.30+dfsg/ecrire/public/cacher.php --- spip-4.4.28+dfsg/ecrire/public/cacher.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/public/cacher.php 2026-11-05 13:48:46.000000000 +0000 @@ -52,11 +52,20 @@ $d = substr($nom_cache, 0, $l1); $u = substr($nom_cache, $l1, $l2); + $rep = ''; if ($ecrire) { + // si on est en train de vider le cache il se peut qu'on n'arrive pas à creer les répertoires + // dans ce cas on veut renvoyer le bon chemin même si pas créé + // et l'écriture echouera $rep = sous_repertoire(_DIR_CACHE, '', false, true); - $rep = sous_repertoire($rep, 'calcul/', false, true); - $rep = sous_repertoire($rep, $d, false, true); - } else { + if ($rep) { + $rep = sous_repertoire($rep, 'calcul/', false, true); + } + if ($rep) { + $rep = sous_repertoire($rep, $d, false, true); + } + } + if (!$rep) { // en lecture on essaye pas de creer les repertoires, on va au plus vite $rep = _DIR_CACHE . "calcul/$d/"; } diff -Nru spip-4.4.28+dfsg/ecrire/public/criteres.php spip-4.4.30+dfsg/ecrire/public/criteres.php --- spip-4.4.28+dfsg/ecrire/public/criteres.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/public/criteres.php 2026-11-05 13:48:46.000000000 +0000 @@ -868,16 +868,24 @@ * @return string|array Clause pour le Order by (array si erreur) */ function calculer_critere_par_expression_num($idb, &$boucles, $crit, $tri, $champ) { - $_champ = calculer_critere_par_champ($idb, $boucles, $crit, $champ, true); - if (is_array($_champ)) { - return ['zbug_critere_inconnu', ['critere' => $crit->op . " num $champ"]]; - } $boucle = &$boucles[$idb]; - $texte = '0+' . $_champ; - $suite = calculer_liste($tri, $idb, $boucles, $boucle->id_parent); - if ($suite !== "''") { - $texte = "\" . ((\$x = $suite) ? ('$texte' . \$x) : '0')" . ' . "'; + + if (!$champ || (!empty($tri) && $tri[0]->type !== 'texte')) { + // Conserver la validation du champ et la conversion numérique ; un champ vide donne 0. + $champ_dynamique = calculer_critere_arg_dynamique($idb, $boucles, $tri, ''); + $texte = '" . (($x = ' . $champ_dynamique . ') ? ("0+" . $x) : "0") . "'; + } else { + $_champ = calculer_critere_par_champ($idb, $boucles, $crit, $champ, true); + if (is_array($_champ)) { + return ['zbug_critere_inconnu', ['critere' => $crit->op . " num $champ"]]; + } + $texte = '0+' . $_champ; + $suite = calculer_liste($tri, $idb, $boucles, $boucle->id_parent); + if ($suite !== "''") { + $texte = "\" . ((\$x = preg_replace(\"/\\W/\",'', $suite)) ? ('$texte' . \$x) : '0')" . ' . "'; + } } + $asnum = 'num' . ($boucle->order ? count($boucle->order) : ''); $boucle->select[] = $texte . " AS $asnum"; @@ -892,8 +900,8 @@ * Calculs pour le critère `{par sinum champ}` qui ordonne les champs avec numéros en premier * * Ajoute au SELECT la valeur 'sinum' qui vaut 0 si le champ a un numéro, 1 s'il n'en a pas. - * Ainsi `{par sinum titre, num titre, titre}` mettra les éléments sans numéro en fin de liste, - * contrairement à `{par num titre, titre}` seulement. + * Ainsi `{par sinum titre, num titre, titre}` mettra les éléments sans numéro en fin de liste. + * Note : depuis b413642a0, `{par num champ}` intègre déjà `sinum`. * * @see calculer_critere_par_expression_num() pour le critère `{par num champ}` * @uses calculer_critere_par_champ() @@ -906,15 +914,22 @@ * @return string|array Clause pour le Order by (array si erreur) */ function calculer_critere_par_expression_sinum($idb, &$boucles, $crit, $tri, $champ) { - $_champ = calculer_critere_par_champ($idb, $boucles, $crit, $champ, true); - if (is_array($_champ)) { - return ['zbug_critere_inconnu', ['critere' => $crit->op . " sinum $champ"]]; - } $boucle = &$boucles[$idb]; - $texte = '0+' . $_champ; - $suite = calculer_liste($tri, $idb, $boucles, $boucle->id_parent); - if ($suite !== "''") { - $texte = "\" . ((\$x = $suite) ? ('$texte' . \$x) : '0')" . ' . "'; + + if (!$champ || (!empty($tri) && $tri[0]->type !== 'texte')) { + // Conserver la validation du champ et la conversion numérique ; un champ vide donne 0. + $champ_dynamique = calculer_critere_arg_dynamique($idb, $boucles, $tri, ''); + $texte = '" . (($x = ' . $champ_dynamique . ') ? ("0+" . $x) : "0") . "'; + } else { + $_champ = calculer_critere_par_champ($idb, $boucles, $crit, $champ, true); + if (is_array($_champ)) { + return ['zbug_critere_inconnu', ['critere' => $crit->op . " sinum $champ"]]; + } + $texte = '0+' . $_champ; + $suite = calculer_liste($tri, $idb, $boucles, $boucle->id_parent); + if ($suite !== "''") { + $texte = "\" . ((\$x = preg_replace(\"/\\W/\",'', $suite)) ? ('$texte' . \$x) : '0')" . ' . "'; + } } $as = false; @@ -953,12 +968,19 @@ * @return string|array Clause pour le Order by (array si erreur) */ function calculer_critere_par_expression_multi($idb, &$boucles, $crit, $tri, $champ) { - $_champ = calculer_critere_par_champ($idb, $boucles, $crit, $champ, true); - if (is_array($_champ)) { - return ['zbug_critere_inconnu', ['critere' => $crit->op . " multi $champ"]]; - } $boucle = &$boucles[$idb]; - $boucle->select[] = "\".sql_multi('" . $_champ . "', \$GLOBALS['spip_lang']).\""; + if (!$champ || (!empty($tri) && $tri[0]->type !== 'texte')) { + // calculer le order dynamique qui verifie les champs + $texte = calculer_critere_arg_dynamique($idb, $boucles, $tri, ''); + } else { + $_champ = calculer_critere_par_champ($idb, $boucles, $crit, $champ, true); + if (is_array($_champ)) { + return ['zbug_critere_inconnu', ['critere' => $crit->op . " multi $champ"]]; + } + $texte = "'" . $_champ . "'"; + } + $boucle->select[] = "\".sql_multi($texte, \$GLOBALS['spip_lang']).\""; + $order = "'multi'"; return $order; } diff -Nru spip-4.4.28+dfsg/ecrire/public/interfaces.php spip-4.4.30+dfsg/ecrire/public/interfaces.php --- spip-4.4.28+dfsg/ecrire/public/interfaces.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/public/interfaces.php 2026-11-05 13:48:46.000000000 +0000 @@ -107,7 +107,7 @@ $GLOBALS['table_des_traitements']['BIO'][] = 'safehtml(' . declarer_traitement_raccourcis('bio') . ')'; $GLOBALS['table_des_traitements']['NOM_SITE']['auteurs'] = 'entites_html(%s)'; - $GLOBALS['table_des_traitements']['NOM']['auteurs'] = 'safehtml(' . declarer_traitement_typo('nom') . ')'; + $GLOBALS['table_des_traitements']['NOM']['auteurs'] = 'safehtml(' . declarer_traitement_typo_sans_numero('nom') . ')'; $GLOBALS['table_des_traitements']['CHAPO'][] = declarer_traitement_raccourcis(); $GLOBALS['table_des_traitements']['DATE'][] = 'normaliser_date(%s)'; $GLOBALS['table_des_traitements']['DATE_REDAC'][] = 'normaliser_date(%s)'; diff -Nru spip-4.4.28+dfsg/ecrire/public.php spip-4.4.30+dfsg/ecrire/public.php --- spip-4.4.28+dfsg/ecrire/public.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/public.php 2026-11-05 13:48:46.000000000 +0000 @@ -35,17 +35,23 @@ $fond = (string) $_GET[_SPIP_PAGE]; // Securite - if ( - (str_contains($fond, '/') - && !( - isset($GLOBALS['visiteur_session']) - && include_spip('inc/autoriser') - && autoriser('webmestre') - )) || str_contains($fond, 'formulaires/') - ) { - $minipage = new MinipageAdmin(); - echo $minipage->page(); - exit; + if (str_contains($fond, '/')) { + if ( + !( + isset($GLOBALS['visiteur_session']) + && include_spip('inc/autoriser') + && autoriser('webmestre') + ) + || str_contains($fond, 'formulaires/') + ) { + $minipage = new MinipageAdmin(); + echo $minipage->page(); + exit; + } + // pas de html dans les arguments + foreach ($_GET as $k => $v) { + $_GET[$k] = is_array($v) ? spip_sanitize_value_recursive($v, 'htmlspecialchars') : htmlspecialchars($v); + } } // l'argument Page a priorite sur l'argument action // le cas se presente a cause des RewriteRule d'Apache diff -Nru spip-4.4.28+dfsg/ecrire/src/Calcul/AjaxContext.php spip-4.4.30+dfsg/ecrire/src/Calcul/AjaxContext.php --- spip-4.4.28+dfsg/ecrire/src/Calcul/AjaxContext.php 1970-01-01 00:00:00.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/src/Calcul/AjaxContext.php 2026-11-05 13:48:46.000000000 +0000 @@ -0,0 +1,400 @@ +forceStoredContext = true; + } + if (defined('_MEMOIZE_CONTEXTES_AJAX') + && _MEMOIZE_CONTEXTES_AJAX === true + && function_exists('cache_get') + && function_exists('cache_set') + ) { + $this->preferMemoizationStorage = true; + } + $this->contexteMaxLength = defined('_CACHE_CONTEXTES_AJAX_SUR_LONGUEUR') ? _CACHE_CONTEXTES_AJAX_SUR_LONGUEUR : 0; + } + + public function encodeIncludeContext(array $context, array $sensitiveContext = []): self { + if (\detecter_contexte_avec_balise_php($context)) { + \spip_log('Contexte ajax avec balise PHP, ignoré', 'ajax' . _LOG_DEBUG); + return $this; + } + + // supprimer les parametres debut_x + // pour que la pagination ajax ne soit pas plantee + // si on charge la page &debut_x=1 : car alors en cliquant sur l'item 0, + // le debut_x=0 n'existe pas, et on resterait sur 1 + // on supprime aussi les clés qui sont dans $sensitiveContext + foreach ($context as $k => $v) { + if (str_starts_with($k, 'debut_') + || array_key_exists($k, $sensitiveContext)) { + unset($context[$k]); + } + } + + // le contexte ajax ne doit pas reprendre les variables $_POSTees par un formulaire + // on ne rejoue jamais un $_POST par un $_GET + // sauf si on est dans une inclusion ajax du formulaire posté + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $is_in_posted_form = + !empty($context['form']) + && array_key_exists('message_ok', $context) + && array_key_exists('message_erreur', $context) + && !empty($context['_pipelines']['formulaire_fond']['je_suis_poste']); + if (!$is_in_posted_form) { + foreach (array_keys($_POST) as $k) { + if (array_key_exists($k, $context) && !array_key_exists($k, $_GET)) { + unset($context[$k]); + } + } + } + } + + $forceStoredContext = true; + if (!$this->forceStoredContext) { + $forceStoredContext = false; + // le contexte disque pour ce qui est sensible + $env = ''; + if (!empty($sensitiveContext)) { + foreach ($sensitiveContext as $k => $v) { + if (array_key_exists($k, $context)) { + unset($context[$k]); + } + } + + $env = $this->encodeContextToStorage($sensitiveContext, 'include:sensitive'); + $env .= '|'; + } + + if (!empty($context)) { + // le reste en base64 + $env .= $this->encodeContextToBase64($context, 'include:context'); + } + + $len = strlen($env); + if ($this->contexteMaxLength && $len > $this->contexteMaxLength) { + $forceStoredContext = true; + \spip_log( + 'Contextes AJAX forces en fichiers !' + . ' Cela arrive lorsque la valeur du contexte' + . ' depasse la longueur maximale autorisee (' . $this->contexteMaxLength . "). Ici : $len.", + _LOG_AVERTISSEMENT + ); + } + } + + if ($forceStoredContext) { + $context = array_merge($context, $sensitiveContext); + $env = $this->encodeContextToStorage($context, 'include:sensitive', $this->dateVars); + $dateContext = array_filter($context, fn ($k) => in_array($k, $this->dateVars), ARRAY_FILTER_USE_KEY); + if (!empty($dateContext)) { + $dateEnv = '|' . $this->encodeContextToBase64($dateContext, 'include:context'); + $len = strlen($env) + strlen($dateEnv); + if (!$this->contexteMaxLength || $len < $this->contexteMaxLength) { + $env .= $dateEnv; + } + } + } + + $this->env = $env; + return $this; + } + + public function decodeIncludeContext(string $env, array $sensitiveKeys = []): ?array { + $sensitiveContext = null; + $sensitiveEnv = null; + if (\str_contains($env, '|')) { + [$sensitiveEnv, $env] = explode('|', $env, 2); + } elseif ($this->isMd5($env)) { + $sensitiveEnv = $env; + $env = null; + } + + if ($sensitiveEnv !== null) { + $sensitiveContext = $this->decodeContextFromStorage($sensitiveEnv, 'include:sensitive'); + if ($sensitiveContext === null) { + return null; + } + if ($env === '' || $env === null) { + return $sensitiveContext; + } + } + + $context = $this->decodeContextFromBase64($env, 'include:context'); + if ($context === null) { + return null; + } + // pas de $sensitiveKeys dans le context qui vient de base64 + if (!empty($sensitiveKeys)) { + foreach ($sensitiveKeys as $k => $v) { + if (array_key_exists($k, $context)) { + unset($context[$k]); + } + } + } + + return $sensitiveContext === null ? $context : array_merge($sensitiveContext, $context); + } + + public function encodeFormContext(array $context, string $formName, bool $secureEnv = false): self { + if (!$secureEnv) { + $this->env = $this->encodeContextToBase64($context, "form:$formName"); + } else { + $this->env = $this->encodeContextToStorage($context, "form:$formName"); + } + return $this; + } + + public function decodeFormContext(string $env, string $formName, bool $secureEnv = false): ?array { + // au decodage, on accepte un contexte depuis le disque meme si on avait pas demandé secureEnv + if (!$secureEnv) { + $c = $this->decodeContextFromBase64($env, "form:$formName"); + if ($c !== null) { + return $c; + } + } + + return $this->decodeContextFromStorage($env, "form:$formName"); + } + + public function encodeVarValue($value, string $keyName): self { + $this->env = $this->encodeContextToStorage(['value' => $value], "var:$keyName"); + return $this; + } + + public function decodeVarValue(string $env, string $keyName) { + $res = $this->decodeContextFromStorage($env, "var:$keyName"); + if (is_array($res) && count($res) === 1 && array_key_exists('value', $res)) { + return $res['value']; + } + // throw Exception ? + return null; + } + + /** + * On peut supprimer le stockage d'une valeur qui serait sensible + * et qu'on ne veut pas garder inutilement sur disque/memoire + */ + public function deleteVarValue(string $env): bool { + return $this->removeContextFromStorage($env); + } + + public function emboiteAjaxEnv(string $html, string $origin, ?string $ajaxId = null): string { + if (!strlen(trim($html))) { + return ''; + } + if (!isset($this->env)) { + return $html; + } + + $class = 'ajaxbloc'; + if ($ajaxId !== null) { + // ajaxid est normalement conforme a un nom de classe css + // on ne verifie pas la conformite, mais on passe attribut_html par dessus par precaution + $class .= ' ajax-id-' . $ajaxId; + } + + // toujours encoder l'url source dans le bloc ajax + return '
' . "\n" + . $html + . "
\n"; + } + + public function getEnv(): string { + return $this->env ?? ''; + } + + private function contextFileName(string $md5, $mkdir = false): ?string { + $subdir = \substr($md5, 0, 1); + $fichier = _DIR_CACHE . 'contextes/' . $subdir . '/' . $md5; + if ($mkdir) { + $dir = \sous_repertoire(_DIR_CACHE, 'contextes', false, true); + if (!$dir) { + return null; + } + $dir = \sous_repertoire($dir, $subdir, false, true); + if (!$dir) { + return null; + } + } + + return $fichier; + } + + private function encodeContextToStorage(array $c, string $signKey, array $ignoreVarsInHash = []): string { + // les dates par defaut (ajoutees par parametrer) changent a chaque calcul : + // on les exclut du nom du fichier de cache, mais on les conserve dans son contenu + // pour que le hit ajax retrouve les caches d'inclusions calcules avec cette date + $c_hash = $c; + foreach ($ignoreVarsInHash as $key) { + if (\array_key_exists($key, $c_hash)) { + unset($c_hash[$key]); + } + } + $c_hash = $signKey . \serialize($c_hash); + + $env = $this->serializeAndSign($c, $signKey); + + // stocker les contextes et ne passer qu'un hash dans l'url + // contenu en base64 pour ne pas declencher les detecteurs de malware des hebergeurs quand on est sur le disque + $md5 = \md5($c_hash); + if ($this->preferMemoizationStorage) { + cache_set("contexte:{$md5}", \base64_encode($env), 48 * 3600); + } else { + $fichier = $this->contextFileName($md5, true); + if ($fichier) { + \file_put_contents($fichier, \base64_encode($env)); + } + } + return $md5; + } + + private function decodeContextFromStorage(string $env, string $signKey): ?array { + if (!$this->isMd5($env)) { + return null; + } + if ($this->preferMemoizationStorage) { + $contexte = cache_get("contexte:{$env}"); + } else { + $fichier = $this->contextFileName($env); + if (!$fichier || !\file_exists($fichier)) { + return null; + } + $contexte = file_get_contents($fichier); + } + + if (!$contexte || !($contexte = \base64_decode($contexte, true))) { + return null; + } + return $this->checkAndDeserialize($contexte, $signKey); + } + + private function removeContextFromStorage(string $env): ?bool { + if (!$this->isMd5($env)) { + return null; + } + if ($this->preferMemoizationStorage) { + return cache_del("contexte:{$env}"); + } else { + $fichier = $this->contextFileName($env); + if (\file_exists($fichier)) { + return \unlink($fichier); + } + return true; + } + } + + private function serializeAndSign(array $context, string $signKey): string { + $c = \serialize($context); + $cle = \calculer_cle_action($signKey . $c); + return "$cle:$c"; + } + + private function checkAndDeserialize(string $serializedContext, string $signKey): ?array { + // extraire la signature en debut de contexte + // et la verifier avant de deserializer + // format : signature:donneesserializees + if ($p = strpos($serializedContext, ':')) { + $cle = substr($serializedContext, 0, $p); + $serializedContext = substr($serializedContext, $p + 1); + + if (verifier_cle_action($signKey . $serializedContext, $cle)) { + $env = @unserialize($serializedContext); + return is_array($env) ? $env : null; + } + } + return null; + } + + private function isMd5(string $env) { + $est_md5 = (strlen($env) === 32) && (bool) preg_match('/^[0-9a-f]{32}$/', $env); + return $est_md5; + } + + private function encodeContextToBase64(array $c, string $signKey): string { + $env = $this->serializeAndSign($c, $signKey); + + if (function_exists('gzdeflate') && function_exists('gzinflate')) { + $env = \gzdeflate($env); + } + $env = $this->_xor($env); + return base64_encode($env); + } + + private function decodeContextFromBase64(string $env, string $signKey): ?array { + $c = @base64_decode($env); + $c = $this->_xor($c); + if (function_exists('gzdeflate') && function_exists('gzinflate')) { + $c = @\gzinflate($c); + } + + return $this->checkAndDeserialize($c, $signKey); + } + + /** + * Encrypte ou décrypte un message + * + * @see http://www.php.net/manual/fr/language.operators.bitwise.php#81358 + * + * @param string $message + * Message à encrypter ou décrypter + * @return string + * Message décrypté ou encrypté + */ + private function _xor(string $message) { + static $key = null; + if ($key === null) { + $key = pack('H*', \calculer_cle_action('_xor')); + } + $keylen = strlen($key); + $messagelen = strlen($message); + for ($i = 0; $i < $messagelen; $i++) { + $message[$i] = ~($message[$i] ^ $key[$i % $keylen]); + } + + return $message; + } +} diff -Nru spip-4.4.28+dfsg/ecrire/src/Chiffrer/SpipCles.php spip-4.4.30+dfsg/ecrire/src/Chiffrer/SpipCles.php --- spip-4.4.28+dfsg/ecrire/src/Chiffrer/SpipCles.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/src/Chiffrer/SpipCles.php 2026-11-05 13:48:46.000000000 +0000 @@ -60,6 +60,10 @@ return self::instance()->getSecretActions(); } + public static function secret_perso(string $nom, bool $composite = true): ?string { + return self::instance()->getSecretPerso($nom, $composite); + } + /** * Renvoyer le secret du site * @@ -91,6 +95,19 @@ return $this->getKey('secret_des_auth', $autoInit); } + /** + * Renvoyer un secret autre pour un plugin + */ + public function getSecretPerso(string $nom, bool $composite = true, bool $autoInit = true): ?string { + $nom = 'secret_de_' . $nom; + $key = $this->getKey($nom, $autoInit); + if ($composite) { + $meta = $this->getMetaKey($nom, $autoInit); + return $key ^ $meta; + } + return $key; + } + public function save(): bool { return ecrire_fichier_securise($this->file, $this->cles->toJson()); } @@ -178,7 +195,7 @@ } private function getMetaKey(string $name, bool $autoInit = true): ?string { - $cachable_name = $name . '_cache'; + $cachable_name = 'cache_' . $name; if (PHP_SAPI !== 'cli' && !isset($this->meta[$name]) && isset($GLOBALS['meta'][$cachable_name])) { if ($key = $this->getKeyfromCachedValue($GLOBALS['meta'][$cachable_name])) { $this->meta[$name] = base64_encode($key); diff -Nru spip-4.4.28+dfsg/ecrire/src/Texte/Collecteur/AbstractCollecteur.php spip-4.4.30+dfsg/ecrire/src/Texte/Collecteur/AbstractCollecteur.php --- spip-4.4.28+dfsg/ecrire/src/Texte/Collecteur/AbstractCollecteur.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/src/Texte/Collecteur/AbstractCollecteur.php 2026-11-05 13:48:46.000000000 +0000 @@ -21,6 +21,27 @@ protected string $markId; + protected static string $echappeBase64Id; + + public static array $listeBalisesBloc = [ + 'address', 'applet', 'article', 'aside', + 'blockquote', 'button', + 'center', + 'dl', 'dt', 'dd', 'div', + 'fieldset', 'figure', 'figcaption', 'footer', 'form', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'hgroup', 'head', 'header', + 'iframe', + 'li', + 'map', 'marquee', + 'nav', 'noscript', + 'object', 'ol', + 'pre', + 'section', + 'table', 'tr', 'td', 'th', 'tbody', 'foot', 'textarea', + 'ul', + 'script', 'style', + ]; + public function collecter(string $texte, array $options = []): array { return []; } @@ -40,7 +61,7 @@ */ public function remplacer(string $texte, callable $callback, array $options = []): string { $collection = $this->collecter($texte, $options); - return $this->remplacer_collection($texte, $collection, $callback); + return self::remplacer_collection($texte, $collection, $callback); } /** @@ -72,7 +93,7 @@ } $callback = fn ($raw) => $this->markId . base64_encode((string) $raw) . '|@'; - $texte = $this->remplacer_collection($texte, $collection, $callback); + $texte = self::remplacer_collection($texte, $collection, $callback); } return $texte; @@ -84,8 +105,9 @@ * @see echapper() */ public function retablir(string $texte): string { - if (!empty($this->markId)) { + $parts = []; + $parts_position = 0; $lm = strlen($this->markId); $pos = 0; while ( @@ -94,14 +116,17 @@ ) { $base64 = substr($texte, $p + $lm, $end - ($p + $lm)); if ($c = base64_decode($base64, true)) { - $texte = substr_replace($texte, $c, $p, $end + 2 - $p); - $pos = $p + strlen($c); + $parts[] = substr($texte, $parts_position, $p - $parts_position); + $parts[] = $c; + $pos = $parts_position = $end + 2; } else { $pos = $end; } } + if ($parts_position > 0) { + $texte = implode('', $parts) . substr($texte, $parts_position); + } } - return $texte; } @@ -181,16 +206,144 @@ /** * Remplacer les occurrences de la collection d’un texte par un contenu donné en callback + * il est implicite que les occurences sont triées dans l'ordre d'apparition (telles que fournies par le collecteur) * * @note On n’altère pas actuellement le texte (raw) et longeurs (length), du tableau de collecte ; ça pourrait être une option. */ - private function remplacer_collection(string $texte, array $collection, callable $callback): string { - $offset_pos = 0; + private static function remplacer_collection(string $texte, array $collection, callable $callback): string { + $parts = []; + $parts_position = 0; foreach ($collection as $c) { $rempl = $callback($c['raw'], $c); if ($rempl !== null) { - $texte = substr_replace($texte, $rempl, $c['pos'] + $offset_pos, $c['length']); - $offset_pos += \strlen($rempl) - $c['length']; + // chevauchement : pas possible, on a déjà remplacé un morceau de texte sur lequel portait cette occurence + // ça ne doit jamais arriver avec un collecteur, mais on gère le cas ici : on ignore silencieusement ce morceaux + if ($c['pos'] < $parts_position) { + continue; + } + $parts[] = substr($texte, $parts_position, $c['pos'] - $parts_position); + $parts[] = $rempl; + $parts_position = $c['pos'] + $c['length']; + } + } + if ($parts_position > 0) { + $texte = implode('', $parts) . substr($texte, $parts_position); + } + return $texte; + } + + /** + * Detecter si un texte contient des balises bloc ou non + * @return bool + */ + public static function echappementTexteContientBaliseBloc(string $texte) { + static $pregBalisesBloc; + + if ($pregBalisesBloc === null) { + $pregBalisesBloc = ',[:space:]],iS'; + } + return (str_contains($texte, '<') && preg_match($pregBalisesBloc, $texte)) ? true : false; + } + + /** + * Definir un nom de class base64- non predictible, en fonction de la source + * @throws \Random\RandomException + */ + protected static function echappeBase64Class(string $source): string { + if (!isset(static::$echappeBase64Id)) { + static::$echappeBase64Id = bin2hex(random_bytes(8)); + } + return 'base64-' . substr(hash('md5', ($source . ':' . static::$echappeBase64Id)), 0, 8); + } + + /** + * Creer un bloc base64 correspondant a $texte ; au besoin en marquant + * une $source differente ; + * si $isBloc n'est pas fourni, le script detecte automagiquement si ce qu'on + * echappe est un div ou un span + * + * @return string + */ + public static function echappementHtmlBase64(string $texte, string $source = '', ?bool $isBloc = null, array $attributs = []) { + if (empty($texte)) { + return ''; + } + + // Tester si on echappe en span ou en div + $isBloc ??= self::echappementTexteContientBaliseBloc($texte); + $tag = $isBloc ? 'div' : 'span'; + $atts = ''; + if (!empty($attributs)) { + if (!function_exists('attribut_html')) { + include_spip('inc/filtres'); + } + foreach ($attributs as $k => $v) { + $atts .= " $k=\"" . \attribut_html($v) . '"'; + } + } + $base64Class = self::echappeBase64Class($source); + // Decouper en morceaux, base64 a des probleme selon la taille de la pile + $taille = 30000; + $return = ''; + for ($i = 0; $i < strlen($texte); $i += $taille) { + // Convertir en base64 et cacher dans un attribut + // utiliser les " pour eviter le re-encodage de ' et ’ + $base64 = base64_encode(substr($texte, $i, $taille)); + $return .= "<$tag class=\"{$base64Class}\" title=\"$base64\"{$atts}>"; + } + + return $return; + } + + /** + * Rétablir les contenus échappés dans un texte en <(div|span) class="base64..."> + * Rq: $source sert a faire des echappements "a soi" qui ne sont pas nettoyes + * par propre() : exemple dans multi et dans typo() + * + * @return string + * + * @see echappementHtmlBase64() + */ + public static function retablir_depuisHtmlBase64(string $texte, string $source = '', string $filtre = '') { + $base64Class = self::echappeBase64Class($source); + if (str_contains($texte, $base64Class)) { + # spip_log(spip_htmlspecialchars($texte)); ## pour les curieux + $max_prof = 5; + $encore = true; + while ($encore and str_contains($texte, $base64Class) !== false and $max_prof--) { + $encore = false; + foreach (['span', 'div'] as $tag) { + $preg = "@<{$tag}\s(class=['\"]{$base64Class}['\"]\stitle=['\"]([^'\">]*)['\"][^>]*?)(/?)>]*>@isS"; + $collection = self::collecteur($texte, "$rempl"; + foreach ($at as $attr => $a) { + $rempl = inserer_attribut($rempl, $attr, $a); + } + } + if ($filtre) { + $rempl = $filtre($rempl); + } + return $rempl; + } + return null; + } + ); + } } } return $texte; diff -Nru spip-4.4.28+dfsg/ecrire/src/Texte/Collecteur/Idiomes.php spip-4.4.30+dfsg/ecrire/src/Texte/Collecteur/Idiomes.php --- spip-4.4.28+dfsg/ecrire/src/Texte/Collecteur/Idiomes.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/src/Texte/Collecteur/Idiomes.php 2026-11-05 13:48:46.000000000 +0000 @@ -75,7 +75,6 @@ * Traiter les idiomes d'un texte * * @uses inc_traduire_dist() - * @uses code_echappement() * @uses echappe_retour() * * @param array{lang?: string, echappe_span?: bool} $options @@ -104,7 +103,7 @@ // si pas de traduction, on laissera l'écriture de l'idiome entier dans le texte. if (strlen($desc->texte ?? '')) { - $trad = code_echappement($desc->texte, 'idiome', false); + $trad = self::echappementHtmlBase64($desc->texte, 'idiome'); if ($l !== $lang) { $trad = str_replace("'", '"', inserer_attribut($trad, 'lang', $l)); } @@ -112,7 +111,7 @@ $trad = str_replace("'", '"', inserer_attribut($trad, 'dir', lang_dir($l))); } if (!$echappe_span) { - $trad = echappe_retour($trad, 'idiome'); + $trad = self::retablir_depuisHtmlBase64($trad, 'idiome'); } $texte = substr_replace($texte, $trad, $idiome['pos'] + $offset_pos, $idiome['length']); $offset_pos += strlen($trad) - $idiome['length']; diff -Nru spip-4.4.28+dfsg/ecrire/src/Texte/Collecteur/Modeles.php spip-4.4.30+dfsg/ecrire/src/Texte/Collecteur/Modeles.php --- spip-4.4.28+dfsg/ecrire/src/Texte/Collecteur/Modeles.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/src/Texte/Collecteur/Modeles.php 2026-11-05 13:48:46.000000000 +0000 @@ -200,7 +200,7 @@ $modele = $wrap_embed_html($m['raw'], $modele); } - $rempl = code_echappement($modele, $echap); + $rempl = self::echappementHtmlBase64($modele, $echap); $texte = substr_replace($texte, $rempl, $m['pos'] + $offset_pos, $m['length']); $offset_pos += strlen($rempl) - $m['length']; } diff -Nru spip-4.4.28+dfsg/ecrire/src/Texte/Collecteur/Multis.php spip-4.4.30+dfsg/ecrire/src/Texte/Collecteur/Multis.php --- spip-4.4.28+dfsg/ecrire/src/Texte/Collecteur/Multis.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/src/Texte/Collecteur/Multis.php 2026-11-05 13:48:46.000000000 +0000 @@ -142,7 +142,6 @@ * * @uses approcher_langue() * @uses lang_typo() - * @uses code_echappement() * @uses echappe_retour() * * @param array{lang?: string, lang_defaut?: string, echappe_span?: bool, appliquer_typo?: bool} $options @@ -161,9 +160,6 @@ if (!function_exists('approcher_langue')) { include_spip('inc/lang'); } - if (!function_exists('code_echappement')) { - include_spip('inc/texte_mini'); - } $offset_pos = 0; foreach ($multis as $m) { @@ -196,17 +192,17 @@ // il ne faut pas echapper en div si propre produit un seul paragraphe include_spip('inc/texte'); $trad_propre = preg_replace(',(^]*>|

$),Uims', '', propre($trad)); - $mode = preg_match(',[:space:]],iS', $trad_propre) ? 'div' : 'span'; - if ($mode === 'div') { + $isBloc = self::echappementTexteContientBaliseBloc($trad_propre); + if ($isBloc) { $trad = rtrim($trad) . "\n\n"; } - $trad = code_echappement($trad, 'multi', false, $mode); - $trad = str_replace("'", '"', inserer_attribut($trad, 'lang', $l)); + $attributs = ['lang' => $l]; if (lang_dir($l) !== lang_dir($lang)) { - $trad = str_replace("'", '"', inserer_attribut($trad, 'dir', lang_dir($l))); + $attributs['dir'] = lang_dir($l); } + $trad = self::echappementHtmlBase64($trad, 'multi', $isBloc, $attributs); if (!$echappe_span) { - $trad = echappe_retour($trad, 'multi'); + $trad = self::retablir_depuisHtmlBase64($trad, 'multi'); } } } diff -Nru spip-4.4.28+dfsg/ecrire/src/Texte/Collecteur/QuotedStrings.php spip-4.4.30+dfsg/ecrire/src/Texte/Collecteur/QuotedStrings.php --- spip-4.4.28+dfsg/ecrire/src/Texte/Collecteur/QuotedStrings.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/ecrire/src/Texte/Collecteur/QuotedStrings.php 2026-11-05 13:48:46.000000000 +0000 @@ -43,6 +43,7 @@ $hash = md5($texte); $this->singularities[$hash] ??= []; $search = $this->isSqlite ? ['\\\\'] : ['\\\\', "\\'", '\\"']; + $texte_length = strlen($texte); foreach ($search as $s) { $pos = 0; $last_pos = strrpos($texte, substr($s, -1)); @@ -53,12 +54,8 @@ && ($next = strpos($texte, $s, $pos)) !== false ) { $pos = $next; - $this->singularities[$hash][] = [ - 'pos' => $pos, - 'raw' => $s, - 'length' => $replace_length, - ]; - $texte = substr_replace($texte, $replace, $pos, $replace_length); + $this->singularities[$hash][$pos] = $s; + self::remplacer_en_place($texte, $texte_length, $pos, $replace, $replace_length); $pos += $replace_length; } } @@ -66,14 +63,39 @@ return $texte; } - protected function reinjecter_singularites($hash, $texte, $pos, $end) { - $singularites = array_filter($this->singularities[$hash], fn ($s) => $s['pos'] >= $pos && $s['pos'] <= $end); - if (!empty($singularites)) { - foreach ($singularites as $s) { - $texte = substr_replace($texte, $s['raw'], $s['pos'] - $pos, $s['length']); + protected function reinjecter_singularites($hash, &$quoted_strings) { + $k_quoted = 0; + $count_quoted = count($quoted_strings); + foreach ($this->singularities[$hash] as $sp => $sraw) { + while ( + $k_quoted < $count_quoted + && $quoted_strings[$k_quoted]['pos'] + $quoted_strings[$k_quoted]['length'] <= $sp + ) { + $k_quoted++; + } + if ($k_quoted === $count_quoted) { + return; + } + if ($quoted_strings[$k_quoted]['pos'] <= $sp) { + self::remplacer_en_place( + $quoted_strings[$k_quoted]['raw'], + $quoted_strings[$k_quoted]['length'], + $sp - $quoted_strings[$k_quoted]['pos'], + $sraw, + strlen($sraw) + ); } } - return $texte; + } + + /** + * Remplacement octet à octet, même longueur d'un morceau de $texte : O(len) sans recopier toute la chaîne. + * On ne déborde pas de la chaine originale grace a $texte_length fourni + */ + private static function remplacer_en_place(string &$texte, int $texte_length, int $pos, string $remplacement, int $length): void { + for ($i = 0; $i < $length && $pos + $i < $texte_length; $i++) { + $texte[$pos + $i] = $remplacement[$i]; + } } /** @@ -98,30 +120,33 @@ return $quoted_strings; } - $prev = null; - foreach ($quoted_strings as $k => &$quoted_string) { - unset($quoted_string['match']); - $quoted_string['raw'] = $this->reinjecter_singularites( - $hash, - $quoted_string['raw'], - $quoted_string['pos'], - $quoted_string['pos'] + $quoted_string['length'] - ); - - // recoller les chaines sqlite avec des ' échappés en '' à l'intérieur : deux chaines 'xxx' et 'yyyy' qui se suivent sans rien entre les 2 - if ($prev !== null - && substr($quoted_string['raw'], 0, 1) === "'" - && $quoted_string['pos'] === $quoted_strings[$prev]['pos'] + $quoted_strings[$prev]['length'] - && substr($quoted_strings[$prev]['raw'], -1, 1) === "'" - ) { - $quoted_strings[$prev]['raw'] .= $quoted_string['raw']; - $quoted_strings[$prev]['length'] += $quoted_string['length']; - unset($quoted_strings[$k]); - } else { - $prev = $k; + if (!empty($this->singularities[$hash])) { + $this->reinjecter_singularites($hash, $quoted_strings); + unset($this->singularities[$hash]); + } + + if ($this->isSqlite) { + $prev = null; + foreach ($quoted_strings as $k => &$quoted_string) { + unset($quoted_string['match']); + // recoller les chaines sqlite avec des ' échappés en '' à l'intérieur : deux chaines 'xxx' et 'yyyy' qui se suivent sans rien entre les 2 + if ($prev !== null + && str_starts_with($quoted_string['raw'], "'") + && $quoted_string['pos'] === $quoted_strings[$prev]['pos'] + $quoted_strings[$prev]['length'] + && str_ends_with($quoted_strings[$prev]['raw'], "'") + ) { + $quoted_strings[$prev]['raw'] .= $quoted_string['raw']; + $quoted_strings[$prev]['length'] += $quoted_string['length']; + unset($quoted_strings[$k]); + } else { + $prev = $k; + } + } + } else { + foreach ($quoted_strings as $k => &$quoted_string) { + unset($quoted_string['match']); } } - unset($this->singularities[$hash]); return $quoted_strings; } diff -Nru spip-4.4.28+dfsg/plugins-dist/bigup/.editorconfig spip-4.4.30+dfsg/plugins-dist/bigup/.editorconfig --- spip-4.4.28+dfsg/plugins-dist/bigup/.editorconfig 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/bigup/.editorconfig 2026-11-05 13:48:46.000000000 +0000 @@ -9,4 +9,4 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true -insert_final_newline = false +insert_final_newline = true \ No newline at end of file diff -Nru spip-4.4.28+dfsg/plugins-dist/bigup/CHANGELOG.md spip-4.4.30+dfsg/plugins-dist/bigup/CHANGELOG.md --- spip-4.4.28+dfsg/plugins-dist/bigup/CHANGELOG.md 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/bigup/CHANGELOG.md 2026-11-05 13:48:46.000000000 +0000 @@ -1,5 +1,11 @@ # Changelog +## 3.3.15 - 2026-11-04 + +### Fixed + +- spip/ecrire#196 Utiliser `AjaxContext` pour encoder le formulaire de téléversement de fichiers + ## 3.3.14 - 2026-09-02 ### Security @@ -65,4 +71,4 @@ ### Fixed - spip/spip#5460 Utiliser des variables CSS et les propriétés logiques dans la CSS de l'espace privé -- #4900 Inclusion manquante dans certains contextes ajax \ No newline at end of file +- #4900 Inclusion manquante dans certains contextes ajax diff -Nru spip-4.4.28+dfsg/plugins-dist/bigup/inc/Bigup/Identifier.php spip-4.4.30+dfsg/plugins-dist/bigup/inc/Bigup/Identifier.php --- spip-4.4.28+dfsg/plugins-dist/bigup/inc/Bigup/Identifier.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/bigup/inc/Bigup/Identifier.php 2026-11-05 13:48:46.000000000 +0000 @@ -2,6 +2,8 @@ namespace Spip\Bigup; +use Spip\Calcul\AjaxContext; + /** * Gère l'identification du formulaire * @@ -104,7 +106,7 @@ } else { $args = $args['args']; array_unshift($args, $GLOBALS['spip_lang']); - $formulaire_args = encoder_contexte_ajax($args, $form); + $formulaire_args = (new AjaxContext())->encodeFormContext($args, $form, !empty($GLOBALS['visiteur_session']['id_auteur']))->getEnv(); } $identifier = new self($form, $formulaire_args); return $identifier; @@ -215,8 +217,11 @@ public function identifier_formulaire() { include_spip('inc/securiser_action'); if ($identifier_args = charger_fonction('identifier', 'formulaires/' . $this->formulaire, true)) { - include_spip('inc/filtres'); - $args = decoder_contexte_ajax($this->formulaire_args, $this->formulaire); + $args = (new AjaxContext())->decodeFormContext( + $this->formulaire_args, + $this->formulaire, + !empty($GLOBALS['visiteur_session']['id_auteur']) + ) ?? ['']; array_shift($args); // enlever la lang qui préfixe les arguments $identite = call_user_func_array($identifier_args, $args); } else { diff -Nru spip-4.4.28+dfsg/plugins-dist/bigup/paquet.xml spip-4.4.30+dfsg/plugins-dist/bigup/paquet.xml --- spip-4.4.28+dfsg/plugins-dist/bigup/paquet.xml 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/bigup/paquet.xml 2026-11-05 13:48:46.000000000 +0000 @@ -1,7 +1,7 @@ - <:medias:info_modifier_document:> -

<:medias:info_gauche_numero_document:> #ENV{new}

- -
-#FORMULAIRE_EDITER_DOCUMENT{#ENV{new},#ENV{parent},#ENV{redirect},#ENV{lier_trad},#ENV{config_fonc}} -
-
-#FORMULAIRE_ILLUSTRER_DOCUMENT{#ENV{new}} -
\ No newline at end of file diff -Nru spip-4.4.28+dfsg/plugins-dist/mots/.editorconfig spip-4.4.30+dfsg/plugins-dist/mots/.editorconfig --- spip-4.4.28+dfsg/plugins-dist/mots/.editorconfig 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/mots/.editorconfig 2026-11-05 13:48:46.000000000 +0000 @@ -9,4 +9,4 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true -insert_final_newline = false +insert_final_newline = true \ No newline at end of file diff -Nru spip-4.4.28+dfsg/plugins-dist/mots/CHANGELOG.md spip-4.4.30+dfsg/plugins-dist/mots/CHANGELOG.md --- spip-4.4.28+dfsg/plugins-dist/mots/CHANGELOG.md 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/mots/CHANGELOG.md 2026-11-05 13:48:46.000000000 +0000 @@ -1,5 +1,11 @@ # Changelog +## 4.3.2 - 2026-11-04 + +### Security + +- spip-security/securite#4965 contrôle de l'autorisation sur l'action supprimer_mot + ## 4.3.1 - 2026-08-10 ### Fixed diff -Nru spip-4.4.28+dfsg/plugins-dist/mots/action/supprimer_mot.php spip-4.4.30+dfsg/plugins-dist/mots/action/supprimer_mot.php --- spip-4.4.28+dfsg/plugins-dist/mots/action/supprimer_mot.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/mots/action/supprimer_mot.php 2026-11-05 13:48:46.000000000 +0000 @@ -24,8 +24,11 @@ function action_supprimer_mot_dist() { $securiser_action = charger_fonction('securiser_action', 'inc'); - $id_mot = $securiser_action(); + $id_mot = (int) $securiser_action(); - include_spip('action/editer_mot'); - mot_supprimer($id_mot); + include_spip('inc/autoriser'); + if ($id_mot && autoriser('supprimer', 'mot', $id_mot)) { + include_spip('action/editer_mot'); + mot_supprimer($id_mot); + } } diff -Nru spip-4.4.28+dfsg/plugins-dist/mots/paquet.xml spip-4.4.30+dfsg/plugins-dist/mots/paquet.xml --- spip-4.4.28+dfsg/plugins-dist/mots/paquet.xml 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/mots/paquet.xml 2026-11-05 13:48:46.000000000 +0000 @@ -1,6 +1,6 @@ []]; @@ -883,6 +880,25 @@ return safehtml($texte); } +/** + * Adapte une chaîne de traitements pour la prévisualisation porte-plume. + * + * Remplace propre() et typo() par les versions surchargées qui injectent + * les options de prévisu, en préservant les fonctions d'extensions + * éventuellement imbriquées (ex. insertions_pre_propre). + * + * @param string $ps Chaîne de traitements (ex. "propre(%s)") + * @param array $options Options à injecter dans propre/typo prévisu + * @return string Chaîne adaptée + */ +function traitements_previsu_adapter(string $ps, array $options): string { + $arg_options = var_export($options, true) . ', '; + $ps = preg_replace('/\bpropre\(/', 'propre_previsu_porte_plume(' . $arg_options, $ps); + $ps = preg_replace('/\btypo\(/', 'typo_previsu_porte_plume(' . $arg_options, $ps); + + return $ps; +} + function propre_previsu_porte_plume($options_previsu, $t, $connect = null, $env = [], array $options = []) { $options = array_merge($options, $options_previsu); return propre($t, $connect, $env, $options); diff -Nru spip-4.4.28+dfsg/plugins-dist/porte_plume/tests/BarreOutilsMarkitupTest.php spip-4.4.30+dfsg/plugins-dist/porte_plume/tests/BarreOutilsMarkitupTest.php --- spip-4.4.28+dfsg/plugins-dist/porte_plume/tests/BarreOutilsMarkitupTest.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/porte_plume/tests/BarreOutilsMarkitupTest.php 2026-11-05 13:48:46.000000000 +0000 @@ -1,6 +1,6 @@ true]; + $this->assertSame($attendu, traitements_previsu_adapter($traitement, $options)); + } + + public function fournirTraitements(): array { + $arg_options = var_export(['interdire_script' => true], true) . ', '; + + return [ + 'propre natif' => [ + 'propre(%s)', + 'propre_previsu_porte_plume(' . $arg_options . '%s)', + ], + 'insertions' => [ + 'propre(insertions_pre_propre(%s))', + 'propre_previsu_porte_plume(' . $arg_options . 'insertions_pre_propre(%s))', + ], + 'insertions et retours' => [ + 'propre(autobr_pre_propre(insertions_pre_propre(%s)))', + 'propre_previsu_porte_plume(' . $arg_options . 'autobr_pre_propre(insertions_pre_propre(%s)))', + ], + 'typo natif' => [ + 'typo(%s)', + 'typo_previsu_porte_plume(' . $arg_options . '%s)', + ], + ]; + } +} diff -Nru spip-4.4.28+dfsg/plugins-dist/revisions/.editorconfig spip-4.4.30+dfsg/plugins-dist/revisions/.editorconfig --- spip-4.4.28+dfsg/plugins-dist/revisions/.editorconfig 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/revisions/.editorconfig 2026-11-05 13:48:46.000000000 +0000 @@ -9,4 +9,4 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true -insert_final_newline = false +insert_final_newline = true \ No newline at end of file diff -Nru spip-4.4.28+dfsg/plugins-dist/revisions/CHANGELOG.md spip-4.4.30+dfsg/plugins-dist/revisions/CHANGELOG.md --- spip-4.4.28+dfsg/plugins-dist/revisions/CHANGELOG.md 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/revisions/CHANGELOG.md 2026-11-05 13:48:46.000000000 +0000 @@ -1,5 +1,11 @@ # Changelog +## 3.1.15 - 2026-11-04 + +### Security + +- spip-security/securite#4954#note_235973 securiser l'affichage de #CLE et le html généré + ## 3.1.14 - 2026-10-06 ### Security diff -Nru spip-4.4.28+dfsg/plugins-dist/revisions/inc/revisions.php spip-4.4.30+dfsg/plugins-dist/revisions/inc/revisions.php --- spip-4.4.28+dfsg/plugins-dist/revisions/inc/revisions.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/revisions/inc/revisions.php 2026-11-05 13:48:46.000000000 +0000 @@ -314,7 +314,7 @@ * Récupérer les fragments d'un objet pour une version demandée * * @param int $id_objet Identifiant de l'objet - * @param string $objet Objet + * @param string $objet * @param int $id_version Identifiant de la version * @return array Couples id_fragment => texte */ @@ -465,7 +465,7 @@ * @uses retrouver_champ_version_objet() * * @param int $id_objet Identifiant de l'objet - * @param string $objet Objet + * @param string $objet * @param int $id_version Identifiant de la version * @return array Couples champs => textes */ @@ -494,7 +494,7 @@ * Récupérer les champs d'un objet, pour une version demandée * * @param int $id_objet Identifiant de l'objet - * @param string $objet Objet + * @param string $objet * @param int $id_version Identifiant de la version * @return array Couples champs => textes */ diff -Nru spip-4.4.28+dfsg/plugins-dist/revisions/inc/suivi_versions.php spip-4.4.30+dfsg/plugins-dist/revisions/inc/suivi_versions.php --- spip-4.4.28+dfsg/plugins-dist/revisions/inc/suivi_versions.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/revisions/inc/suivi_versions.php 2026-11-05 13:48:46.000000000 +0000 @@ -25,7 +25,7 @@ * Afficher un diff correspondant à une révision d'un objet * * @param int $id_objet Identifiant de l'objet - * @param string $objet Objet + * @param string $objet * @param int $id_version Identifiant de la version * @param bool $court * - false : affiche le diff complet @@ -66,7 +66,7 @@ * récupérer une version qui contient ce champ. On complète alors la liste * des champs avec la version du champ trouvée. * - * @param string $objet Objet + * @param string $objet * @param int $id_objet Identifiant de l'objet * @param int $id_version Identifiant de la version * @param string $champ Le nom du champ à retrouver @@ -106,7 +106,7 @@ * Le retour est plus ou moins locace en fonction du paramètre format. * * @param int $id_objet Identifiant de l'objet - * @param string $objet Objet + * @param string $objet * @param int $id_version Identifiant de la version * @param string $format * Type de retour diff -Nru spip-4.4.28+dfsg/plugins-dist/revisions/paquet.xml spip-4.4.30+dfsg/plugins-dist/revisions/paquet.xml --- spip-4.4.28+dfsg/plugins-dist/revisions/paquet.xml 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/revisions/paquet.xml 2026-11-05 13:48:46.000000000 +0000 @@ -1,6 +1,6 @@ -
-
[(#CLE|label_champ{#ENV{objet}})]
-
[(#VALEUR*|propre_diff)]
+
+
[(#CLE|replace{\W,_}|label_champ{#ENV{objet}})]
+
[(#VALEUR*|propre_diff)]
[
diff -Nru spip-4.4.28+dfsg/plugins-dist/safehtml/CHANGELOG.md spip-4.4.30+dfsg/plugins-dist/safehtml/CHANGELOG.md --- spip-4.4.28+dfsg/plugins-dist/safehtml/CHANGELOG.md 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/safehtml/CHANGELOG.md 2026-11-05 13:48:46.000000000 +0000 @@ -1,5 +1,18 @@ # Changelog +## 3.2.3 - 2026-11-05 + +### Fixed + +- #4803 svg_force_viewBox_px n'est pas là pour la sécurité, et casse plein de sites + +## 3.2.2 - 2026-11-04 + +### Security + +- spip-security/securite#4954 sanitization des `` éventuels dans les html +- spip-security/securite#4964 toujours réécrire les balises meta microdata + ## 3.2.1 - 2026-10-07 ### Fixed diff -Nru spip-4.4.28+dfsg/plugins-dist/safehtml/paquet.xml spip-4.4.30+dfsg/plugins-dist/safehtml/paquet.xml --- spip-4.4.28+dfsg/plugins-dist/safehtml/paquet.xml 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/safehtml/paquet.xml 2026-11-05 13:48:46.000000000 +0000 @@ -1,6 +1,6 @@ remplacerInertes(); + $this->sanitizerInclures(); $this->sanitizerBoucles(); $this->sanitizerChamps(); $this->sanitizerPolyglottes(); @@ -110,6 +111,16 @@ return $this->squelette; } + protected function sanitizerInclures() { + if (preg_match_all(BALISE_INCLURE, $this->squelette, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + foreach (array_reverse($matches) as $match) { + $echappe = str_replace('squelette = substr_replace($this->squelette, $echappe, $pos, strlen($match[0][0])); + } + } + } + protected function sanitizerBoucles() { if (str_contains($this->squelette, BALISE_BOUCLE)) { $this->squelette = str_replace(BALISE_BOUCLE, str_replace('<', '<', BALISE_BOUCLE), $this->squelette); diff -Nru spip-4.4.28+dfsg/plugins-dist/safehtml/sanitizer/svg.php spip-4.4.30+dfsg/plugins-dist/safehtml/sanitizer/svg.php --- spip-4.4.28+dfsg/plugins-dist/safehtml/sanitizer/svg.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/safehtml/sanitizer/svg.php 2026-11-05 13:48:46.000000000 +0000 @@ -43,9 +43,6 @@ } unset($file_or_svg); - // forcer une viewBox et width+height en px - $svg = svg_force_viewBox_px($svg, true); - // Securite si pas autorise : virer les scripts et les references externes // sauf si on est en mode javascript 'ok' (1), cf. inc_version // qu'on soit admin ou non, on sanitize les SVGs car rien ne dit qu'un admin sait que ca contient du JS diff -Nru spip-4.4.28+dfsg/plugins-dist/safehtml/src/Texte/Collecteur/MetasMicroData.php spip-4.4.30+dfsg/plugins-dist/safehtml/src/Texte/Collecteur/MetasMicroData.php --- spip-4.4.28+dfsg/plugins-dist/safehtml/src/Texte/Collecteur/MetasMicroData.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/safehtml/src/Texte/Collecteur/MetasMicroData.php 2026-11-05 13:48:46.000000000 +0000 @@ -61,20 +61,15 @@ $tag = $meta['raw']; // on n'accepte que les qui ont un itemprop et un content if ( - $itemprop = extraire_attribut($meta['raw'], 'itemprop') - and ($content = extraire_attribut($meta['raw'], 'content')) !== null + $itemprop = \extraire_attribut($meta['raw'], 'itemprop') + and ($content = \extraire_attribut($meta['raw'], 'content')) !== null ) { if (!empty($options['detecter_presence'])) { // si on veut seulement detecter la présence, on peut retourner tel quel avec cette meta verifiee return [$meta]; } - $tag = vider_attribut($tag, 'itemprop'); - $tag = vider_attribut($tag, 'content'); - // si la balise contient d'autres attributs on la reecrit avec ces 2 seuls là - if (strpos($tag, '=') !== false) { - $tag = ''; - $meta['raw'] = $tag; - } + // on reecrit la balise avec ces 2 seuls attributs proprements échappés + $meta['raw'] = ''; } else { unset($metas[$k]); } diff -Nru spip-4.4.28+dfsg/plugins-dist/sites/.editorconfig spip-4.4.30+dfsg/plugins-dist/sites/.editorconfig --- spip-4.4.28+dfsg/plugins-dist/sites/.editorconfig 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/sites/.editorconfig 2026-11-05 13:48:46.000000000 +0000 @@ -9,4 +9,4 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true -insert_final_newline = false +insert_final_newline = true \ No newline at end of file diff -Nru spip-4.4.28+dfsg/plugins-dist/sites/CHANGELOG.md spip-4.4.30+dfsg/plugins-dist/sites/CHANGELOG.md --- spip-4.4.28+dfsg/plugins-dist/sites/CHANGELOG.md 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/sites/CHANGELOG.md 2026-11-05 13:48:46.000000000 +0000 @@ -1,5 +1,16 @@ # Changelog +## 4.4.8 - 2026-11-04 + +### Security + +- spip-security/securite#4960 s'assurer que la copie locale du logo est dans tmp/ lors de la création d'un site +- spip-security/securite#4962 valider les urls distantes avant récupération + +### Fixed + +- spip-security/securite#4960 si pas de protocole, c'est https:// par défaut + ## 4.4.7 - 2026-10-06 ### Fixed diff -Nru spip-4.4.28+dfsg/plugins-dist/sites/action/editer_site.php spip-4.4.30+dfsg/plugins-dist/sites/action/editer_site.php --- spip-4.4.28+dfsg/plugins-dist/sites/action/editer_site.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/sites/action/editer_site.php 2026-11-05 13:48:46.000000000 +0000 @@ -16,6 +16,7 @@ */ use Spip\Afficher\Minipage\Admin as MinipageAdmin; +use Spip\Calcul\AjaxContext; if (!defined('_ECRIRE_INC_VERSION')) { return; @@ -57,14 +58,30 @@ if (!$id_syndic = intval($arg)) { $id_syndic = site_inserer(_request('id_parent')); if ( - $logo = _request('logo') - and $format_logo = _request('format_logo') + $infos_logo = _request('infos_logo') ) { - include_spip('inc/distant'); - $logo = _DIR_RACINE . copie_locale($logo); - include_spip('action/editer_logo'); - logo_modifier('site', $id_syndic, 'on', $logo); - @unlink($logo); + $ajaxContext = new AjaxContext(); + $infos_logo = $ajaxContext->decodeVarValue($infos_logo, 'infos_logo'); + if (is_array($infos_logo) + && !empty($infos_logo['logo']) + && !empty($infos_logo['format_logo']) + ) { + include_spip('inc/distant'); + if (valider_url_distante($infos_logo['logo'])) { + $nom_copie_locale = nom_fichier_copie_locale($infos_logo['logo'], $infos_logo['format_logo']); + $nom_copie_locale = _DIR_TMP . basename($nom_copie_locale); + if (($logo = copie_locale($infos_logo['logo'], 'auto', $nom_copie_locale)) + && file_exists($logo = _DIR_RACINE . $logo) + && $logo === $nom_copie_locale + ) { + include_spip('action/editer_logo'); + logo_modifier('site', $id_syndic, 'on', $logo); + if (file_exists($logo)) { + @unlink($logo); + } + } + } + } } } diff -Nru spip-4.4.28+dfsg/plugins-dist/sites/formulaires/editer_site.html spip-4.4.30+dfsg/plugins-dist/sites/formulaires/editer_site.html --- spip-4.4.28+dfsg/plugins-dist/sites/formulaires/editer_site.html 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/sites/formulaires/editer_site.html 2026-11-05 13:48:46.000000000 +0000 @@ -8,9 +8,9 @@ parametre : url d'action ] #ACTION_FORMULAIRE - - [] - [] + + [] + [] [ diff -Nru spip-4.4.28+dfsg/plugins-dist/sites/formulaires/editer_site.php spip-4.4.30+dfsg/plugins-dist/sites/formulaires/editer_site.php --- spip-4.4.28+dfsg/plugins-dist/sites/formulaires/editer_site.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/sites/formulaires/editer_site.php 2026-11-05 13:48:46.000000000 +0000 @@ -9,6 +9,8 @@ * Ce programme est un logiciel libre distribué sous licence GNU/GPL. */ +use Spip\Calcul\AjaxContext; + /** * Gestion du formulaire de d'édition d'un site * @@ -66,7 +68,7 @@ ); # pour recuperer le logo issu d'analyse auto $valeurs['logo'] = ''; - $valeurs['format_logo'] = ''; + $valeurs['infos_logo'] = ''; if (intval($id_syndic) and !autoriser('modifier', 'site', intval($id_syndic))) { $valeurs['editable'] = ''; @@ -185,6 +187,9 @@ foreach ($auto as $k => $v) { set_request($k, $v); } + $infos_logo = ['logo' => $auto['logo'], 'format_logo' => $auto['format_logo']]; + $ajaxContext = new AjaxContext(); + set_request('infos_logo', $ajaxContext->encodeVarValue($infos_logo, 'infos_logo')->getEnv()); $erreurs['verif_url_auto'] = _T('sites:texte_referencement_automatique_verifier', ['url' => $u]); $erreurs['message_erreur'] = ''; } else { diff -Nru spip-4.4.28+dfsg/plugins-dist/sites/formulaires/site.php spip-4.4.30+dfsg/plugins-dist/sites/formulaires/site.php --- spip-4.4.28+dfsg/plugins-dist/sites/formulaires/site.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/sites/formulaires/site.php 2026-11-05 13:48:46.000000000 +0000 @@ -36,10 +36,16 @@ $erreurs['url_site'] = _T('info_obligatoire'); } - if (!count($erreurs)) { - // Tester l'URL du site + if (empty($erreurs['url_site'])) { include_spip('inc/distant'); - if (!recuperer_url($url)) { + if (!valider_url_distante($url)) { + $erreurs['url_site'] = _T('form_pet_url_invalide'); + } + } + + if (empty($erreurs)) { + // Tester l'URL du site + if (!recuperer_url($url, ['callback_valider_url' => 'valider_url_distante'])) { $erreurs['url_site'] = _T('form_pet_url_invalide'); } } diff -Nru spip-4.4.28+dfsg/plugins-dist/sites/inc/site.php spip-4.4.30+dfsg/plugins-dist/sites/inc/site.php --- spip-4.4.28+dfsg/plugins-dist/sites/inc/site.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/sites/inc/site.php 2026-11-05 13:48:46.000000000 +0000 @@ -32,13 +32,17 @@ include_spip('inc/filtres'); include_spip('inc/distant'); - // Accepter les URLs au format feed:// ou qui ont oublie le http:// + // Accepter les URLs au format feed:// ou qui ont oublie le https:// $url = preg_replace(',^feed://,i', 'http://', $url); if (!preg_match(',^[a-z]+://,i', $url)) { - $url = 'http://' . $url; + $url = 'https://' . $url; } - $res = recuperer_url($url, ['transcoder' => true]); + if (!valider_url_distante($url)) { + return false; + } + + $res = recuperer_url($url, ['transcoder' => true, 'callback_valider_url' => 'valider_url_distante']); if (!$res or !$res['page'] or intval($res['status'] / 100) != 2) { return false; } else { @@ -104,7 +108,8 @@ $r ) and preg_match(',(https?://.*/.*(gif|png|jpg|svg)),ims', $r[1], $r) - and $image = recuperer_infos_distantes($r[1]) + and valider_url_distante($r[1]) + and $image = recuperer_infos_distantes($r[1], ['callback_valider_url' => 'valider_url_distante']) ) { if (in_array($image['extension'], ['gif', 'jpg', 'png', 'svg'])) { $result['format_logo'] = $image['extension']; diff -Nru spip-4.4.28+dfsg/plugins-dist/sites/paquet.xml spip-4.4.30+dfsg/plugins-dist/sites/paquet.xml --- spip-4.4.28+dfsg/plugins-dist/sites/paquet.xml 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/sites/paquet.xml 2026-11-05 13:48:46.000000000 +0000 @@ -1,6 +1,6 @@ $v) { $lowk = strtolower($k); if ( @@ -362,25 +365,22 @@ || str_starts_with($k, 'debut_') || in_array($lowk, ['amp'], true) ) { - unset($args[$k]); + $search = ($count ? '&' : '?') . $k . '=' . urlencode($v); + if (str_ends_with($qs, $search)) { + $qs = substr($qs, 0, -strlen($search)); + } elseif (($p = strpos($qs, $search . '&')) !== false) { + $qs = substr_replace($qs, '', $p, strlen($search)); + } + $removed = true; } + $count++; } - - $qs = http_build_query($args); - // cas des URLs propres QS : le premier arg est vide et on a pas le = dans la QS - if (!empty($args) && !strlen(reset($args))) { - $first_key = array_keys($args)[0]; - if (count($args) === 1) { - $qs = rtrim($qs, '='); - } elseif (!str_starts_with($parts['query'], "{$first_key}=") && str_starts_with($qs, "{$first_key}=")) { - $qs = $first_key . substr($qs, strlen("{$first_key}=")); - } + if ($removed) { + $parts['query'] = substr($qs, 1); } - - $parts['query'] = $qs; } - // Reconstruction de la URL normalisee + // Reconstruction de l'URL normalisee $scheme = $parts['scheme'] ?? ''; $host = $parts['host'] ?? ''; $port = $parts['port'] ?? ''; diff -Nru spip-4.4.28+dfsg/plugins-dist/statistiques/paquet.xml spip-4.4.30+dfsg/plugins-dist/statistiques/paquet.xml --- spip-4.4.28+dfsg/plugins-dist/statistiques/paquet.xml 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/statistiques/paquet.xml 2026-11-05 13:48:46.000000000 +0000 @@ -1,6 +1,6 @@ - + diff -Nru spip-4.4.28+dfsg/plugins-dist/statistiques/public/stats.php spip-4.4.30+dfsg/plugins-dist/statistiques/public/stats.php --- spip-4.4.28+dfsg/plugins-dist/statistiques/public/stats.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/statistiques/public/stats.php 2026-11-05 13:48:46.000000000 +0000 @@ -170,8 +170,15 @@ $referer = substr($referer, 9); } + if (empty($referer)) { + return null; + } + // On ne stocke pas plus de 255 chars dans la base... - if (empty($referer) || strlen($referer) > 255) { + if (strlen($referer) > 255 && str_contains($referer, 'fbclid=')) { + $referer = preg_replace(',([&?])fbclid=.*(&|$),', '\\1', $referer); + } + if (strlen($referer) > 255) { return null; } diff -Nru spip-4.4.28+dfsg/plugins-dist/statistiques/tests/StatsNettoyerRefererTest.php spip-4.4.30+dfsg/plugins-dist/statistiques/tests/StatsNettoyerRefererTest.php --- spip-4.4.28+dfsg/plugins-dist/statistiques/tests/StatsNettoyerRefererTest.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/statistiques/tests/StatsNettoyerRefererTest.php 2026-11-05 13:48:46.000000000 +0000 @@ -139,6 +139,10 @@ 'input' => 'http://example.com/?utm_source=google&page=1', 'expected' => 'http://example.com/?page=1', ], + 'real_case' => [ + 'input' => 'https://www.example.com/_rubrique-truc_?fbclid=IwZXh0bgNhZW0BMABwZG9mAWFkaWQBqzXcPdNhuHNydGMGYXBwX2lkDDM1MDY4NTUzMTcyOAABHj7zxkXtdSoOLPNEg_DnEEifRfLhRZqw5XqtaSGEjgK7C_CPP7IORuJ4PeLV_aem_UT-1BFsMvA4a8z2UMRuyRA&utm_medium=paid&utm_source=fb&utm_id=120249034942170648&utm_content=120249034942200648&utm_term=120249034942180648&utm_campaign=120249034942170648', + 'expected' => 'https://www.example.com/_rubrique-truc_', + ], ]; } @@ -283,6 +287,14 @@ 'input' => 'HTTPS://Example.Com:8080/?fbclid=xyz', 'expected' => 'https://example.com:8080/', ], + 'first_arg_array' => [ + 'input' => ' https://example.com/unepage.html?mots%5B%5D=277', + 'expected' => 'https://example.com/unepage.html?mots%5B%5D=277', + ], + 'first_arg_array_with_debut' => [ + 'input' => ' https://example.com/unepage.html?mots%5B%5D%5B%5D=277&debut_articles=10', + 'expected' => 'https://example.com/unepage.html?mots%5B%5D%5B%5D=277', + ], ]; } } diff -Nru spip-4.4.28+dfsg/plugins-dist/svp/CHANGELOG.md spip-4.4.30+dfsg/plugins-dist/svp/CHANGELOG.md --- spip-4.4.28+dfsg/plugins-dist/svp/CHANGELOG.md 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/svp/CHANGELOG.md 2026-11-05 13:48:46.000000000 +0000 @@ -1,5 +1,11 @@ # Changelog +## 3.2.8 - 2026-11-04 + +### Fixed + +- spip/ecrire#196 Utiliser `AjaxContext` pour encoder le formulaire d’admin des plugins + ## 3.2.7 - 2026-10-06 ### Security diff -Nru spip-4.4.28+dfsg/plugins-dist/svp/formulaires/admin_plugin.php spip-4.4.30+dfsg/plugins-dist/svp/formulaires/admin_plugin.php --- spip-4.4.28+dfsg/plugins-dist/svp/formulaires/admin_plugin.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/svp/formulaires/admin_plugin.php 2026-11-05 13:48:46.000000000 +0000 @@ -8,6 +8,8 @@ * @package SPIP\SVP\Formulaires */ +use Spip\Calcul\AjaxContext; + if (!defined('_ECRIRE_INC_VERSION')) { return; } @@ -75,7 +77,8 @@ // les actions en clair pour affichage $c = _request('_libelles_actions'); if ($c && is_string($c)) { - $valeurs['_libelles_actions'] = decoder_contexte_ajax($c, 'svp_libelles_actions') ?: []; + $valeurs['_libelles_actions'] = + (new AjaxContext())->decodeVarValue($c, 'svp_libelles_actions') ?? []; } if (!empty($valeurs['_libelles_actions'])) { // c'est un contexte ajax qu'on fait passer à verifier/traiter via un input hidden @@ -161,9 +164,14 @@ // verification des dependances include_spip('inc/svp_decider'); svp_decider_verifier_actions_demandees($a_actionner, $erreurs); - include_spip('inc/filtres'); - $todo = ($c = _request('_todo')) ? (decoder_contexte_ajax($c, 'svp_todo') ?: []) : []; - $actions = ($c = _request('_libelles_actions')) ? (decoder_contexte_ajax($c, 'svp_libelles_actions') ?: []) : []; + $todo = (($c = _request('_todo')) && is_string($c)) ? + ((new AjaxContext())->decodeVarValue($c, 'svp_todo') ?? []) + : + []; + $actions = (($c = _request('_libelles_actions')) && is_string($c)) ? + ((new AjaxContext())->decodeVarValue($c, 'svp_libelles_actions') ?? []) + : + []; // si c'est une action simple (hors suppression) sans rien a faire de plus que demande, on y go direct if (in_array('stop', $todo) or in_array('kill', $todo)) { if (in_array('stop', $todo)) { @@ -220,8 +228,10 @@ // Ajout de la liste des actions à l'actionneur // c'est lui qui va effectuer rellement les actions // lors de l'appel de action/actionner - include_spip('inc/filtres'); - $todo = ($c = _request('_todo')) ? (decoder_contexte_ajax($c, 'svp_todo') ?: []) : []; + $todo = (($c = _request('_todo')) && is_string($c)) ? + ((new AjaxContext())->decodeVarValue($c, 'svp_todo') ?? []) + : + []; if ($todo) { include_spip('inc/svp_actionner'); svp_actionner_traiter_actions_demandees($todo, $retour, $redirect); diff -Nru spip-4.4.28+dfsg/plugins-dist/svp/formulaires/charger_plugin.php spip-4.4.30+dfsg/plugins-dist/svp/formulaires/charger_plugin.php --- spip-4.4.28+dfsg/plugins-dist/svp/formulaires/charger_plugin.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/svp/formulaires/charger_plugin.php 2026-11-05 13:48:46.000000000 +0000 @@ -9,6 +9,8 @@ * @package SPIP\SVP\Formulaires */ +use Spip\Calcul\AjaxContext; + if (!defined('_ECRIRE_INC_VERSION')) { return; } @@ -43,7 +45,8 @@ // les actions en clair pour affichage $c = _request('_libelles_actions'); if ($c && is_string($c)) { - $valeurs['_libelles_actions'] = decoder_contexte_ajax($c, 'svp_libelles_actions') ?: []; + $valeurs['_libelles_actions'] = + (new AjaxContext())->decodeVarValue($c, 'svp_libelles_actions') ?? []; } if (!empty($valeurs['_libelles_actions'])) { // c'est un contexte ajax qu'on fait passer à verifier/traiter via un input hidden @@ -144,7 +147,11 @@ // c'est lui qui va effectuer rellement les actions // lors de l'appel de action/actionner include_spip('inc/filtres'); - $todo = ($c = _request('_todo')) ? (decoder_contexte_ajax($c, 'svp_todo') ?: []) : []; + $todo = + (($c = _request('_todo')) && is_string($c)) ? + ((new AjaxContext())->decodeVarValue($c, 'svp_todo') ?? []) + : + []; if (!empty($todo)) { include_spip('inc/svp_actionner'); // si toutes les actions sont des téléchargements (pas d'activation), on reste sur cette page diff -Nru spip-4.4.28+dfsg/plugins-dist/svp/inc/svp_decider.php spip-4.4.30+dfsg/plugins-dist/svp/inc/svp_decider.php --- spip-4.4.28+dfsg/plugins-dist/svp/inc/svp_decider.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/svp/inc/svp_decider.php 2026-11-05 13:48:46.000000000 +0000 @@ -10,6 +10,8 @@ * @package SPIP\SVP\Decideur */ +use Spip\Calcul\AjaxContext; + if (!defined('_ECRIRE_INC_VERSION')) { return; } @@ -1253,7 +1255,10 @@ $actions['decideur_propositions'] = $decideur->presenter_actions('changes'); $actions['decideur_demandes'] = $decideur->presenter_actions('ask'); $actions['decideur_actions'] = $decideur->presenter_actions('todo'); - set_request('_libelles_actions', encoder_contexte_ajax($actions, 'svp_libelles_actions')); + set_request( + '_libelles_actions', + (new AjaxContext())->encodeVarValue($actions, 'svp_libelles_actions')->getEnv() + ); // On construit la liste des actions pour la passer au formulaire en hidden $todo = []; @@ -1261,7 +1266,10 @@ $todo[$_todo['i']] = $_todo['todo']; } include_spip('inc/filtres'); - set_request('_todo', encoder_contexte_ajax($todo, 'svp_todo')); + set_request( + '_todo', + (new AjaxContext())->encodeVarValue($todo, 'svp_todo')->getEnv() + ); return true; } diff -Nru spip-4.4.28+dfsg/plugins-dist/svp/paquet.xml spip-4.4.30+dfsg/plugins-dist/svp/paquet.xml --- spip-4.4.28+dfsg/plugins-dist/svp/paquet.xml 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/plugins-dist/svp/paquet.xml 2026-11-05 13:48:46.000000000 +0000 @@ -1,7 +1,7 @@ + +### Fixed + +- #145 confirmation du mot de passe et rubriques restreintes conservées à la création d'un auteur admin +- spip/ecrire#176 affichage des arguments des jobs via un filtre + ## 1.1.7 - 2026-10-06 ### Security diff -Nru spip-4.4.28+dfsg/prive/formulaires/editer_auteur.html spip-4.4.30+dfsg/prive/formulaires/editer_auteur.html --- spip-4.4.28+dfsg/prive/formulaires/editer_auteur.html 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/prive/formulaires/editer_auteur.html 2026-11-05 13:48:46.000000000 +0000 @@ -51,7 +51,7 @@ ] [(#AUTORISER{modifier,auteur,#ID_AUTEUR,'',#ARRAY{statut,'?'}}|oui)
- #INCLURE{fond=formulaires/inc-instituer_auteur,statut=#ENV{statut,#CONST{_STATUT_AUTEUR_CREATION}},id_auteur,webmestre} + #INCLURE{fond=formulaires/inc-instituer_auteur,statut=#ENV{statut,#CONST{_STATUT_AUTEUR_CREATION}},id_auteur,webmestre,restreintes}
] [(#REM) Site Web : nom et url @@ -145,4 +145,9 @@ jQuery('.confirm-current-password').show('fast'); } }) + jQuery('select#statut, input#webmestre').on('change', function() { + if (jQuery('select#statut').val() === '0minirezo' || jQuery('input#webmestre').is(':checked')) { + jQuery('.confirm-current-password:hidden').show('fast'); + } + }) diff -Nru spip-4.4.28+dfsg/prive/formulaires/editer_auteur.php spip-4.4.30+dfsg/prive/formulaires/editer_auteur.php --- spip-4.4.28+dfsg/prive/formulaires/editer_auteur.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/prive/formulaires/editer_auteur.php 2026-11-05 13:48:46.000000000 +0000 @@ -53,6 +53,9 @@ $valeurs['new_login'] = $valeurs['login']; + // rubriques restreintes postées, pour les réafficher en cas d'erreur + $valeurs['restreintes'] = null; + // S'il n'y a pas la langue, on prend la langue du site $valeurs['langue'] = $valeurs['langue'] ?: $GLOBALS['meta']['langue_site']; @@ -124,7 +127,14 @@ $config['edit_pass'] = (auth_autoriser_modifier_pass($auth_methode) && autoriser('modifier', 'auteur', $row['id_auteur'])); - $config['maybe_confirm_pass'] = ($row['statut'] === '0minirezo' || $row['webmestre'] === 'oui') ? ' ' : ''; + // le champ de confirmation par le mot de passe de l'auteur connecté est nécessaire + // pour un auteur admin ou webmestre, mais aussi à la création ou si on peut promouvoir l'auteur admin + $config['maybe_confirm_pass'] = ( + !intval($row['id_auteur'] ?? 0) + || ($row['statut'] ?? '') === '0minirezo' + || ($row['webmestre'] ?? '') === 'oui' + || autoriser('modifier', 'auteur', $row['id_auteur'], null, ['statut' => '0minirezo']) + ) ? ' ' : ''; return $config; } diff -Nru spip-4.4.28+dfsg/prive/formulaires/editer_liens.php spip-4.4.30+dfsg/prive/formulaires/editer_liens.php --- spip-4.4.28+dfsg/prive/formulaires/editer_liens.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/prive/formulaires/editer_liens.php 2026-11-05 13:48:46.000000000 +0000 @@ -9,6 +9,8 @@ * Ce programme est un logiciel libre distribué sous licence GNU/GPL. */ +use Spip\Calcul\AjaxContext; + /** * Gestion du formulaire d'édition de liens * @@ -216,13 +218,15 @@ if (!$table_source || !$objet || !$objet_lien) { return $res; } + $objet_table_source = objet_type($table_source); if (_request('tout_voir')) { set_request('recherche', ''); } include_spip('inc/autoriser'); - if (autoriser('modifier', $objet, $id_objet)) { + if (autoriser('modifier', $objet, $id_objet) + && autoriser('associer' . $table_source, $objet, $id_objet)) { // recuperer le oups du coup d'avant pour le propager à charger() si on ne fait rien par exemple lien_gerer__oups('editer_liens', 'request'); @@ -261,11 +265,13 @@ // remplacer_lien[old][new] if ($remplacer = _request('remplacer_lien')) { foreach ($remplacer as $k => $v) { - if ($old = lien_verifier_action($k, '')) { + if ($old = lien_verifier_action_sur_objet($objet_table_source, $objet, $id_objet, $k, '')) { + $old_action = reset($old); foreach (is_array($v) ? $v : [$v] as $kn => $vn) { - if ($new = lien_verifier_action($kn, $vn)) { - $supprimer[$old] = 'x'; - $ajouter[$new] = '+'; + if ($new = lien_verifier_action_sur_objet($objet_table_source, $objet, $id_objet, $kn, $vn)) { + $new_action = reset($new); + $supprimer[$old_action] = 'x'; + $ajouter[$new_action] = '+'; } } } @@ -286,23 +292,29 @@ $oups = []; foreach ($supprimer as $k => $v) { - if ($lien = lien_verifier_action($k, $v)) { - $lien = explode('-', $lien); - [$objet_source, $ids, $objet_lie, $idl, $role] = array_pad($lien, 5, null); + if ($lien = lien_verifier_action_sur_objet($objet_table_source, $objet, $id_objet, $k, $v)) { + [$raw_link, $objet_source, $ids, $objet_lie, $idl, $role] = $lien; // appliquer une condition sur le rôle si défini ('*' pour tous les roles) $cond = ($role !== null ? ['role' => $role] : []); if ($objet_lien == $objet_source) { - $oups = array_merge( - $oups, - objet_trouver_liens([$objet_source => $ids], [$objet_lie => $idl], $cond) - ); - objet_dissocier([$objet_source => $ids], [$objet_lie => $idl], $cond); + $todo_liens = objet_trouver_liens([$objet_source => $ids], [$objet_lie => $idl], $cond); + $ids_to_delete = []; + foreach ($todo_liens as $todo_lien) { + if (autoriser('modifier', $objet_source, $todo_lien[$objet_source])) { + $oups[] = $todo_lien; + $ids_to_delete[] = $todo_lien[$objet_source]; + } + } + objet_dissocier([$objet_source => $ids_to_delete], [$objet_lie => $idl], $cond); } else { - $oups = array_merge( - $oups, - objet_trouver_liens([$objet_lie => $idl], [$objet_source => $ids], $cond) - ); - objet_dissocier([$objet_lie => $idl], [$objet_source => $ids], $cond); + $todo_liens = objet_trouver_liens([$objet_lie => $idl], [$objet_source => $ids], $cond); + foreach ($todo_liens as $todo_lien) { + if (autoriser('modifier', $objet_source, $todo_lien[$objet_source])) { + $oups[] = $todo_lien; + $ids_to_delete[] = $todo_lien[$objet_source]; + } + } + objet_dissocier([$objet_lie => $idl], [$objet_source => $ids_to_delete], $cond); } } } @@ -323,16 +335,19 @@ $ajout_ok = false; include_spip('action/editer_liens'); foreach ($ajouter as $k => $v) { - if ($lien = lien_verifier_action($k, $v)) { + if ($lien = lien_verifier_action_sur_objet($objet_table_source, $objet, $id_objet, $k, $v)) { $ajout_ok = true; - [$objet1, $ids, $objet2, $idl] = explode('-', $lien); + [$raw_link, $objet_source, $ids, $objet_lie, $idl] = $lien; $qualifs = lien_retrouver_qualif($objet_lien, $lien); - if ($objet_lien == $objet1) { - lien_ajouter_liaisons($objet1, $ids, $objet2, $idl, $qualifs); - } else { - lien_ajouter_liaisons($objet2, $idl, $objet1, $ids, $qualifs); + // ici $ids doit être numérique ('*' pas autorisé) + if (intval($ids) && autoriser('modifier', $objet_source, $ids)) { + if ($objet_lien == $objet_source) { + lien_ajouter_liaisons($objet_source, $ids, $objet_lie, $idl, $qualifs); + } else { + lien_ajouter_liaisons($objet_lie, $idl, $objet_source, $ids, $qualifs); + } + set_request('id_lien_ajoute', $ids); } - set_request('id_lien_ajoute', $ids); } } } @@ -347,14 +362,17 @@ if ($ordonner) { include_spip('action/editer_liens'); foreach ($ordonner as $k => $rang_lien) { - if ($lien = lien_verifier_action($k, '')) { - [$objet1, $ids, $objet2, $idl] = explode('-', $lien); + if ($lien = lien_verifier_action_sur_objet($objet_table_source, $objet, $id_objet, $k, $v)) { + [$raw_link, $objet_source, $ids, $objet_lie, $idl] = $lien; $qualif = ['rang_lien' => $rang_lien]; - - if ($objet_lien == $objet1) { - objet_qualifier_liens([$objet1 => $ids], [$objet2 => $idl], $qualif); - } else { - objet_qualifier_liens([$objet2 => $idl], [$objet1 => $ids], $qualif); + // ici $ids doit être numérique ('*' pas autorisé) + // est-ce qu'une autorisation serait pertinente ici ? potentiellement on "modifie" tous les objets liés + if (intval($ids)) { + if ($objet_lien == $objet_source) { + objet_qualifier_liens([$objet_source => $ids], [$objet_lie => $idl], $qualif); + } else { + objet_qualifier_liens([$objet_lie => $idl], [$objet_source => $ids], $qualif); + } } set_request('id_lien_ajoute', $ids); lien_gerer__oups('editer_liens', 'reset'); @@ -397,29 +415,49 @@ $action = $v; } } - // ajout un role null fictif (plus pratique) si pas défini - if ($action && count(explode('-', $action)) == 4) { - $action .= '-'; - } return $action; } /** + * Verifier que l'action trouvée par lien_verifier_action() concerne bien les bons objets + * et mettre si besoin les arguments dans le bon sens + */ +function lien_verifier_action_sur_objet(string $objet_source, string $objet, int $id_objet, $k, $v): ?array { + $action = (string) lien_verifier_action($k, $v); + $lien = explode('-', $action); + [$objet_g, $id_g, $objet_d, $id_d, $role] = array_pad($lien, 5, null); + if ( + (string) $objet_g === $objet + && (int) $id_g === $id_objet + && (string) $objet_d === $objet_source + ) { + return [$action, (string) $objet_d, $id_d, (string) $objet_g, (int) $id_g, (string) $role]; + } + if ( + (string) $objet_d === $objet + && (int) $id_d === $id_objet + && (string) $objet_g === $objet_source + ) { + return [$action, (string) $objet_g, $id_g, (string) $objet_d, (int) $id_d, (string) $role]; + } + + return null; +} + +/** * Retrouve le ou les qualificatifs postés avec une liaison demandée * * @internal * @param string $objet_lien * objet qui porte le lien - * @param string $lien - * Action du lien + * @param array $lien + * Action du lien préparée par lien_verifier_action_sur_objet() * @return array * Liste des qualifs pour chaque lien. Tableau vide s'il n'y en a pas. */ -function lien_retrouver_qualif($objet_lien, $lien) { - // un role est défini dans la liaison - $defs = explode('-', $lien); - [$objet1, , $objet2, , $role] = array_pad($defs, 5, null); +function lien_retrouver_qualif($objet_lien, array $lien): ?array { + [$raw_link, $objet1, , $objet2, , $role] = $lien; if ($objet_lien == $objet1) { $colonne_role = roles_colonne($objet1, $objet2); } else { @@ -443,15 +481,15 @@ return []; } - // pas avec l'action complete (incluant le role) + // pas avec l'action complete (incluant un role vide) $qualif = []; if ( - (!isset($qualifier_lien[$lien]) || !($qualif = $qualifier_lien[$lien])) && count($defs) == 5 + (!isset($qualifier_lien[$raw_link]) || !($qualif = $qualifier_lien[$raw_link])) + && str_ends_with($raw_link, '-') ) { - // on tente avec l'action sans le role - array_pop($defs); - $lien = implode('-', $defs); - if (!isset($qualifier_lien[$lien]) || !($qualif = $qualifier_lien[$lien])) { + // on tente avec l'action sans le role vide + $raw_link = substr($raw_link, 0, -1); + if (!isset($qualifier_lien[$raw_link]) || !($qualif = $qualifier_lien[$raw_link])) { $qualif = []; } } @@ -538,7 +576,7 @@ if ($oups = _request('_oups')) { include_spip('inc/filtres'); // on accepte uniquement une valeur signée - if ($oups = decoder_contexte_ajax($oups, $form)) { + if ($oups = (new AjaxContext())->decodeVarValue($oups, "$form:oups")) { if ( !is_array($oups) || empty($oups['id_auteur']) @@ -583,14 +621,13 @@ return ''; } - include_spip('inc/filtres'); $oups = [ 'id_auteur' => $GLOBALS['visiteur_session']['id_auteur'] ?? 0, 'time' => strtotime(date('Y-m-d H:00:00')), 'args' => lien_gerer__oups_collecter_args($form, debug_backtrace(0, 5)), 'oups_value' => $_oups_value, ]; - return encoder_contexte_ajax($oups, $form); + return (new AjaxContext())->encodeVarValue($oups, "$form:oups")->getEnv(); } } diff -Nru spip-4.4.28+dfsg/prive/formulaires/inc-instituer_auteur.html spip-4.4.30+dfsg/prive/formulaires/inc-instituer_auteur.html --- spip-4.4.28+dfsg/prive/formulaires/inc-instituer_auteur.html 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/prive/formulaires/inc-instituer_auteur.html 2026-11-05 13:48:46.000000000 +0000 @@ -29,6 +29,12 @@ [(#SET{label,<:info_ajouter_rubrique:>})] + [(#REM) réafficher les rubriques postées si le formulaire est réaffiché après une erreur ] + [(#ENV{restreintes}|is_array|oui)#SET{selected,#LISTE}] + + #SET{id_rubrique,#VALEUR|match{^rubrique[|]([0-9]+)$,UimsS,1}|intval} + [(#GET{id_rubrique}|oui)#SET{selected,#GET{selected}|push{rubrique|#GET{id_rubrique}}}] + [

(#GET{selected}|count|non|et{#ENV{statut}|=={0minirezo}}|?{<:info_auteur_gere_toutes_rubriques:>})

]
[

(#GET{selected}|count|?{<:info_auteur_gere_rubriques:>}|sinon{#GET{label}})

diff -Nru spip-4.4.28+dfsg/prive/formulaires/login.php spip-4.4.30+dfsg/prive/formulaires/login.php --- spip-4.4.28+dfsg/prive/formulaires/login.php 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/prive/formulaires/login.php 2026-11-05 13:48:46.000000000 +0000 @@ -350,13 +350,19 @@ } if ($cible) { + // securiser_redirect_action() est definie dans public/aiguiller, qui n'est pas charge + // quand charger() appelle traiter() pour un visiteur deja connecte (simple GET) + include_spip('public/aiguiller'); $cible = parametre_url($cible, 'var_login', '', '&'); // transformer la cible absolue en cible relative // pour pas echouer quand la meta adresse_site est foireuse - if (strncmp($cible, $u = url_de_base(), strlen($u)) == 0) { + if (str_starts_with($cible, $u = url_de_base())) { $cible = './' . substr($cible, strlen($u)); - } elseif (tester_url_absolue($cible) && !defined('_AUTORISER_LOGIN_ABS_REDIRECT')) { + } elseif ( + !securiser_redirect_action($cible, defined('_AUTORISER_LOGIN_ABS_REDIRECT') ? _AUTORISER_LOGIN_ABS_REDIRECT : false) + || str_starts_with(strtolower(trim($cible)), 'javascript:') + ) { // si c'est une url absolue, refuser la redirection // sauf si cette securite est levee volontairement par le webmestre $cible = ''; diff -Nru spip-4.4.28+dfsg/prive/identifiants.html spip-4.4.30+dfsg/prive/identifiants.html --- spip-4.4.28+dfsg/prive/identifiants.html 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/prive/identifiants.html 2026-11-05 13:48:46.000000000 +0000 @@ -17,7 +17,7 @@ ] [(#REM|styles_inline_page_login_pass)] - +

#NOM_SITE_SPIP

diff -Nru spip-4.4.28+dfsg/prive/javascript/SearchHighlight.js spip-4.4.30+dfsg/prive/javascript/SearchHighlight.js --- spip-4.4.28+dfsg/prive/javascript/SearchHighlight.js 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/prive/javascript/SearchHighlight.js 2026-11-05 13:48:46.000000000 +0000 @@ -1,51 +1,51 @@ /** * SearchHighlight plugin for jQuery - * + * * Thanks to Scott Yang * for the original idea and some code - * - * @author Renato Formato - * + * + * @author Renato Formato + * * @version 0.37 (9/1/2009) * * Options - * - exact (string, default:"exact") + * - exact (string, default:"exact") * "exact" : find and highlight the exact words. * "whole" : find partial matches but highlight whole words * "partial": find and highlight partial matches - * + * * - tag_name (string, default:'span') * The tag that is used to wrap the matched words * * - style_name (string, default:'hilite') * The class given to the tag wrapping the matched words. - * + * * - style_name_suffix (boolean, default:true) * If true a different number is added to style_name for every different matched word. - * + * * - debug_referrer (string, default:null) * Set a referrer for debugging purpose. - * + * * - engines (array of regex, default:null) * Add a new search engine regex to highlight searches coming from new search engines. * The first element is the regex to match the domain. - * The second element is the regex to match the query string. - * Ex: [/^http:\/\/my\.site\.net/i,/search=([^&]+)/i] - * + * The second element is the regex to match the query string. + * Ex: [/^http:\/\/my\.site\.net/i,/search=([^&]+)/i] + * * - highlight (string, default:null) * A jQuery selector or object to set the elements enabled for highlight. * If null or no elements are found, all the document is enabled for highlight. - * - * - nohighlight (string, default:null) + * + * - nohighlight (string, default:null) * A jQuery selector or object to set the elements not enabled for highlight. - * This option has priority on highlight. - * + * This option has priority on highlight. + * * - keys (string, default:null) - * Disable the analisys of the referrer and search for the words given as argument - * + * Disable the analisys of the referrer and search for the words given as argument + * * - min_length (number, default:null) - * Set the minimun length of a key - * + * Set the minimun length of a key + * */ if (window.jQuery) @@ -53,10 +53,10 @@ jQuery.fn.SearchHighlight = function(options) { var ref = options.debug_referrer || document.referrer; if(!ref && options.keys==undefined) return this; - + SearchHighlight.options = $.extend({exact:"exact",tag_name:'span',style_name:'hilite',style_name_suffix:true},options); - - if(options.engines) SearchHighlight.engines.unshift(options.engines); + + if(options.engines) SearchHighlight.engines.unshift(options.engines); var q = SearchHighlight.splitKeywords(options.keys!=undefined?options.keys.toLowerCase():SearchHighlight.decodeURL(ref,SearchHighlight.engines)); if(q && q.join("")) { SearchHighlight.buildReplaceTools(q); @@ -64,10 +64,10 @@ return this.each(function(){ var el = this; if(el==document) el = $("body")[0]; - SearchHighlight.hiliteElement(el); + SearchHighlight.hiliteElement(el); }) } else return this; - }; + }; var SearchHighlight = { options: {}, @@ -103,7 +103,7 @@ } } }); - + return query; }, splitKeywords: function(query) { @@ -128,7 +128,7 @@ }; return query; }, - + regexAccent : [ [/[\xC0-\xC5\u0100-\u0105]/ig,'a'], [/[\xC7\u0106-\u010D]/ig,'c'], @@ -143,7 +143,7 @@ [/\xFF/ig,'y'], [/[\x91\x92\u2018\u2019]/ig,'\''] ], - matchAccent : /[\x91\x92\xC0-\xC5\xC7-\xCF\xD1-\xD6\xD8-\xDC\xFF\u0100-\u010D\u0141\u015A-\u0167\u2018\u2019]/ig, + matchAccent : /[\x91\x92\xC0-\xC5\xC7-\xCF\xD1-\xD6\xD8-\xDC\xFF\u0100-\u010D\u0141\u015A-\u0167\u2018\u2019]/ig, replaceAccent: function(q) { SearchHighlight.matchAccent.lastIndex = 0; if(SearchHighlight.matchAccent.test(q)) { @@ -158,9 +158,9 @@ $.each(query,function(i,n){ if(!SearchHighlight.options.min_length || n.length>=SearchHighlight.options.min_length) if(n = SearchHighlight.replaceAccent(n).replace(SearchHighlight.escapeRegEx,"$1\\$2")) - re.push(n); + re.push(n); }); - + if(!re.length) return; regex = re.join("|"); switch(SearchHighlight.options.exact) { @@ -170,21 +170,21 @@ case "whole": regex = '\\b\\w*('+regex+')\\w*\\b'; break; - } + } SearchHighlight.regex = new RegExp(regex, "gi"); - + $.each(re,function(i,n){ SearchHighlight.subs[n] = SearchHighlight.options.style_name+ - (SearchHighlight.options.style_name_suffix?i+1:''); - }); + (SearchHighlight.options.style_name_suffix?i+1:''); + }); }, nosearch: /s(?:cript|tyle)|textarea/i, hiliteElement: function(el) { var opt = SearchHighlight.options, elHighlight, noHighlight; - elHighlight = opt.highlight?$(opt.highlight):$("body"); - if(!elHighlight.length) elHighlight = $("body"); + elHighlight = opt.highlight?$(opt.highlight):$("body"); + if(!elHighlight.length) elHighlight = $("body"); noHighlight = opt.nohighlight?$(opt.nohighlight):$([]); - + elHighlight.each(function(){ SearchHighlight.hiliteTree(this,noHighlight); }); @@ -212,16 +212,16 @@ endIndex += repl.length-1; startIndex += repl.length-1; $(item).before(repl).remove(); - } + } } else { if(item.nodeType==1 && item.nodeName.search(SearchHighlight.nosearch)==-1) SearchHighlight.hiliteTree(item,noHighlight); - } + } } - } + } }, fixTags : function(text) { - return text.replace("<","<").replace(">",">"); + return text.replace(//g,">"); } }; })(jQuery) diff -Nru spip-4.4.28+dfsg/prive/login.html spip-4.4.30+dfsg/prive/login.html --- spip-4.4.28+dfsg/prive/login.html 2026-10-07 13:48:22.000000000 +0000 +++ spip-4.4.30+dfsg/prive/login.html 2026-11-05 13:48:46.000000000 +0000 @@ -17,7 +17,7 @@ ] [(#REM|styles_inline_page_login_pass)] - +