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 @@
| {{ __('Technician') }} | -{{ __('Technicians group') }} | -{{ __('Category') }} | -{{ __('Location') }} | -{{ __('Solution') }} | ++ | {{ __('Ticket') }} | +{{ __('Change') }} | +{{ __('Problem') }} | +{{ __('All') }} |
|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | + | {{ __('Technician') }} | ++ | + | + | + |
| {{ __('Technicians group') }} | ++ | + | + | + | |||||
| {{ __('Category') }} | ++ | + | + | + | |||||
| {{ __('Location') }} | ++ | + | + | + | |||||
| {{ __('Solution') }} | ++ | + | + |