diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..4fc5c6d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## Checklist before requesting a review + +*Please delete options that are not relevant.* + +- [ ] I have performed a self-review of my code. +- [ ] I have added tests (when available) that prove my fix is effective or that my feature works. +- [ ] I have updated the CHANGELOG with a short functional description of the fix or new feature. +- [ ] This change requires a documentation update. + +## Description + +- It fixes # (issue number, if applicable) +- Here is a brief description of what this PR does + +## Screenshots (if appropriate): diff --git a/.gitignore b/.gitignore index e8e2cea..4cee350 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor/ .gh_token *.min.* var +tests/files/ diff --git a/.twig_cs.dist.php b/.twig_cs.dist.php new file mode 100644 index 0000000..352fe0a --- /dev/null +++ b/.twig_cs.dist.php @@ -0,0 +1,15 @@ +in(__DIR__ . '/templates') + ->name('*.html.twig') + ->ignoreVCSIgnored(true); + +return Twigcs\Config\Config::create() + ->setFinder($finder) + ->setRuleSet(\Glpi\Tools\GlpiTwigRuleset::class) +; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..432426a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Change Log + +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/). + +## [UNRELEASE] diff --git a/composer.json b/composer.json index a02f189..3e53354 100644 --- a/composer.json +++ b/composer.json @@ -11,5 +11,10 @@ "php": "8.2.99" }, "sort-packages": true + }, + "autoload": { + "psr-4": { + "GlpiPlugin\\Moreoptions\\Tests\\": "tests" + } } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..7f67e55 --- /dev/null +++ b/composer.lock @@ -0,0 +1,886 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "5a913d2ce2dc9ba9bc1cff231b3bcd74", + "packages": [], + "packages-dev": [ + { + "name": "glpi-project/tools", + "version": "0.7.8", + "source": { + "type": "git", + "url": "https://github.com/glpi-project/tools.git", + "reference": "bd78ad2ab0d30510729530c077f84d52b8f02866" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/glpi-project/tools/zipball/bd78ad2ab0d30510729530c077f84d52b8f02866", + "reference": "bd78ad2ab0d30510729530c077f84d52b8f02866", + "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-08-20T09:58:56+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.25", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", + "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", + "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.25" + }, + "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-08-22T10:21:53+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-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.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": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "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.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-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.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "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.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-12-23T08:48:59+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.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "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/error-handler": "^6.4|^7.0", + "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.3" + }, + "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-08-25T06:35:40+00:00" + }, + { + "name": "twig/twig", + "version": "v3.21.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "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.21.1" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-05-03T07:21:55+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.2" + }, + "platform-dev": [], + "platform-overrides": { + "php": "8.2.99" + }, + "plugin-api-version": "2.6.0" +} diff --git a/hook.php b/hook.php index e48f05e..3105ab5 100644 --- a/hook.php +++ b/hook.php @@ -1,5 +1,36 @@ doQuery($query); diff --git a/src/Controller.php b/src/Controller.php index 448babb..f5ca861 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -40,18 +40,21 @@ use Change; use Change_Group; +use Change_Item; use Change_User; use ChangeTask; use CommonDBTM; use CommonITILActor; use CommonITILObject; use CommonITILValidation; -use Glpi\Application\View\TemplateRenderer; +use Glpi\Form\Category; use GlpiPlugin\Moreoptions\Config; -use Group_Change; +use Group_Item; use Group_Problem; use Group_Ticket; -use Html; +use Item_Problem; +use Item_Ticket; +use ITILCategory; use ITILSolution; use Planning; use Problem; @@ -92,9 +95,6 @@ public static function useConfig(CommonDBTM $item): void switch ($item) { case $item instanceof Ticket_User: - if ($moconfig->fields['take_item_group_ticket'] == 1) { - $test = "OK"; - } if ($item->fields['type'] == \CommonITILActor::REQUESTER) { if ($moconfig->fields['take_requester_group_ticket'] != 0) { self::addGroupsForActorType($item, $moconfig, \CommonITILActor::REQUESTER, 'take_requester_group_ticket', 'Ticket'); @@ -106,9 +106,6 @@ public static function useConfig(CommonDBTM $item): void } break; case $item instanceof Change_User: - if ($moconfig->fields['take_item_group_change'] == 1) { - $test = "OK"; - } if ($item->fields['type'] == \CommonITILActor::REQUESTER) { if ($moconfig->fields['take_requester_group_change'] != 0) { self::addGroupsForActorType($item, $moconfig, \CommonITILActor::REQUESTER, 'take_requester_group_change', 'Change'); @@ -120,9 +117,6 @@ public static function useConfig(CommonDBTM $item): void } break; case $item instanceof Problem_User: - if ($moconfig->fields['take_item_group_problem'] == 1) { - $test = "OK"; - } if ($item->fields['type'] == \CommonITILActor::REQUESTER) { if ($moconfig->fields['take_requester_group_problem'] != 0) { self::addGroupsForActorType($item, $moconfig, \CommonITILActor::REQUESTER, 'take_requester_group_problem', 'Problem'); @@ -138,12 +132,71 @@ public static function useConfig(CommonDBTM $item): void } } + public static function addItemGroups(CommonDBTM $item): void + { + $conf = Config::getCurrentConfig(); + if ($conf->fields['is_active'] != 1) { + return; + } + + // Mapping of item types to their configuration fields and group classes + $itemMappings = [ + Item_Ticket::class => [ + 'config_field' => 'take_item_group_ticket', + 'group_class' => Group_Ticket::class, + 'foreign_key' => 'tickets_id', + ], + Change_Item::class => [ + 'config_field' => 'take_item_group_change', + 'group_class' => Change_Group::class, + 'foreign_key' => 'changes_id', + ], + Item_Problem::class => [ + 'config_field' => 'take_item_group_problem', + 'group_class' => Group_Problem::class, + 'foreign_key' => 'problems_id', + ], + ]; + + $itemClass = get_class($item); + + // Check if the item is supported and the configuration is enabled + if (!isset($itemMappings[$itemClass]) || $conf->fields[$itemMappings[$itemClass]['config_field']] != 1) { + return; + } + + $mapping = $itemMappings[$itemClass]; + + // Get the groups associated with the item + $gitems = new Group_Item(); + $groups = $gitems->find([ + 'itemtype' => $item->fields['itemtype'], + 'items_id' => $item->fields['items_id'], + ]); + + // Add each group to the ticket/change/problem + foreach ($groups as $g) { + $groupClass = $mapping['group_class']; + $gitem = new $groupClass(); + + $criteria = [ + 'groups_id' => $g['groups_id'], + $mapping['foreign_key'] => $item->fields[$mapping['foreign_key']], + 'type' => CommonITILActor::ASSIGN, + ]; + + if (!$gitem->getFromDBByCrit($criteria)) { + $gitem->add($criteria); + } + } + } + /** - * Ajoute les groupes d'un type d'acteur donné au ticket/change/problem + * Add groups for the given actor type based on the configuration */ private static function addGroupsForActorType(CommonDBTM $item, Config $moconfig, int $actorType, string $configField, string $itemType): void { - // Déterminer le type d'objet et les classes appropriées + // Determine the class to use switch ($itemType) { case 'Ticket': $object = new Ticket(); @@ -168,13 +221,12 @@ private static function addGroupsForActorType(CommonDBTM $item, Config $moconfig $actors = $object->getActorsForType($actorType); foreach ($actors as $actor) { - // Ne garder que les acteurs de type User if (!is_array($actor) || !isset($actor['itemtype']) || $actor['itemtype'] !== 'User') { continue; } if ($moconfig->fields[$configField] == 1) { - // Utiliser le groupe principal de l'utilisateur + // Use only the main group of the user $user = new User(); if (isset($actor['items_id'])) { $user->getFromDB($actor['items_id']); @@ -185,7 +237,7 @@ private static function addGroupsForActorType(CommonDBTM $item, Config $moconfig $idField => $object->fields['id'], ]; - // Ajouter le type pour les techniciens assignés + // Add type for assigned technicians if ($actorType == \CommonITILActor::ASSIGN) { $criteria['type'] = \CommonITILActor::ASSIGN; } @@ -198,7 +250,7 @@ private static function addGroupsForActorType(CommonDBTM $item, Config $moconfig $t_group->add($criteria); } } else { - // Utiliser tous les groupes de l'utilisateur + // USe all groups of the user $users_groups = new \Group_User(); if (isset($actor['items_id'])) { $u_groups = $users_groups->find([ @@ -214,7 +266,7 @@ private static function addGroupsForActorType(CommonDBTM $item, Config $moconfig $idField => $object->fields['id'], ]; - // Ajouter le type pour les techniciens assignés + // Add type for assigned technicians if ($actorType == \CommonITILActor::ASSIGN) { $criteria['type'] = \CommonITILActor::ASSIGN; } @@ -237,7 +289,7 @@ private static function addGroupsForActorType(CommonDBTM $item, Config $moconfig } } - public static function beforeCloseTicket(CommonDBTM $item): void + public static function beforeCloseITILObject(CommonITILObject $item): void { if (!is_array($item->input)) { return; @@ -290,7 +342,7 @@ public static function preventClosure(CommonDBTM $item): void } } - public static function requireFieldsToClose(CommonDBTM $item): void + public static function requireFieldsToClose(CommonITILObject $item): void { $conf = Config::getCurrentConfig(); if ($conf->fields['is_active'] != 1) { @@ -299,45 +351,69 @@ public static function requireFieldsToClose(CommonDBTM $item): void $message = ''; $itemtype = get_class($item); - if ($conf->fields['require_technician_to_close_ticket'] == 1) { - $tech = new Ticket_User(); + + // Determine the configuration suffix and actor classes based on item type + $configSuffix = '_' . strtolower($itemtype); + $userClass = $item->userlinkclass; + $groupClass = $item->grouplinkclass; + $itemIdField = $item->getForeignKeyField(); + + // Check for required technician + if ($conf->fields['require_technician_to_close' . $configSuffix] == 1) { + if (is_a($userClass, CommonDBTM::class, true)) { + $tech = new $userClass(); + } else { + // If the user class is not valid, skip this check + return; + } $techs = $tech->find([ - 'tickets_id' => $item->fields['id'], - 'type' => Ticket_User::ASSIGN, + $itemIdField => $item->fields['id'], + 'type' => CommonITILActor::ASSIGN, ]); if (count($techs) == 0) { $message .= '- ' . __s('Technician') . '
'; } } - if ($conf->fields['require_technicians_group_to_close_ticket'] == 1) { - $group = new Group_Ticket(); + + // Check for required technician group + if ($conf->fields['require_technicians_group_to_close' . $configSuffix] == 1) { + if (is_a($groupClass, CommonDBTM::class, true)) { + $group = new $groupClass(); + } else { + // If the group class is not valid, skip this check + return; + } $groups = $group->find([ - 'tickets_id' => $item->fields['id'], - 'type' => Ticket_User::ASSIGN, + $itemIdField => $item->fields['id'], + 'type' => CommonITILActor::ASSIGN, ]); if (count($groups) == 0) { $message .= '- ' . __s('Technician group') . '
'; } } - if ($conf->fields['require_category_to_close_ticket'] == 1) { - if ((isset($item->input['itilcategories_id']) && empty($item->input['itilcategories_id']))) { + + // Check for required category + if ($conf->fields['require_category_to_close' . $configSuffix] == 1) { + if ((!isset($item->input['itilcategories_id']) || empty($item->input['itilcategories_id']))) { $message .= '- ' . __s('Category') . '
'; } } - if ($conf->fields['require_location_to_close_ticket'] == 1) { - if ((isset($item->input['locations_id']) && empty($item->input['locations_id']))) { + + // Check for required location + if ($conf->fields['require_location_to_close' . $configSuffix] == 1) { + if ((!isset($item->input['locations_id']) || empty($item->input['locations_id']))) { $message .= '- ' . __s('Location') . '
'; } } - // Check if solution exist before closing the ticket - if ($conf->fields['require_solution_to_close_ticket'] == 1 + // Check if solution exists before closing + if ($conf->fields['require_solution_to_close' . $configSuffix] == 1 && is_array($item->input) && isset($item->input['status']) && $item->input['status'] == CommonITILObject::CLOSED) { $solution = new ITILSolution(); $solutions = $solution->find([ - 'itemtype' => Ticket::class, + 'itemtype' => $itemtype, 'items_id' => $item->fields['id'], 'NOT' => [ 'status' => CommonITILValidation::REFUSED, @@ -349,7 +425,9 @@ public static function requireFieldsToClose(CommonDBTM $item): void } if (!empty($message)) { - $message = __s('To close this ticket, you must fill in the following fields:', 'moreoptions') . '
' . $message; + $itemTypeLabel = $item->getTypeName(); + + $message = sprintf(__s('To close this %s, you must fill in the following fields:', 'moreoptions'), $itemTypeLabel) . '
' . $message; Session::addMessageAfterRedirect($message, false, ERROR); $item->input = false; return; @@ -396,4 +474,65 @@ public static function checkTaskRequirements(CommonDBTM $item): CommonDBTM return $item; } + + public static function updateItemActors(CommonITILObject $item): CommonITILObject + { + $conf = Config::getCurrentConfig(); + if ($conf->fields['is_active'] != 1) { + return $item; + } + + switch (get_class($item)) { + case 'Ticket': + $assign_tech_manager = $conf->fields['assign_technical_manager_when_changing_category_ticket']; + $assign_tech_group = $conf->fields['assign_technical_group_when_changing_category_ticket']; + break; + case 'Change': + $assign_tech_manager = $conf->fields['assign_technical_manager_when_changing_category_change']; + $assign_tech_group = $conf->fields['assign_technical_group_when_changing_category_change']; + break; + case 'Problem': + $assign_tech_manager = $conf->fields['assign_technical_manager_when_changing_category_problem']; + $assign_tech_group = $conf->fields['assign_technical_group_when_changing_category_problem']; + break; + default: + return $item; + } + + if ($assign_tech_manager || $assign_tech_group) { + + $itemIdField = strtolower(get_class($item)) . 's_id'; + $category = new ITILCategory(); + $fund = $category->getFromDB($item->fields['itilcategories_id']); + if ($fund) { + if ($assign_tech_manager) { + if (is_a($item->userlinkclass, CommonDBTM::class, true)) { + $user_link = new $item->userlinkclass(); + $criteria = [ + 'users_id' => $category->fields['users_id'], + 'type' => CommonITILActor::ASSIGN, + $itemIdField => $item->fields['id'], + ]; + if (!$user_link->getFromDBByCrit($criteria)) { + $user_link->add($criteria); + } + } + } + if ($assign_tech_group) { + if (is_a($item->grouplinkclass, CommonDBTM::class, true)) { + $group_link = new $item->grouplinkclass(); + $criteria = [ + 'groups_id' => $category->fields['groups_id'], + 'type' => CommonITILActor::ASSIGN, + $itemIdField => $item->fields['id'], + ]; + if (!$group_link->getFromDBByCrit($criteria)) { + $group_link->add($criteria); + } + } + } + } + } + return $item; + } } diff --git a/templates/config.html.twig b/templates/config.html.twig index d8d12ff..e251b57 100644 --- a/templates/config.html.twig +++ b/templates/config.html.twig @@ -147,33 +147,75 @@ + + {{ __('Assign technical manager when changing category', 'moreoptions') }} + + + + + + + {{ __('Assign technical group when changing category', 'moreoptions') }} + + + + +
- {{ __('Solve and Close Tickets') }} + {{ __('Solve and Close Items') }}
- {{ alerts.alert_danger(__('The options below relate to the mandatory fields for resolving and closing tickets.', 'moreoptions')) }} + {{ alerts.alert_danger(__('The options below relate to the mandatory fields for resolving and closing ITIL items.', 'moreoptions')) }} - - - - - + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ __('Technician') }}{{ __('Technicians group') }}{{ __('Category') }}{{ __('Location') }}{{ __('Solution') }}   {{ __('Ticket') }}{{ __('Change') }}{{ __('Problem') }}{{ __('All') }}
{{ __('Technician') }}
{{ __('Technicians group') }}
{{ __('Category') }}
{{ __('Location') }}
{{ __('Solution') }}
@@ -183,6 +225,8 @@ {{ __('Tasks') }} + {{ alerts.alert_danger(__('The options below relate to the mandatory fields for creating a task.', 'moreoptions')) }} + @@ -201,17 +245,6 @@
- - {# {% if pentityconfig != false %} - - {% set use_parent_field = '
- ' ~ pentityconfig ~ 's
  ' %} - {{ fields.htmlField( - '', - use_parent_field, - __('Use parent entity config'), - ) }} - {% endif %} #} @@ -224,6 +257,7 @@ \ No newline at end of file diff --git a/tests/MoreOptionsTestCase.php b/tests/MoreOptionsTestCase.php new file mode 100644 index 0000000..bbfc537 --- /dev/null +++ b/tests/MoreOptionsTestCase.php @@ -0,0 +1,162 @@ +beginTransaction(); + + // Connect the test user + $this->login(); + + parent::setUp(); + } + + protected function tearDown(): void + { + global $DB; + + // Rollback the transaction to clean up the database + $DB->rollback(); + + parent::tearDown(); + } + + /** + * Login with the test user + */ + protected function login( + string $user_name = self::TU_USER, + string $user_pass = self::TU_PASS, + bool $noauto = true, + bool $expected = true, + ): Auth { + Session::destroy(); + Session::start(); + + $auth = new Auth(); + $this->assertEquals($expected, $auth->login($user_name, $user_pass, $noauto)); + + return $auth; + } + + /** + * Logout the current user + */ + protected function logOut(): void + { + $ctime = $_SESSION['glpi_currenttime'] ?? null; + Session::destroy(); + if ($ctime) { + $_SESSION['glpi_currenttime'] = $ctime; + } + } + + /** + * Create a test configuration for the plugin + * @param array $options + */ + protected function createTestConfig(array $options = []): Config + { + $config = new Config(); + + $default_config = [ + 'is_active' => 1, + 'entities_id' => 0, + 'take_item_group_ticket' => 0, + 'take_requester_group_ticket' => 0, + 'take_technician_group_ticket' => 0, + 'take_item_group_change' => 0, + 'take_requester_group_change' => 0, + 'take_technician_group_change' => 0, + 'take_item_group_problem' => 0, + 'take_requester_group_problem' => 0, + 'take_technician_group_problem' => 0, + 'require_technician_to_close_ticket' => 0, + 'require_technicians_group_to_close_ticket' => 0, + 'require_category_to_close_ticket' => 0, + 'require_location_to_close_ticket' => 0, + 'require_solution_to_close_ticket' => 0, + 'prevent_closure_ticket' => 0, + 'prevent_closure_change' => 0, + 'prevent_closure_problem' => 0, + 'mandatory_task_category' => 0, + 'mandatory_task_duration' => 0, + 'mandatory_task_user' => 0, + 'mandatory_task_group' => 0, + ]; + + $input = array_merge($default_config, $options); + + $result = $config->add($input); + $this->assertGreaterThan(0, $result, 'Failed to create test config'); + + return $config; + } + + /** + * Update the test configuration + * @param array $updates + */ + protected function updateTestConfig(Config $config, array $updates): bool + { + $input = array_merge(['id' => $config->getID()], $updates); + return $config->update($input); + } + + /** + * Get the current configuration or create one + */ + protected function getCurrentConfig(): Config + { + $config = Config::getCurrentConfig(); + if (empty($config->fields) || $config->isNewItem()) { + $config = $this->createTestConfig(); + } + return $config; + } +} diff --git a/tests/Units/ConfigTest.php b/tests/Units/ConfigTest.php new file mode 100644 index 0000000..f76d967 --- /dev/null +++ b/tests/Units/ConfigTest.php @@ -0,0 +1,1209 @@ +getCurrentConfig(); + + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'mandatory_task_category' => 1, + 'mandatory_task_duration' => 1, + 'mandatory_task_user' => 1, + 'mandatory_task_group' => 1, + ]); + $this->assertTrue($result); + + $conf = Config::getCurrentConfig(); + + //Create a ticket + $ticket = new \Ticket(); + $ticket->add( + [ + 'name' => 'Test ticket task mandatory fields', + 'content' => 'Test content', + ], + ); + $this->assertNotFalse($ticket->getID()); + + //Create a task without mandatory fields (Expected to fail) + $task = new \TicketTask(); + $result = $task->add( + [ + 'tickets_id' => $ticket->getID(), + 'content' => 'Test task', + 'state' => \Planning::TODO, + ], + ); + $this->assertFalse($result); + + // Create category + $category = new \TaskCategory(); + $result = $category->add( + [ + 'name' => 'Test category', + ], + ); + $this->assertNotFalse($result); + + //Create a task with mandatory fields (Expected to succeed) + $task = new \TicketTask(); + $result = $task->add( + [ + 'tickets_id' => $ticket->getID(), + 'content' => 'Test task', + 'taskcategories_id' => 1, + 'users_id_tech' => 1, + 'groups_id_tech' => 1, + 'actiontime' => 300, + 'state' => \Planning::TODO, + ], + ); + $this->assertNotFalse($result); + + // Create task without user (Expected to fail) + $task = new \TicketTask(); + $result = $task->add( + [ + 'tickets_id' => $ticket->getID(), + 'content' => 'Test task without user', + 'taskcategories_id' => 1, + 'groups_id_tech' => 1, + 'actiontime' => 300, + 'state' => \Planning::TODO, + ], + ); + $this->assertFalse($result); + + // Create task without group (Expected to fail) + $task = new \TicketTask(); + $result = $task->add( + [ + 'tickets_id' => $ticket->getID(), + 'content' => 'Test task without group', + 'taskcategories_id' => 1, + 'users_id_tech' => 1, + 'actiontime' => 300, + 'state' => \Planning::TODO, + ], + ); + $this->assertFalse($result); + + // Create task without duration (Expected to fail) + $task = new \TicketTask(); + $result = $task->add( + [ + 'tickets_id' => $ticket->getID(), + 'content' => 'Test task without duration', + 'taskcategories_id' => 1, + 'users_id_tech' => 1, + 'groups_id_tech' => 1, + 'state' => \Planning::TODO, + ], + ); + $this->assertFalse($result); + + // Create task without category (Expected to fail) + $task = new \TicketTask(); + $result = $task->add( + [ + 'tickets_id' => $ticket->getID(), + 'content' => 'Test task without category', + 'users_id_tech' => 1, + 'groups_id_tech' => 1, + 'actiontime' => 300, + 'state' => \Planning::TODO, + ], + ); + $this->assertFalse($result); + + //Check if we have only 1 task + $tasks = new \TicketTask(); + $tasks = count($tasks->find(['tickets_id' => $ticket->getID()])); + $this->assertEquals(1, $tasks); + + // Reset config + $resetResult = $this->updateTestConfig($conf, [ + 'mandatory_task_category' => 0, + 'mandatory_task_duration' => 0, + 'mandatory_task_user' => 0, + 'mandatory_task_group' => 0, + ]); + $this->assertTrue($resetResult); + } + + /** + * Test mandatory fields before closing a ticket + */ + public function testTicketMandatoryFieldsBeforeCloseTicket(): void + { + $this->login(); + + $conf = $this->getCurrentConfig(); + + // Configure mandatory fields before closing + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'require_technician_to_close_ticket' => 1, + 'require_technicians_group_to_close_ticket' => 1, + 'require_category_to_close_ticket' => 1, + 'require_location_to_close_ticket' => 1, + ]); + $this->assertTrue($result); + + $conf = Config::getCurrentConfig(); + + //Create a ticket without mandatory fields (Expected to succeed) + $ticket = new \Ticket(); + $tid = $ticket->add( + [ + 'name' => 'Test ticket close', + 'content' => 'Test content', + ], + ); + $this->assertGreaterThan(0, $tid); + + // Create group + $group = new \Group(); + $gid = $group->add( + [ + 'name' => 'Test group close ticket', + ], + ); + $this->assertNotFalse($gid); + + // Close the ticket without mandatory fields (Expected to fail) + $ticket = new \Ticket(); + $result = $ticket->update( + [ + 'id' => $tid, + 'status' => \Ticket::CLOSED, + ], + ); + $this->assertFalse($result); + + // Create category + $category = new \ITILCategory(); + $cid = $category->add( + [ + 'name' => 'Test category close ticket', + ], + ); + $this->assertNotFalse($cid); + + // Create location + $location = new \Location(); + $lid = $location->add( + [ + 'name' => 'Test location close ticket', + ], + ); + $this->assertNotFalse($lid); + + // Add technician group to the ticket + $gticket = new \Group_Ticket(); + $this->assertNotFalse($gticket->add( + [ + 'tickets_id' => $tid, + 'groups_id' => $gid, + 'type' => \Group_Ticket::ASSIGN, + ], + )); + + // Add technician to the ticket + $user = new \User(); + $this->assertTrue($user->getFromDBByCrit( + [ + 'name' => 'glpi', + ], + )); + + $uticket = new \Ticket_User(); + $this->assertNotFalse($uticket->add( + [ + 'tickets_id' => $tid, + 'users_id' => $user->getID(), + 'type' => \Ticket_User::ASSIGN, + ], + )); + + // Close the ticket without location and category (Expected to fail) + $ticket = new \Ticket(); + $this->assertFalse($ticket->update( + [ + 'id' => $tid, + 'status' => \Ticket::CLOSED, + ], + )); + + // Close the ticket with location and category (Expected to succeed) + $ticket = new \Ticket(); + $this->assertTrue($ticket->update( + [ + 'id' => $tid, + 'locations_id' => $lid, + 'itilcategories_id' => $cid, + 'status' => \Ticket::CLOSED, + ], + )); + + // Reset config + $resetResult = $this->updateTestConfig($conf, [ + 'require_technician_to_close_ticket' => 0, + 'require_technicians_group_to_close_ticket' => 0, + 'require_category_to_close_ticket' => 0, + 'require_location_to_close_ticket' => 0, + ]); + $this->assertTrue($resetResult); + } + + /** + * Test mandatory fields before closing a change + */ + public function testChangeMandatoryFieldsBeforeCloseChange(): void + { + $this->login(); + + $conf = $this->getCurrentConfig(); + + // Configure mandatory fields before closing + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'require_technician_to_close_change' => 1, + 'require_technicians_group_to_close_change' => 1, + 'require_category_to_close_change' => 1, + 'require_location_to_close_change' => 1, + ]); + $this->assertTrue($result); + + $conf = Config::getCurrentConfig(); + + //Create a change without mandatory fields (Expected to succeed) + $change = new \Change(); + $cid = $change->add( + [ + 'name' => 'Test change close', + 'content' => 'Test content', + ], + ); + $this->assertGreaterThan(0, $cid); + + // Create group + $group = new \Group(); + $gid = $group->add( + [ + 'name' => 'Test group close change', + ], + ); + $this->assertNotFalse($gid); + + // Close the change without mandatory fields (Expected to fail) + $change = new \Change(); + $result = $change->update( + [ + 'id' => $cid, + 'status' => \Change::CLOSED, + ], + ); + $this->assertFalse($result); + + // Create category + $category = new \ITILCategory(); + $catid = $category->add( + [ + 'name' => 'Test category close change', + ], + ); + $this->assertNotFalse($catid); + + // Create location + $location = new \Location(); + $lid = $location->add( + [ + 'name' => 'Test location close change', + ], + ); + $this->assertNotFalse($lid); + + // Add technician group to the change + $gchange = new \Change_Group(); + $this->assertNotFalse($gchange->add( + [ + 'changes_id' => $cid, + 'groups_id' => $gid, + 'type' => \Change_Group::ASSIGN, + ], + )); + + // Add technician to the change + $user = new \User(); + $this->assertTrue($user->getFromDBByCrit( + [ + 'name' => 'glpi', + ], + )); + + $uchange = new \Change_User(); + $this->assertNotFalse($uchange->add( + [ + 'changes_id' => $cid, + 'users_id' => $user->getID(), + 'type' => \Change_User::ASSIGN, + ], + )); + + // Close the change without location and category (Expected to fail) + $change = new \Change(); + $this->assertFalse($change->update( + [ + 'id' => $cid, + 'status' => \Change::CLOSED, + ], + )); + + // Close the change with location and category (Expected to succeed) + $change = new \Change(); + $this->assertTrue($change->update( + [ + 'id' => $cid, + 'locations_id' => $lid, + 'itilcategories_id' => $catid, + 'status' => \Change::CLOSED, + ], + )); + + // Reset config + $resetResult = $this->updateTestConfig($conf, [ + 'require_technician_to_close_change' => 0, + 'require_technicians_group_to_close_change' => 0, + 'require_category_to_close_change' => 0, + 'require_location_to_close_change' => 0, + ]); + $this->assertTrue($resetResult); + } + + /** + * Test mandatory fields before closing a problem + */ + public function testProblemMandatoryFieldsBeforeCloseProblem(): void + { + $this->login(); + + $conf = $this->getCurrentConfig(); + + // Configure mandatory fields before closing + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'require_technician_to_close_problem' => 1, + 'require_technicians_group_to_close_problem' => 1, + 'require_category_to_close_problem' => 1, + 'require_location_to_close_problem' => 1, + ]); + $this->assertTrue($result); + + $conf = Config::getCurrentConfig(); + + //Create a problem without mandatory fields (Expected to succeed) + $problem = new \Problem(); + $pid = $problem->add( + [ + 'name' => 'Test problem close', + 'content' => 'Test content', + ], + ); + $this->assertGreaterThan(0, $pid); + + // Create group + $group = new \Group(); + $gid = $group->add( + [ + 'name' => 'Test group close problem', + ], + ); + $this->assertNotFalse($gid); + + // Close the problem without mandatory fields (Expected to fail) + $problem = new \Problem(); + $result = $problem->update( + [ + 'id' => $pid, + 'status' => \Problem::CLOSED, + ], + ); + $this->assertFalse($result); + + // Create category + $category = new \ITILCategory(); + $catid = $category->add( + [ + 'name' => 'Test category close problem', + ], + ); + $this->assertNotFalse($catid); + + // Create location + $location = new \Location(); + $lid = $location->add( + [ + 'name' => 'Test location close problem', + ], + ); + $this->assertNotFalse($lid); + + // Add technician group to the problem + $gproblem = new \Group_Problem(); + $this->assertNotFalse($gproblem->add( + [ + 'problems_id' => $pid, + 'groups_id' => $gid, + 'type' => \Group_Problem::ASSIGN, + ], + )); + + // Add technician to the problem + $user = new \User(); + $this->assertTrue($user->getFromDBByCrit( + [ + 'name' => 'glpi', + ], + )); + + $uproblem = new \Problem_User(); + $this->assertNotFalse($uproblem->add( + [ + 'problems_id' => $pid, + 'users_id' => $user->getID(), + 'type' => \Problem_User::ASSIGN, + ], + )); + + // Close the problem without location and category (Expected to fail) + $problem = new \Problem(); + $this->assertFalse($problem->update( + [ + 'id' => $pid, + 'status' => \Problem::CLOSED, + ], + )); + + // Close the problem with location and category (Expected to succeed) + $problem = new \Problem(); + $this->assertTrue($problem->update( + [ + 'id' => $pid, + 'locations_id' => $lid, + 'itilcategories_id' => $catid, + 'status' => \Problem::CLOSED, + ], + )); + + // Reset config + $resetResult = $this->updateTestConfig($conf, [ + 'require_technician_to_close_problem' => 0, + 'require_technicians_group_to_close_problem' => 0, + 'require_category_to_close_problem' => 0, + 'require_location_to_close_problem' => 0, + ]); + $this->assertTrue($resetResult); + } + + /** + * Test take the requester group + */ + public function testTakeTheRequesterGroup(): void + { + $conf = $this->getCurrentConfig(); + + // Configure to take all groups of the requester + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'take_requester_group_ticket' => 2, // All + ]); + $this->assertTrue($result); + + $conf = Config::getCurrentConfig(); + + // Create two groups + $group1 = new \Group(); + $result = $group1->add( + [ + 'name' => 'Test group 1', + ], + ); + $this->assertNotFalse($result); + + $group2 = new \Group(); + $result = $group2->add( + [ + 'name' => 'Test group 2', + ], + ); + $this->assertNotFalse($result); + + // Get the user glpi + $user = new \User(); + $this->assertTrue($user->getFromDBByCrit( + [ + 'name' => 'glpi', + ], + )); + + // Assign the user to the group + $group_user = new \Group_User(); + $result = $group_user->add( + [ + 'groups_id' => $group1->getID(), + 'users_id' => $user->getID(), + ], + ); + $this->assertNotFalse($result); + + $result = $group_user->add( + [ + 'groups_id' => $group2->getID(), + 'users_id' => $user->getID(), + ], + ); + $this->assertNotFalse($result); + + //Create a ticket + $ticket = new \Ticket(); + $tid = $ticket->add( + [ + 'name' => 'Test ticket requester group', + 'content' => 'Test content', + ], + ); + $this->assertGreaterThan(0, $tid); + + $uticket = new \Ticket_User(); + $this->assertNotFalse($uticket->add( + [ + 'tickets_id' => $tid, + 'users_id' => $user->getID(), + 'type' => \Ticket_User::REQUESTER, + ], + )); + + // Check if the group of the requester is in the actors + $ticket_group = new \Group_Ticket(); + $groups = $ticket_group->find(['tickets_id' => $ticket->getID()]); + $this->assertCount(2, $groups); + + $config = new Config(); + // Configurer pour ne prendre que le groupe principal du demandeur + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'take_requester_group_ticket' => 1, // Default + ]); + $this->assertTrue($result); + + $conf = Config::getCurrentConfig(); + + //Create a ticket + $ticket = new \Ticket(); + $tid = $ticket->add( + [ + 'name' => 'Test ticket requester group - 2', + 'content' => 'Test content', + ], + ); + $this->assertNotFalse($ticket->getID()); + + //Add default group to the user + $user2 = new \User(); + $this->assertTrue($user2->update( + [ + 'id' => $user->getID(), + 'groups_id' => $group1->getID(), + ], + )); + + $uticket = new \Ticket_User(); + $this->assertNotFalse($uticket->add( + [ + 'tickets_id' => $tid, + 'users_id' => $user2->getID(), + 'type' => \Ticket_User::REQUESTER, + ], + )); + + // Check if the group of the requester is in the actors + $ticket_group = new \Group_Ticket(); + $groups = $ticket_group->find(['tickets_id' => $tid]); + $this->assertCount(1, $groups); + + // Reset config + // Réinitialiser la configuration + $resetResult = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'take_requester_group_ticket' => 0, // Default + ]); + $this->assertTrue($resetResult); + } + + /** + * Test take the technician group + */ + public function testTakeTheTechnicianGroup(): void + { + $conf = $this->getCurrentConfig(); + + // Setup to take all groups of the technician + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'take_technician_group_ticket' => 2, // All + ]); + $this->assertTrue($result); + + $conf = Config::getCurrentConfig(); + + // Create two groups + $group1 = new \Group(); + $result = $group1->add( + [ + 'name' => 'Test group 1', + ], + ); + $this->assertNotFalse($result); + + $group2 = new \Group(); + $result = $group2->add( + [ + 'name' => 'Test group 2', + ], + ); + $this->assertNotFalse($result); + + // Get the user tech + $user = new \User(); + $this->assertTrue($user->getFromDBByCrit( + [ + 'name' => 'tech', + ], + )); + + // Assign the user to the group + $group_user = new \Group_User(); + $result = $group_user->add( + [ + 'groups_id' => $group1->getID(), + 'users_id' => $user->getID(), + ], + ); + $this->assertNotFalse($result); + + $result = $group_user->add( + [ + 'groups_id' => $group2->getID(), + 'users_id' => $user->getID(), + ], + ); + $this->assertNotFalse($result); + + //Create a ticket + $ticket = new \Ticket(); + $tid = $ticket->add( + [ + 'name' => 'Test ticket', + 'content' => 'Test content', + ], + ); + $this->assertNotFalse($ticket->getID()); + + $uticket = new \Ticket_User(); + $this->assertNotFalse($uticket->add( + [ + 'tickets_id' => $tid, + 'users_id' => $user->getID(), + 'type' => \Ticket_User::ASSIGN, + ], + )); + + // Check if the group of the requester is in the actors + $ticket_group = new \Group_Ticket(); + $groups = $ticket_group->find(['tickets_id' => $ticket->getID()]); + $this->assertCount(2, $groups); + + // Setup to take only the main group of the technician + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'take_technician_group_ticket' => 1, // Default + ]); + $this->assertTrue($result); + + $conf = Config::getCurrentConfig(); + + //Create a ticket + $ticket = new \Ticket(); + $tid = $ticket->add( + [ + 'name' => 'Test ticket tech group - 2', + 'content' => 'Test content', + ], + ); + $this->assertNotFalse($ticket->getID()); + + //Add default group to the user + $user2 = new \User(); + $this->assertTrue($user2->update( + [ + 'id' => $user->getID(), + 'groups_id' => $group1->getID(), + ], + )); + + $uticket = new \Ticket_User(); + $this->assertNotFalse($uticket->add( + [ + 'tickets_id' => $tid, + 'users_id' => $user2->getID(), + 'type' => \Ticket_User::ASSIGN, + ], + )); + + // Check if the group of the requester is in the actors + $ticket_group = new \Group_Ticket(); + $groups = $ticket_group->find(['tickets_id' => $ticket->getID()]); + $this->assertCount(1, $groups); + } + + /** + * Test take the item groups + */ + public function testTakeItemGroups(): void + { + $conf = $this->getCurrentConfig(); + + // Setup to take the groups of the items + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'take_item_group_ticket' => 1, + ]); + $this->assertTrue($result); + + $conf = Config::getCurrentConfig(); + + // Create two groups + $group1 = new \Group(); + $result = $group1->add( + [ + 'name' => 'Test group 1', + ], + ); + $this->assertNotFalse($result); + + //Create item computer + $computer = new \Computer(); + $cid = $computer->add( + [ + 'name' => 'Test computer', + ], + ); + $this->assertNotFalse($cid); + + //Create item ticket + $group_item = new \Group_Item(); + $this->assertNotFalse($group_item->add( + [ + 'items_id' => $computer->getID(), + 'itemtype' => \Computer::class, + 'groups_id' => $group1->getID(), + 'type' => 1, + ], + )); + + //Create a ticket + $ticket = new \Ticket(); + $tid = $ticket->add( + [ + 'name' => 'Test ticket item groups', + 'content' => 'Test content', + ], + ); + $this->assertGreaterThan(0, $tid); + + // Assign the computer to the ticket + $item_ticket = new \Item_Ticket(); + $this->assertNotFalse($item_ticket->add( + [ + 'tickets_id' => $tid, + 'items_id' => $computer->getID(), + 'itemtype' => \Computer::class, + ], + )); + + // Check if the groups are in the actors + $ticket_group = new \Group_Ticket(); + $groups = $ticket_group->find( + [ + 'tickets_id' => $ticket->getID(), + 'type' => \CommonITILActor::ASSIGN, + ], + ); + $this->assertCount(1, $groups); + } + + /** + * Test automatic assignment of technical manager and group when updating ticket category + */ + public function testUpdateTicketActorsOnCategoryChange(): void + { + $this->login(); + + $conf = $this->getCurrentConfig(); + + // Configure to assign technical manager and group when changing category + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'assign_technical_manager_when_changing_category_ticket' => 1, + 'assign_technical_group_when_changing_category_ticket' => 1, + ]); + $this->assertTrue($result); + + // Create a group for the category + $group = new \Group(); + $gid = $group->add([ + 'name' => 'Test Technical Group', + ]); + $this->assertNotFalse($gid); + + // Create a user for technical manager + $user = new \User(); + $uid = $user->add([ + 'name' => 'test_tech_manager', + 'login' => 'test_tech_manager', + ]); + $this->assertNotFalse($uid); + + // Create a category with technical manager and group + $category = new \ITILCategory(); + $cid = $category->add([ + 'name' => 'Test Category with Tech', + 'users_id' => $uid, + 'groups_id' => $gid, + ]); + $this->assertNotFalse($cid); + + // Create a ticket + $ticket = new \Ticket(); + $tid = $ticket->add([ + 'name' => 'Test ticket category update', + 'content' => 'Test content', + ]); + $this->assertGreaterThan(0, $tid); + + // Update ticket with the category + $ticket = new \Ticket(); + $this->assertTrue($ticket->update([ + 'id' => $tid, + 'itilcategories_id' => $cid, + ])); + + // Check if technical manager was assigned + $ticket_user = new \Ticket_User(); + $assigned_users = $ticket_user->find([ + 'tickets_id' => $tid, + 'users_id' => $uid, + 'type' => \CommonITILActor::ASSIGN, + ]); + $this->assertCount(1, $assigned_users); + + // Check if technical group was assigned + $ticket_group = new \Group_Ticket(); + $assigned_groups = $ticket_group->find([ + 'tickets_id' => $tid, + 'groups_id' => $gid, + 'type' => \CommonITILActor::ASSIGN, + ]); + $this->assertCount(1, $assigned_groups); + + // Reset config + $resetResult = $this->updateTestConfig($conf, [ + 'assign_technical_manager_when_changing_category_ticket' => 0, + 'assign_technical_group_when_changing_category_ticket' => 0, + ]); + $this->assertTrue($resetResult); + } + + /** + * Test automatic assignment of technical manager and group when updating change category + */ + public function testUpdateChangeActorsOnCategoryChange(): void + { + $this->login(); + + $conf = $this->getCurrentConfig(); + + // Configure to assign technical manager and group when changing category + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'assign_technical_manager_when_changing_category_change' => 1, + 'assign_technical_group_when_changing_category_change' => 1, + ]); + $this->assertTrue($result); + + // Create a group for the category + $group = new \Group(); + $gid = $group->add([ + 'name' => 'Test Technical Group Change', + ]); + $this->assertNotFalse($gid); + + // Create a user for technical manager + $user = new \User(); + $uid = $user->add([ + 'name' => 'test_tech_manager_change', + 'login' => 'test_tech_manager_change', + ]); + $this->assertNotFalse($uid); + + // Create a category with technical manager and group + $category = new \ITILCategory(); + $cid = $category->add([ + 'name' => 'Test Category with Tech Change', + 'users_id' => $uid, + 'groups_id' => $gid, + ]); + $this->assertNotFalse($cid); + + // Create a change + $change = new \Change(); + $chid = $change->add([ + 'name' => 'Test change category update', + 'content' => 'Test content', + ]); + $this->assertGreaterThan(0, $chid); + + // Update change with the category + $change = new \Change(); + $this->assertTrue($change->update([ + 'id' => $chid, + 'itilcategories_id' => $cid, + ])); + + // Check if technical manager was assigned + $change_user = new \Change_User(); + $assigned_users = $change_user->find([ + 'changes_id' => $chid, + 'users_id' => $uid, + 'type' => \CommonITILActor::ASSIGN, + ]); + $this->assertCount(1, $assigned_users); + + // Check if technical group was assigned + $change_group = new \Change_Group(); + $assigned_groups = $change_group->find([ + 'changes_id' => $chid, + 'groups_id' => $gid, + 'type' => \CommonITILActor::ASSIGN, + ]); + $this->assertCount(1, $assigned_groups); + + // Reset config + $resetResult = $this->updateTestConfig($conf, [ + 'assign_technical_manager_when_changing_category_change' => 0, + 'assign_technical_group_when_changing_category_change' => 0, + ]); + $this->assertTrue($resetResult); + } + + /** + * Test automatic assignment of technical manager and group when updating problem category + */ + public function testUpdateProblemActorsOnCategoryChange(): void + { + $this->login(); + + $conf = $this->getCurrentConfig(); + + // Configure to assign technical manager and group when changing category + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'assign_technical_manager_when_changing_category_problem' => 1, + 'assign_technical_group_when_changing_category_problem' => 1, + ]); + $this->assertTrue($result); + + // Create a group for the category + $group = new \Group(); + $gid = $group->add([ + 'name' => 'Test Technical Group Problem', + ]); + $this->assertNotFalse($gid); + + // Create a user for technical manager + $user = new \User(); + $uid = $user->add([ + 'name' => 'test_tech_manager_problem', + 'login' => 'test_tech_manager_problem', + ]); + $this->assertNotFalse($uid); + + // Create a category with technical manager and group + $category = new \ITILCategory(); + $cid = $category->add([ + 'name' => 'Test Category with Tech Problem', + 'users_id' => $uid, + 'groups_id' => $gid, + ]); + $this->assertNotFalse($cid); + + // Create a problem + $problem = new \Problem(); + $pid = $problem->add([ + 'name' => 'Test problem category update', + 'content' => 'Test content', + ]); + $this->assertGreaterThan(0, $pid); + + // Update problem with the category + $problem = new \Problem(); + $this->assertTrue($problem->update([ + 'id' => $pid, + 'itilcategories_id' => $cid, + ])); + + // Check if technical manager was assigned + $problem_user = new \Problem_User(); + $assigned_users = $problem_user->find([ + 'problems_id' => $pid, + 'users_id' => $uid, + 'type' => \CommonITILActor::ASSIGN, + ]); + $this->assertCount(1, $assigned_users); + + // Check if technical group was assigned + $problem_group = new \Group_Problem(); + $assigned_groups = $problem_group->find([ + 'problems_id' => $pid, + 'groups_id' => $gid, + 'type' => \CommonITILActor::ASSIGN, + ]); + $this->assertCount(1, $assigned_groups); + + // Reset config + $resetResult = $this->updateTestConfig($conf, [ + 'assign_technical_manager_when_changing_category_problem' => 0, + 'assign_technical_group_when_changing_category_problem' => 0, + ]); + $this->assertTrue($resetResult); + } + + /** + * Test that actors are not assigned when configuration is disabled + */ + public function testUpdateActorsDisabledConfiguration(): void + { + $this->login(); + + $conf = $this->getCurrentConfig(); + + // Ensure configuration is disabled + $result = $this->updateTestConfig($conf, [ + 'is_active' => 1, + 'entities_id' => 0, + 'assign_technical_manager_when_changing_category_ticket' => 0, + 'assign_technical_group_when_changing_category_ticket' => 0, + ]); + $this->assertTrue($result); + + // Create a category with technical manager and group + $group = new \Group(); + $gid = $group->add(['name' => 'Test Group Disabled']); + $this->assertNotFalse($gid); + + $user = new \User(); + $uid = $user->add(['name' => 'test_user_disabled', 'login' => 'test_user_disabled']); + $this->assertNotFalse($uid); + + $category = new \ITILCategory(); + $cid = $category->add([ + 'name' => 'Test Category Disabled', + 'users_id' => $uid, + 'groups_id' => $gid, + ]); + $this->assertNotFalse($cid); + + // Create and update a ticket + $ticket = new \Ticket(); + $tid = $ticket->add(['name' => 'Test disabled config', 'content' => 'Test content']); + $this->assertGreaterThan(0, $tid); + + $ticket = new \Ticket(); + $this->assertTrue($ticket->update(['id' => $tid, 'itilcategories_id' => $cid])); + + // Verify no technical actors were assigned + $ticket_user = new \Ticket_User(); + $assigned_users = $ticket_user->find([ + 'tickets_id' => $tid, + 'users_id' => $uid, + 'type' => \CommonITILActor::ASSIGN, + ]); + $this->assertCount(0, $assigned_users); + + $ticket_group = new \Group_Ticket(); + $assigned_groups = $ticket_group->find([ + 'tickets_id' => $tid, + 'groups_id' => $gid, + 'type' => \CommonITILActor::ASSIGN, + ]); + $this->assertCount(0, $assigned_groups); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 45cfeb7..fc5fa92 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -31,8 +31,17 @@ * ------------------------------------------------------------------------- */ -require __DIR__ . '/../../../phpunit/bootstrap.php'; +use Glpi\Application\Environment; +use Glpi\Kernel\Kernel; -if (!Plugin::isPluginActive("moreoptions")) { - throw new RuntimeException("Plugin moreoptions is not active in the test database"); -} +use function Safe\define; + +define('GLPI_LOG_DIR', __DIR__ . '/files/_logs'); + +require_once __DIR__ . '/../../../phpunit/GLPITestCase.php'; +require_once __DIR__ . '/../../../phpunit/DbTestCase.php'; +require_once __DIR__ . '/../../../vendor/autoload.php'; +require_once __DIR__ . '/MoreOptionsTestCase.php'; + +$kernel = new Kernel(Environment::TESTING->value); +$kernel->boot();