diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 06a5dc3..12bf7a8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -20,7 +20,7 @@ jobs: name: "Generate CI matrix" uses: "glpi-project/plugin-ci-workflows/.github/workflows/generate-ci-matrix.yml@v1" with: - glpi-version: "11.0.x" + glpi-version: "12.0.x" ci: name: "GLPI ${{ matrix.glpi-version }} - php:${{ matrix.php-version }} - ${{ matrix.db-image }}" needs: "generate-ci-matrix" diff --git a/CHANGELOG.md b/CHANGELOG.md index d834ef4..e9d233f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.21.0] - 2026-09-07 + +### Added + +- GLPI 12 compatibility + ## [1.20.3] - 2026-09-04 ### Fixed diff --git a/MIGRATION_NOTES.md b/MIGRATION_NOTES.md new file mode 100644 index 0000000..8c26398 --- /dev/null +++ b/MIGRATION_NOTES.md @@ -0,0 +1,172 @@ +# Notes de migration GLPI 12 — plugin TreeView + +Migration réalisée sur la branche `feature/glpi-12.0`. Environnement : GLPI +core `main` (`12.0.0-dev`, hôte `glpi-db-v12`), PHP 8.4, plugin +`feature/glpi-12.0` rebasé sur `origin/main` (`722fbf2`, « 1.20.3 » #127). + +**Aucun environnement de test / migration Docker disponible** pour cette +session : la Phase 2 (migration réelle 11 → 12 avec BDD isolée) et +l'exécution de la Phase 3 (`phpunit`, qui exige une BDD de test avec le +plugin installé + actif) n'ont **pas** pu être déroulées. Les fichiers de +tests ont été validés statiquement (lint, PHPStan, cohérence avec les +helpers `Glpi\Tests\*` du core 12). Tout le reste (Phases 1, 4, 5) a été +mené à bien. + +## Points d'attention rencontrés + +- **`vendor/` local requis** : `plugin_treeview_check_prerequisites()` + échoue tant que `plugins/treeview/vendor/autoload.php` n'existe pas → + `composer install --no-dev` obligatoire après chaque clone / rebuild. + `vendor/` est git-ignoré. TreeView n'a aucune dépendance runtime + (`composer.json require` = `php` seul), `composer install` ne fait que + générer l'autoloader. +- **`require-dev: glpi-project/tools`** retiré de `composer.json` par la + branche `feature/glpi-12.0` (évite la collision `symfony/console` / + `Cannot redeclare CommandLoaderInterface` avec le core). `composer.lock` + allégé en conséquence (869 lignes de packages-dev supprimées). +- **Protection CSRF sans token en GLPI 12** : `Session::getNewCSRFToken()` + et la fonction Twig `csrf_token()` sont **dépréciées** (« Csrf protection + is now handled without tokens », `src/Session.php`). Le core n'utilise + plus `csrf_token()` dans aucun template. Appeler ces API émettrait une + déprécation → échec des tests via `GLPITestCase::tearDown()`. D'où la + suppression des `` des 3 + templates et du token manuel dans l'URL de recherche + (`inc/config.class.php`). +- **`CacheManager->clearSymfonyCache()`** émet un `CRITICAL` non bloquant en + fin de `database:update` — bug d'environnement core, indépendant du + plugin, à ne pas porter dans la PR. +- **Suspension des plugins après MAJ majeure** (non observé ici, pas de + bascule réelle) : une vraie migration 11 → 12 met + `core.plugins_execution_mode = suspended_by_update` → toutes les URL + `/plugins/*/front/*.php` renvoient **404** jusqu'à + `php bin/console plugin:resume_execution`. + +## Phase 1 — Rebase de `feature/glpi-12.0` sur `main` + +`feature/glpi-12.0` (`ced8c55`) partait de `b2b679a` ; `main` avait 3 +commits d'avance (#123 « item validation when loading trees », #125 +« enforce preference check », #127 « 1.20.3 ») → **rebase non trivial**. + +Conflits résolus : + +| Fichier | Résolution | +|---|---| +| `CHANGELOG.md` | nouvelle section `## [1.21.0]` (Added: GLPI 12 compatibility) au-dessus de `## [1.20.3]` | +| `setup.php` | `VERSION` = `1.20.3` (côté main) puis bumpé `1.21.0` en Phase 5 ; `MIN`/`MAX_GLPI` = `12.0.0` / `12.0.99` (côté feature) | +| `templates/preference.html.twig` | `#125` retire `` (l'`id` est désormais résolu côté serveur), `ced8c55` retire `` → **les deux lignes supprimées** | +| `inc/config.class.php` | auto-merge OK : refactor sécurité de `#123` + suppression du token CSRF de l'URL de recherche | + +Le commit `ced8c55` rebasé apporte : contraintes GLPI +`12.0.0` / `12.0.99` (`setup.php`), matrice CI `12.0.x`, `Makefile`, +suppression `require-dev` tools + `composer.lock` allégé, retrait des +tokens CSRF manuels (3 templates + `inc/config.class.php`). + +## Phase 2 — Migration GLPI 11 → 12 + +**Non réalisée** (pas d'environnement Docker / BDD isolée). Le plugin crée +3 tables (`glpi_plugin_treeview_configs`, `_profiles`, `_preferences`) via +`hook.php` ; aucun secret chiffré, aucune tâche cron, aucune notification, +et `plugin_treeview_install()` porte déjà toute la chaîne d'upgrade +historique (1.0 → 1.4, renommages de tables inclus). Le risque de +régression de données sur une bascule 11 → 12 est faible, mais **reste à +valider** dans un environnement dédié avant publication — la Phase 1 du +schéma est désormais couverte par `InstallationTest` en CI. + +## Phase 3 — Tests fonctionnels automatisés + +Harnais **déjà correct** sur `main` (repris tel quel par le rebase), aux +standards GLPI 12 : + +- `tests/bootstrap.php` → `require ../../../tests/bootstrap.php` (bootstrap + core, expose `Glpi\Tests\*`) + `require_once TreeviewTestCase.php` + + garde `Plugin::isPluginActive('treeview')`. +- `phpunit.xml` → suite unique `tests`. +- `tests/TreeviewTestCase.php` : `abstract` sur `Glpi\Tests\DbTestCase`, + helper `getTreeOutput()` qui capture la sortie JS de + `PluginTreeviewConfig::getNodesFromDb()`. +- `tests/Units/ConfigTest.php` : la sortie de l'arbre ne montre que les + données de l'entité active + masque un itemtype sans droit `READ`. +- `tests/Units/PreferenceTest.php` : `checkIfPreferenceExists()` / + `update()` sont bien cloisonnés au propriétaire connecté. + +Ces deux fichiers couvrent les correctifs sécurité #123/#125 mais rien de +spécifique à la migration de version. Ajouté pour cette PR : + +- `tests/bootstrap.php` → `require_once ../hook.php` (les routines + `plugin_treeview_install/uninstall()` ne sont autoloadées que pendant + l'(dés)installation). +- `tests/Units/InstallationTest.php` (`extends GLPITestCase`, pas de + transaction car DDL) : contraintes GLPI = `12.0.0`/`12.0.99` ; les 3 + tables + leurs colonnes existent, tables héritées (`_display`, + `_displayprefs`, `_preference`) absentes ; ligne de conf `id=1` semée ; + **idempotence** de `plugin_treeview_install()` (re-run ⇒ aucun doublon, + `clearSchemaCache`). +- `tests/Units/ProfileTest.php` (`extends TreeviewTestCase`, transactionnel) : + `PluginTreeviewProfile::createFirstAccess()` accorde le droit `r` à un + profil neuf et est idempotent ; `cleanProfiles()` (hook `PRE_ITEM_PURGE`) + supprime la ligne quand le profil est purgé. (Remplace un test qui + supposait à tort que l'install CLI sème toujours un profil — faux quand + `bin/console plugin:install` tourne sans session de profil active.) +- `tests/Units/UninstallationTest.php` (`extends GLPITestCase`) : + `plugin_treeview_uninstall()` réel ⇒ les 3 tables sont supprimées, puis + **réinstallation dans un `finally`** (BDD de test partagée) ⇒ tables + recréées + conf `id=1` re-semée. +- `tests/Units/FormRenderingTest.php` (`extends TreeviewTestCase`) : rend + les 3 formulaires Twig modifiés (`showConfigForm()`, + `PluginTreeviewProfile::showForm()`, `showFormUserPreference()`) ⇒ + `
` présent, champ attendu présent, **aucun `_glpi_csrf_token`**. + `GLPITestCase::tearDown()` transforme toute déprécation + (`Session::getNewCSRFToken()`…) en échec ⇒ verrou anti-régression CSRF. + +Vérifications effectuées (sans BDD de test) : + +- `parallel-lint` sur `tests/` → aucune erreur de syntaxe. +- Helpers utilisés (`login`, `getTestRootEntity`, `createItem`, + `setEntity`, `removeRightFromProfile`, `addRightToProfile`, + `getUniqueString`) → tous présents dans `tests/src/{DbTestCase, + GLPITestCase}.php` du core 12. +- `phpunit --list-tests` s'arrête proprement sur « Plugin treeview is not + active in the test database » (bootstrap core chargé, autoload plugin OK, + seule la BDD de test manque) → structure du harnais saine. +- Les modifications de code (résolutions de conflit, Rector) ne touchent + pas les API couvertes par les tests (`getNodesFromDb`, + `checkIfPreferenceExists`, `addDefaultPreference`, `update`). + +**À faire avant merge** : `make test-setup` (env testing) puis +`php ../../vendor/bin/phpunit` depuis `plugins/treeview/` dans un +environnement disposant d'une BDD de test. + +## Phase 4 — Analyse statique / lint + +| Outil | Résultat | +|---|---| +| `parallel-lint` (front, inc, public, hook.php, setup.php, tests) | ✅ aucune erreur | +| `php-cs-fixer` (dry-run) | ✅ 0 finding sur le code source (seuls des fichiers de cache `var/rector/`, git-ignorés, sont signalés) | +| `rector` (baseline `../../PluginsRector.php`) | 🔧 6 fichiers corrigés → dry-run ensuite propre | +| PHPStan level 5 | ✅ `No errors` | +| Psalm (+ taint) | ✅ `No errors` | +| `twigcs` (templates/) | ✅ `No violation found` | +| `tools:licence_headers_check --plugin=treeview` | ✅ headers valides | + +Correctifs Rector (équivalences sûres en GLPI 12) : + +- `ReplaceCommonGlpiGetTypeByClassConstantRector` : `$item->getType()` / + `$item::getType()` → `$item::class` dans `inc/{config,preference, + profile}.class.php` (`getTabNameForItem` / `displayTabContentForItem`). + En GLPI 12 `getType()` renvoie déjà `static::class` ; pour les classes + core (`Config`, `Preference`, `Profile`, non namespacées) la valeur est + identique. +- `ReplaceHardcodedRightnameByCommonDBTMRightnamePropertyRector` : + `'config'` → `Config::$rightname`, `'profile'` → `Profile::$rightname` + dans `setup.php`, `front/config.form.php`, `front/profile.form.php`, + `inc/profile.class.php`. + +## Phase 5 — Versioning & finalisation + +- Bump **`1.20.3` → `1.21.0`** : + - `setup.php` : `PLUGIN_TREEVIEW_VERSION` = `1.21.0` ; + `PLUGIN_TREEVIEW_MIN_GLPI` / `MAX_GLPI` = `12.0.0` / `12.0.99`. + - `treeview.xml` : nouveau bloc `` `1.21.0` / + `~12.0.0`. + - `CHANGELOG.md` : `## [1.21.0] - 2026-09-07`. +- `composer install --no-dev` rejoué (autoloader). diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ef1bed5 --- /dev/null +++ b/Makefile @@ -0,0 +1 @@ +include ../../PluginsMakefile.mk diff --git a/composer.json b/composer.json index 8338bce..5881de2 100644 --- a/composer.json +++ b/composer.json @@ -2,9 +2,6 @@ "require": { "php": ">=8.2" }, - "require-dev": { - "glpi-project/tools": "^0.8.3" - }, "config": { "optimize-autoloader": true, "platform": { diff --git a/composer.lock b/composer.lock index 9bbf694..e798ec8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,876 +4,9 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3b0805ca810a8f2c1092a852c12f1234", + "content-hash": "4cacb822b12bf0d2a0dbcf952b20ef7b", "packages": [], - "packages-dev": [ - { - "name": "glpi-project/tools", - "version": "0.8.3", - "source": { - "type": "git", - "url": "https://github.com/glpi-project/tools.git", - "reference": "8ea2a7d4702a858f4b0360ba7d4f1841a5e77026" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/glpi-project/tools/zipball/8ea2a7d4702a858f4b0360ba7d4f1841a5e77026", - "reference": "8ea2a7d4702a858f4b0360ba7d4f1841a5e77026", - "shasum": "" - }, - "require": { - "symfony/console": "^5.4 || ^6.0", - "twig/twig": "^3.3" - }, - "require-dev": { - "nikic/php-parser": "^4.13", - "phpstan/phpstan-src": "^1.10" - }, - "bin": [ - "bin/extract-locales", - "bin/licence-headers-check", - "tools/plugin-release" - ], - "type": "library", - "autoload": { - "psr-4": { - "GlpiProject\\Tools\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-3.0-or-later" - ], - "authors": [ - { - "name": "Teclib'", - "email": "glpi@teclib.com", - "homepage": "http://teclib-group.com" - } - ], - "description": "Various tools for GLPI and its plugins", - "keywords": [ - "glpi", - "plugins", - "tools" - ], - "support": { - "issues": "https://github.com/glpi-project/tools/issues", - "source": "https://github.com/glpi-project/tools" - }, - "time": "2025-10-14T10:26:06+00:00" - }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, - { - "name": "symfony/console", - "version": "v6.4.27", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/13d3176cf8ad8ced24202844e9f95af11e2959fc", - "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" - }, - "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/v6.4.27" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-10-06T10:25:16+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.7.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", - "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-dev" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-06-05T06:23:12+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.37.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "141046a8f9477948ff284fa65be2095baafb94f2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", - "reference": "141046a8f9477948ff284fa65be2095baafb94f2", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-04-10T16:19:22+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-06-27T09:58:17+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.38.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-27T06:59:30+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v3.6.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-04-25T09:37:31+00:00" - }, - { - "name": "symfony/string", - "version": "v7.3.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/translation-contracts": "<2.5" - }, - "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" - }, - "type": "library", - "autoload": { - "files": [ - "Resources/functions.php" - ], - "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", - "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], - "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-09-11T14:36:48+00:00" - }, - { - "name": "twig/twig", - "version": "v3.27.1", - "source": { - "type": "git", - "url": "https://github.com/twigphp/Twig.git", - "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/ae2071bffb38f04847fc0864d730c94b9cb8ab74", - "reference": "ae2071bffb38f04847fc0864d730c94b9cb8ab74", - "shasum": "" - }, - "require": { - "php": ">=8.1.0", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3" - }, - "require-dev": { - "php-cs-fixer/shim": "^3.0@stable", - "phpstan/phpstan": "^2.0@stable", - "psr/container": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" - }, - "type": "library", - "autoload": { - "files": [ - "src/Resources/core.php", - "src/Resources/debug.php", - "src/Resources/escaper.php", - "src/Resources/string_loader.php" - ], - "psr-4": { - "Twig\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Twig Team", - "role": "Contributors" - }, - { - "name": "Armin Ronacher", - "email": "armin.ronacher@active-4.com", - "role": "Project Founder" - } - ], - "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "https://twig.symfony.com", - "keywords": [ - "templating" - ], - "support": { - "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.27.1" - }, - "funding": [ - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/twig/twig", - "type": "tidelift" - } - ], - "time": "2026-05-30T17:09:26+00:00" - } - ], + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": {}, diff --git a/front/config.form.php b/front/config.form.php index 824f815..31e9609 100644 --- a/front/config.form.php +++ b/front/config.form.php @@ -28,7 +28,7 @@ * ------------------------------------------------------------------------- */ -Session::checkRight('config', UPDATE); +Session::checkRight(Config::$rightname, UPDATE); $config = new PluginTreeviewConfig(); if (isset($_POST['update'])) { diff --git a/front/profile.form.php b/front/profile.form.php index 576576e..fb347ef 100644 --- a/front/profile.form.php +++ b/front/profile.form.php @@ -28,7 +28,7 @@ * ------------------------------------------------------------------------- */ -Session::checkRight('profile', UPDATE); +Session::checkRight(Profile::$rightname, UPDATE); $prof = new PluginTreeviewProfile(); diff --git a/inc/config.class.php b/inc/config.class.php index da447ff..be04c5b 100644 --- a/inc/config.class.php +++ b/inc/config.class.php @@ -64,8 +64,8 @@ public static function getTypeName($nb = 0) public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { - if ($item->getType() === "Config") { - return self::createTabEntry(self::getTypeName(), 0, $item::getType(), self::getIcon()); + if ($item::class === "Config") { + return self::createTabEntry(self::getTypeName(), 0, $item::class, self::getIcon()); } return ''; @@ -74,7 +74,7 @@ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) { $config = new self(); - if ($item->getType() === "Config") { + if ($item::class === "Config") { $config->showConfigForm(); } @@ -365,8 +365,7 @@ public function getNodesFromDb() } $value = $r['id']; - $token = Session::getNewCSRFToken(); - $getParam = sprintf('?is_deleted=0&criteria[0][field]=%d&criteria[0][searchtype]=equals&criteria[0][value]=%s&search=Rechercher&start=0&_glpi_csrf_token=%s', $field_num, $value, $token); + $getParam = sprintf('?is_deleted=0&criteria[0][field]=%d&criteria[0][searchtype]=equals&criteria[0][value]=%s&search=Rechercher&start=0', $field_num, $value); $searchUrl = Toolbox::getItemTypeSearchURL($type) . $getParam; diff --git a/inc/preference.class.php b/inc/preference.class.php index 13fd601..f2c4955 100644 --- a/inc/preference.class.php +++ b/inc/preference.class.php @@ -110,8 +110,8 @@ public function checkPreferenceValue($users_id) public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { - if ($item->getType() == 'Preference') { - return self::createTabEntry(PluginTreeviewConfig::getTypeName(), 0, $item::getType(), PluginTreeviewConfig::getIcon()); + if ($item::class == 'Preference') { + return self::createTabEntry(PluginTreeviewConfig::getTypeName(), 0, $item::class, PluginTreeviewConfig::getIcon()); } return ''; @@ -119,7 +119,7 @@ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) { - if ($item->getType() == 'Preference') { + if ($item::class == 'Preference') { $pref = new self(); $pref_ID = $pref->checkIfPreferenceExists(Session::getLoginUserID()); if (!$pref_ID) { diff --git a/inc/profile.class.php b/inc/profile.class.php index 5ffe9b1..a59aa26 100644 --- a/inc/profile.class.php +++ b/inc/profile.class.php @@ -78,7 +78,7 @@ public static function changeProfile() **/ public function showForm($id, $options = []) { - if (!Session::haveRight('profile', READ)) { + if (!Session::haveRight(Profile::$rightname, READ)) { return false; } @@ -106,8 +106,8 @@ public static function cleanProfiles(Profile $prof) public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { - if ($item->getType() == 'Profile') { - return self::createTabEntry(PluginTreeviewConfig::getTypeName(), 0, $item::getType(), PluginTreeviewConfig::getIcon()); + if ($item::class == 'Profile') { + return self::createTabEntry(PluginTreeviewConfig::getTypeName(), 0, $item::class, PluginTreeviewConfig::getIcon()); } return ''; diff --git a/setup.php b/setup.php index 4ab5fac..948050e 100644 --- a/setup.php +++ b/setup.php @@ -32,9 +32,9 @@ use function Safe\define; -define('PLUGIN_TREEVIEW_VERSION', '1.20.3'); -define('PLUGIN_TREEVIEW_MIN_GLPI', '11.0.0'); -define('PLUGIN_TREEVIEW_MAX_GLPI', '11.0.99'); +define('PLUGIN_TREEVIEW_VERSION', '1.21.0'); +define('PLUGIN_TREEVIEW_MIN_GLPI', '12.0.0'); +define('PLUGIN_TREEVIEW_MAX_GLPI', '12.0.99'); function plugin_init_treeview() { @@ -101,7 +101,7 @@ function plugin_init_treeview() } // Config page - if (Session::haveRight('config', UPDATE)) { + if (Session::haveRight(Config::$rightname, UPDATE)) { $PLUGIN_HOOKS[Hooks::CONFIG_PAGE]['treeview'] = '../../front/config.form.php?forcetab=PluginTreeviewConfig$1'; } diff --git a/templates/config.html.twig b/templates/config.html.twig index fa1f854..9853ce9 100644 --- a/templates/config.html.twig +++ b/templates/config.html.twig @@ -35,7 +35,6 @@ - {{ fields.dropdownArrayField( 'target', diff --git a/templates/preference.html.twig b/templates/preference.html.twig index e3580cf..5586017 100644 --- a/templates/preference.html.twig +++ b/templates/preference.html.twig @@ -34,7 +34,6 @@ - {{ fields.checkboxField( 'show_on_load', diff --git a/templates/profile.html.twig b/templates/profile.html.twig index 7bae5fc..5df5971 100644 --- a/templates/profile.html.twig +++ b/templates/profile.html.twig @@ -35,7 +35,6 @@ - {{ fields.checkboxField( 'treeview', diff --git a/tests/Units/FormRenderingTest.php b/tests/Units/FormRenderingTest.php new file mode 100644 index 0000000..feddabe --- /dev/null +++ b/tests/Units/FormRenderingTest.php @@ -0,0 +1,108 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2007-2023 by Teclib'. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/treeview + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Treeview\Tests\Units; + +use GlpiPlugin\Treeview\Tests\TreeviewTestCase; +use PluginTreeviewConfig; +use PluginTreeviewPreference; +use PluginTreeviewProfile; +use Session; + +/** + * Smoke tests for the three Twig forms of the plugin. + * + * On GLPI 12 CSRF tokens are injected transparently: the templates no longer + * carry a `_glpi_csrf_token` hidden input nor call the deprecated + * `Session::getNewCSRFToken()`. GLPITestCase::tearDown() turns any deprecation + * or logged error raised while rendering into a test failure, so simply + * rendering each form guards the migration. + */ +final class FormRenderingTest extends TreeviewTestCase +{ + private function render(callable $renderer): string + { + $ob_level = ob_get_level(); + try { + ob_start(); + $renderer(); + return (string) ob_get_clean(); + } catch (\Throwable $e) { + while (ob_get_level() > $ob_level) { + ob_end_clean(); + } + throw $e; + } + } + + public function testConfigFormRenders(): void + { + $this->login(); + + $config = new PluginTreeviewConfig(); + $html = $this->render(static fn() => $config->showConfigForm()); + + $this->assertStringContainsString('assertMatchesRegularExpression('/name=[\'"]target[\'"]/', $html); + $this->assertStringNotContainsString('_glpi_csrf_token', $html); + } + + public function testProfileFormRenders(): void + { + $this->login(); + $profile_id = (int) $_SESSION['glpiactiveprofile']['id']; + + $plugin_profile = new PluginTreeviewProfile(); + if (!$plugin_profile->getFromDB($profile_id)) { + $plugin_profile->add(['id' => $profile_id, 'name' => 'treeview_test', 'treeview' => 'r']); + } + + $html = $this->render(static fn() => $plugin_profile->showForm($profile_id)); + + $this->assertStringContainsString('assertMatchesRegularExpression('/name=[\'"]treeview[\'"]/', $html); + $this->assertStringNotContainsString('_glpi_csrf_token', $html); + } + + public function testPreferenceFormRenders(): void + { + $this->login(); + + $pref = new PluginTreeviewPreference(); + $pref_id = $pref->addDefaultPreference((int) Session::getLoginUserID()); + $this->assertGreaterThan(0, $pref_id); + + $html = $this->render(static fn() => $pref->showFormUserPreference($pref->getFormURL(), $pref_id)); + + $this->assertStringContainsString('assertMatchesRegularExpression('/name=[\'"]show_on_load[\'"]/', $html); + $this->assertStringNotContainsString('_glpi_csrf_token', $html); + } +} diff --git a/tests/Units/InstallationTest.php b/tests/Units/InstallationTest.php new file mode 100644 index 0000000..220b352 --- /dev/null +++ b/tests/Units/InstallationTest.php @@ -0,0 +1,153 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2007-2023 by Teclib'. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/treeview + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Treeview\Tests\Units; + +use DBmysql; +use Glpi\Tests\GLPITestCase; +use PluginTreeviewConfig; + +/** + * Post-install state assertions + idempotence of plugin_treeview_install(). + * + * The plugin is installed and active in the test database (tests/bootstrap.php); + * these tests describe what that install produced and make sure re-running the + * raw install routine on GLPI 12 stays a no-op. + */ +final class InstallationTest extends GLPITestCase +{ + /** Super-Admin, present in every GLPI install. */ + private const SUPER_ADMIN_PROFILE_ID = 4; + + /** table => columns that must be present. */ + private const EXPECTED_SCHEMA = [ + 'glpi_plugin_treeview_configs' => [ + 'id', 'target', 'folderLinks', 'useSelection', 'useLines', + 'useIcons', 'closeSameLevel', 'itemName', 'locationName', + ], + 'glpi_plugin_treeview_profiles' => [ + 'id', 'name', 'treeview', + ], + 'glpi_plugin_treeview_preferences' => [ + 'id', 'users_id', 'show_on_load', + ], + ]; + + public function testPluginTargetsGlpi12(): void + { + $this->assertSame('12.0.0', PLUGIN_TREEVIEW_MIN_GLPI); + $this->assertSame('12.0.99', PLUGIN_TREEVIEW_MAX_GLPI); + } + + public function testPluginTablesAndColumnsExist(): void + { + /** @var DBmysql $DB */ + global $DB; + + foreach (self::EXPECTED_SCHEMA as $table => $columns) { + $this->assertTrue($DB->tableExists($table), "missing table {$table}"); + foreach ($columns as $column) { + $this->assertTrue( + $DB->fieldExists($table, $column), + "missing column {$table}.{$column}", + ); + } + } + + // Legacy tables renamed / dropped by the upgrade path must be gone. + foreach (['glpi_plugin_treeview_display', 'glpi_plugin_treeview_displayprefs', 'glpi_plugin_treeview_preference'] as $legacy) { + $this->assertFalse($DB->tableExists($legacy), "legacy table {$legacy} still present"); + } + } + + public function testDefaultConfigRowIsSeeded(): void + { + $config = new PluginTreeviewConfig(); + + $this->assertTrue($config->getFromDB(1), 'config row id=1 was not seeded by install'); + $this->assertSame(1, countElementsInTable('glpi_plugin_treeview_configs')); + + foreach (['target', 'folderLinks', 'useSelection', 'useLines', 'useIcons', 'closeSameLevel', 'itemName', 'locationName'] as $field) { + $this->assertArrayHasKey($field, $config->fields); + } + $this->assertContains($config->fields['target'], ['right', '_blank']); + } + + public function testInstallScriptIsIdempotent(): void + { + /** @var DBmysql $DB */ + global $DB; + + // plugin_treeview_install() reads the active profile id from the session. + $previous_profile = $_SESSION['glpiactiveprofile'] ?? null; + $_SESSION['glpiactiveprofile']['id'] = self::SUPER_ADMIN_PROFILE_ID; + + $ob_level = ob_get_level(); + try { + // First re-run may still grant first-access to the stubbed profile; + // compare the state between two consecutive extra runs. + $this->assertTrue($this->runInstall()); + $configs_after_1 = countElementsInTable('glpi_plugin_treeview_configs'); + $profiles_after_1 = countElementsInTable('glpi_plugin_treeview_profiles'); + + $this->assertTrue($this->runInstall()); + } finally { + while (ob_get_level() > $ob_level) { + ob_end_clean(); + } + if ($previous_profile === null) { + unset($_SESSION['glpiactiveprofile']); + } else { + $_SESSION['glpiactiveprofile'] = $previous_profile; + } + } + + $DB->clearSchemaCache(); + foreach (array_keys(self::EXPECTED_SCHEMA) as $table) { + $this->assertTrue($DB->tableExists($table)); + } + $this->assertSame($configs_after_1, countElementsInTable('glpi_plugin_treeview_configs')); + $this->assertSame($profiles_after_1, countElementsInTable('glpi_plugin_treeview_profiles')); + $this->assertSame(1, countElementsInTable('glpi_plugin_treeview_configs')); + } + + private function runInstall(): bool + { + $ob_level = ob_get_level(); + try { + ob_start(); + return plugin_treeview_install(); + } finally { + while (ob_get_level() > $ob_level) { + ob_end_clean(); + } + } + } +} diff --git a/tests/Units/ProfileTest.php b/tests/Units/ProfileTest.php new file mode 100644 index 0000000..5829f5e --- /dev/null +++ b/tests/Units/ProfileTest.php @@ -0,0 +1,90 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2007-2023 by Teclib'. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/treeview + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Treeview\Tests\Units; + +use GlpiPlugin\Treeview\Tests\TreeviewTestCase; +use PluginTreeviewProfile; +use Profile; + +/** + * Profile access mechanism used by plugin_treeview_install() (createFirstAccess) + * and by the PRE_ITEM_PURGE hook (cleanProfiles). Transactional: every write is + * rolled back at tearDown. + */ +final class ProfileTest extends TreeviewTestCase +{ + private function makeProfile(): Profile + { + return $this->createItem(Profile::class, [ + 'name' => 'treeview_' . $this->getUniqueString(), + ]); + } + + public function testCreateFirstAccessGrantsReadRight(): void + { + $this->login(); + $profile_id = (int) $this->makeProfile()->getID(); + + PluginTreeviewProfile::createFirstAccess($profile_id); + + $row = new PluginTreeviewProfile(); + $this->assertTrue($row->getFromDB($profile_id), 'no plugin row created for the profile'); + $this->assertSame('r', $row->fields['treeview']); + } + + public function testCreateFirstAccessIsIdempotent(): void + { + $this->login(); + $profile_id = (int) $this->makeProfile()->getID(); + + PluginTreeviewProfile::createFirstAccess($profile_id); + PluginTreeviewProfile::createFirstAccess($profile_id); + + $this->assertSame( + 1, + countElementsInTable('glpi_plugin_treeview_profiles', ['id' => $profile_id]), + ); + } + + public function testCleanProfilesRemovesRowWhenProfileIsPurged(): void + { + $this->login(); + $profile = $this->makeProfile(); + $profile_id = (int) $profile->getID(); + + PluginTreeviewProfile::createFirstAccess($profile_id); + $this->assertSame(1, countElementsInTable('glpi_plugin_treeview_profiles', ['id' => $profile_id])); + + PluginTreeviewProfile::cleanProfiles($profile); + + $this->assertSame(0, countElementsInTable('glpi_plugin_treeview_profiles', ['id' => $profile_id])); + } +} diff --git a/tests/Units/UninstallationTest.php b/tests/Units/UninstallationTest.php new file mode 100644 index 0000000..cc58bb1 --- /dev/null +++ b/tests/Units/UninstallationTest.php @@ -0,0 +1,100 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2007-2023 by Teclib'. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/treeview + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Treeview\Tests\Units; + +use DBmysql; +use Glpi\Tests\GLPITestCase; +use PluginTreeviewConfig; + +/** + * Real plugin_treeview_uninstall() run, followed by a reinstall in finally so + * the shared test database is left in its original state for the other suites. + */ +final class UninstallationTest extends GLPITestCase +{ + private const SUPER_ADMIN_PROFILE_ID = 4; + + private const PLUGIN_TABLES = [ + 'glpi_plugin_treeview_configs', + 'glpi_plugin_treeview_profiles', + 'glpi_plugin_treeview_preferences', + ]; + + public function testUninstallDropsTablesThenReinstallRestoresThem(): void + { + /** @var DBmysql $DB */ + global $DB; + + foreach (self::PLUGIN_TABLES as $table) { + $this->assertTrue($DB->tableExists($table), "precondition: {$table} should exist"); + } + + $previous_profile = $_SESSION['glpiactiveprofile'] ?? null; + $_SESSION['glpiactiveprofile']['id'] = self::SUPER_ADMIN_PROFILE_ID; + + $ob_level = ob_get_level(); + try { + // plugin_treeview_uninstall() has no return value; success is + // observed through the dropped tables below. + ob_start(); + plugin_treeview_uninstall(); + while (ob_get_level() > $ob_level) { + ob_end_clean(); + } + $DB->clearSchemaCache(); + + foreach (self::PLUGIN_TABLES as $table) { + $this->assertFalse($DB->tableExists($table), "{$table} not dropped by uninstall"); + } + } finally { + ob_start(); + $reinstalled = plugin_treeview_install(); + while (ob_get_level() > $ob_level) { + ob_end_clean(); + } + $DB->clearSchemaCache(); + + if ($previous_profile === null) { + unset($_SESSION['glpiactiveprofile']); + } else { + $_SESSION['glpiactiveprofile'] = $previous_profile; + } + } + + $this->assertTrue($reinstalled, 'reinstall failed - test database left without treeview tables'); + foreach (self::PLUGIN_TABLES as $table) { + $this->assertTrue($DB->tableExists($table), "{$table} not recreated by reinstall"); + } + + $config = new PluginTreeviewConfig(); + $this->assertTrue($config->getFromDB(1), 'reinstall did not re-seed config row id=1'); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 58781cb..083485c 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -34,3 +34,7 @@ if (!Plugin::isPluginActive('treeview')) { throw new RuntimeException('Plugin treeview is not active in the test database'); } + +// Install / uninstall routines are only autoloaded by GLPI during plugin +// (un)installation; load them explicitly so tests can exercise them. +require_once dirname(__DIR__) . '/hook.php'; diff --git a/treeview.xml b/treeview.xml index 8571572..4e5d7a3 100644 --- a/treeview.xml +++ b/treeview.xml @@ -25,6 +25,11 @@ Teclib' + + 1.21.0 + ~12.0.0 + https://github.com/pluginsGLPI/treeview/releases/download/1.21.0/glpi-treeview-1.21.0.tar.bz2 + 1.20.3 ~11.0.0