diff --git a/.gitignore b/.gitignore index 283db0af..2e847868 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist/ vendor/ .gh_token *.min.* +*.cache diff --git a/composer.json b/composer.json index 4a460f31..e7e8aee1 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "php-parallel-lint/php-parallel-lint": "^1.4", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.1", - "phpstan/phpstan-deprecation-rules": "^2.0" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, "config": { "optimize-autoloader": true, diff --git a/composer.lock b/composer.lock index e63266fb..91685089 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "edee4b40b4a23f0b5146a771fc5e5322", + "content-hash": "aa2b1621d126bd003fdb3efcc09d4d24", "packages": [ { "name": "symfony/deprecation-contracts", @@ -75,7 +75,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -134,7 +134,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -519,6 +519,76 @@ ], "time": "2024-05-06T16:37:16+00:00" }, + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, { "name": "evenement/evenement", "version": "v3.0.2", @@ -788,16 +858,16 @@ }, { "name": "glpi-project/tools", - "version": "0.7.4", + "version": "0.7.5", "source": { "type": "git", "url": "https://github.com/glpi-project/tools.git", - "reference": "65a09a93350da6fa67d423dd94e4cb4023a17e20" + "reference": "c6ff4a7640384232ead150b46d4a647a14d12ab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/glpi-project/tools/zipball/65a09a93350da6fa67d423dd94e4cb4023a17e20", - "reference": "65a09a93350da6fa67d423dd94e4cb4023a17e20", + "url": "https://api.github.com/repos/glpi-project/tools/zipball/c6ff4a7640384232ead150b46d4a647a14d12ab3", + "reference": "c6ff4a7640384232ead150b46d4a647a14d12ab3", "shasum": "" }, "require": { @@ -840,7 +910,243 @@ "issues": "https://github.com/glpi-project/tools/issues", "source": "https://github.com/glpi-project/tools" }, - "time": "2024-09-18T06:58:02+00:00" + "time": "2025-05-22T07:31:28+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-04-29T12:36:36+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" }, { "name": "php-parallel-lint/php-parallel-lint", @@ -953,16 +1259,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.13", + "version": "2.1.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e55e03e6d4ac49cd1240907e5b08e5cd378572a9" + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e55e03e6d4ac49cd1240907e5b08e5cd378572a9", - "reference": "e55e03e6d4ac49cd1240907e5b08e5cd378572a9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8ca5f79a8f63c49b2359065832a654e1ec70ac30", + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30", "shasum": "" }, "require": { @@ -1007,25 +1313,25 @@ "type": "github" } ], - "time": "2025-04-27T12:28:25+00:00" + "time": "2025-03-24T13:45:00+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "9d8e7d4e32711715ad78a1fb6ec368df9af01fdf" + "reference": "468e02c9176891cc901143da118f09dc9505fc2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/9d8e7d4e32711715ad78a1fb6ec368df9af01fdf", - "reference": "9d8e7d4e32711715ad78a1fb6ec368df9af01fdf", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.13" + "phpstan/phpstan": "^2.1.15" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -1052,9 +1358,9 @@ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.2" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.1" }, - "time": "2025-04-26T19:59:57+00:00" + "time": "2024-11-28T21:56:36+00:00" }, { "name": "psr/container", @@ -1482,279 +1788,1185 @@ "homepage": "https://cboden.dev/" } ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", - "keywords": [ - "asynchronous", - "event-loop" - ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2023-11-13T13:48:05+00:00" + "time": "2020-10-26T13:14:26+00:00" }, { - "name": "react/promise", - "version": "v3.2.0", + "name": "sebastian/recursion-context", + "version": "4.0.5", "source": { "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { - "php": ">=7.1.0" + "php": ">=7.3" }, "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" + "phpunit/phpunit": "^9.3" }, "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\": "src/" + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" }, { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2024-05-24T10:39:05+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { - "name": "react/socket", - "version": "v1.16.0", + "name": "sebastian/resource-operations", + "version": "3.0.4", "source": { "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/dns": "^1.13", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.6 || ^1.2.1", - "react/stream": "^1.4" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3.3 || ^2", - "react/promise-stream": "^1.4", - "react/promise-timer": "^1.11" + "phpunit/phpunit": "^9.0" }, "type": "library", - "autoload": { - "psr-4": { - "React\\Socket\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", - "keywords": [ - "Connection", - "Socket", - "async", - "reactphp", - "stream" - ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.16.0" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2024-07-26T10:38:09+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { - "name": "react/stream", - "version": "v1.4.0", + "name": "sebastian/type", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.8", - "react/event-loop": "^1.2" + "php": ">=7.3" }, "require-dev": { - "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + "phpunit/phpunit": "^9.5" }, "type": "library", - "autoload": { - "psr-4": { - "React\\Stream\\": "src/" + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", - "keywords": [ - "event-driven", - "io", - "non-blocking", - "pipe", - "reactphp", - "readable", - "stream", - "writable" - ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.4.0" + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2024-06-11T12:45:25+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { - "name": "sebastian/diff", - "version": "4.0.6", + "name": "sebastian/version", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { "php": ">=7.3" }, - "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" - }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1769,24 +2981,15 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" }, "funding": [ { @@ -1794,7 +2997,7 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2020-09-28T06:39:44+00:00" }, { "name": "symfony/console", @@ -2260,7 +3463,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -2318,7 +3521,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -2338,7 +3541,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -2399,7 +3602,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -2419,19 +3622,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -2479,7 +3683,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -2495,11 +3699,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -2555,7 +3759,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0" }, "funding": [ { @@ -2575,16 +3779,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -2635,7 +3839,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -2651,11 +3855,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -2711,7 +3915,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" }, "funding": [ { @@ -3022,6 +4226,56 @@ ], "time": "2024-11-10T20:33:58+00:00" }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, { "name": "twig/twig", "version": "v3.11.3", @@ -3105,15 +4359,15 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=7.4" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.4.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/front/container.form.php b/front/container.form.php index 8a80410a..ce5441d8 100644 --- a/front/container.form.php +++ b/front/container.form.php @@ -56,6 +56,17 @@ } elseif (isset($_POST['update_fields_values'])) { $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $_POST['plugin_fields_containers_id']); if ($right > READ) { + $containerID = $_POST['plugin_fields_containers_id']; + $data = []; + foreach ($_REQUEST as $key => $value) { + // if key starts with plugin_fields__ remove the prefix + if (strpos($key, "plugin_fields_{$containerID}_") === 0) { + $new_key = substr($key, strlen("plugin_fields_{$containerID}_")); + $data[$new_key] = $value; + } else { + $data[$key] = $value; + } + } $container->updateFieldsValues($_REQUEST, $_REQUEST['itemtype'], false); } Html::back(); diff --git a/inc/container.class.php b/inc/container.class.php index d8cc983f..28d92335 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -557,22 +557,6 @@ public function prepareInputForAdd($input) $input['itemtypes'] = [$input['itemtypes']]; } - if ($input['type'] === 'dom') { - //check for already exist dom container with this itemtype - $found = $this->find(['type' => 'dom']); - if (count($found) > 0) { - foreach (array_column($found, 'itemtypes') as $founditemtypes) { - foreach (json_decode($founditemtypes) as $founditemtype) { - if (in_array($founditemtype, $input['itemtypes'])) { - Session::AddMessageAfterRedirect(__("You cannot add several blocks with type 'Insertion in the form' on same object", 'fields'), false, ERROR); - - return false; - } - } - } - } - } - if ($input['type'] === 'domtab') { //check for already exist domtab container with this itemtype on this tab $found = $this->find(['type' => 'domtab', 'subtype' => $input['subtype']]); @@ -1604,6 +1588,92 @@ public static function findContainer($itemtype, $type = 'tab', $subtype = '') return $id; } + /** + * Find containers for a specific itemtype, type, subtype and entity id + * + * @param string $itemtype Itemtype GLPI + * @param string $type Type of container (tab, dom, domtab) + * @param string $subtype + * @param integer $entityId Entity ID default is 0 (root entity) + * + * @return array List of container IDs + */ + public static function findContainers($itemtype, $type = 'tab', $subtype = '', $entityId = 0): array + { + /** @var DBmysql $DB */ + global $DB; + + if ($itemtype === '') { + return []; + } + + $entitiesIds = getAncestorsOf("glpi_entities", (string) $entityId); + $entitiesIds[] = $entityId; // Add entity active itself to the list + + $where = [ + 'is_active' => 1, + 'type' => $type, + new \QueryExpression("JSON_CONTAINS(itemtypes, " . $DB->quote('"' . $itemtype . '"') . ")"), + 'AND' => [ + 'OR' => [ + [ + 'is_recursive' => 1, + 'entities_id' => $entitiesIds, + ], + [ + 'is_recursive' => 0, + 'entities_id' => $entityId, + ], + ], + ], + ]; + + if ($subtype !== '') { + if ($subtype === $itemtype . '$main') { + $where['type'] = 'dom'; + } else { + $where['type'] = ['!=', 'dom']; + $where['subtype'] = $subtype; + } + } else { + $where['type'] = $type; + } + + $entityRestriction = getEntitiesRestrictCriteria('', '', $entityId, true, true); + if (!empty($entityRestriction)) { + $allowedEntities = []; + foreach ($entityRestriction as $restriction) { + if (isset($restriction['entities_id']) && is_array($restriction['entities_id'])) { + $allowedEntities = array_merge($allowedEntities, $restriction['entities_id']); + } + } + if (!empty($allowedEntities)) { + $where['entities_id'] = $allowedEntities; + } + } + + $iterator = $DB->request([ + 'SELECT' => 'id', + 'FROM' => self::getTable(), + 'WHERE' => $where, + ]); + + $ids = []; + foreach ($iterator as $row) { + $containerId = (int) $row['id']; + + if (isset($_SESSION['glpiactiveprofile']['id'])) { + $profileId = $_SESSION['glpiactiveprofile']['id']; + if (PluginFieldsProfile::getRightOnContainer($profileId, $containerId) < READ) { + continue; + } + } + $ids[] = $containerId; + } + + return $ids; + } + /** * Post item hook for add * Do store data in db @@ -1614,18 +1684,20 @@ public static function findContainer($itemtype, $type = 'tab', $subtype = '') */ public static function postItemAdd(CommonDBTM $item) { - if (array_key_exists('_plugin_fields_data', $item->input)) { - $data = $item->input['_plugin_fields_data']; - $data['items_id'] = $item->getID(); - $data['entities_id'] = $item->isEntityAssign() ? $item->getEntityID() : 0; - //update data - $container = new self(); - if ($container->updateFieldsValues($data, $item->getType(), isset($_REQUEST['massiveaction']))) { - return true; - } + if (array_key_exists('_plugin_fields_data_multi', $item->input)) { + $dataMulti = $item->input['_plugin_fields_data_multi']; + foreach ($dataMulti as $data) { + $data['items_id'] = $item->getID(); + $data['entities_id'] = $item->isEntityAssign() ? $item->getEntityID() : 0; + //update data + $container = new self(); + if ($container->updateFieldsValues($data, $item->getType(), isset($_REQUEST['massiveaction']))) { + continue; + }; - $item->input = []; - return $item; + $item->input = []; + continue; + } } return true; @@ -1642,23 +1714,22 @@ public static function postItemAdd(CommonDBTM $item) public static function preItemUpdate(CommonDBTM $item) { self::preItem($item); - if (array_key_exists('_plugin_fields_data', $item->input)) { - $data = $item->input['_plugin_fields_data']; - $data['entities_id'] = $item->isEntityAssign() ? $item->getEntityID() : 0; - //update data - $container = new self(); - if ( - count($data) == 0 - || $container->updateFieldsValues($data, $item->getType(), isset($_REQUEST['massiveaction'])) - ) { - $item->input['date_mod'] = $_SESSION['glpi_currenttime']; - - return true; + if (array_key_exists('_plugin_fields_data_multi', $item->input)) { + $dataMulti = $item->input['_plugin_fields_data_multi']; + foreach ($dataMulti as $data) { + $data['entities_id'] = $item->isEntityAssign() ? $item->getEntityID() : 0; + //update data + $container = new self(); + if ( + count($data) == 0 + || $container->updateFieldsValues($data, $item->getType(), isset($_REQUEST['massiveaction'])) + ) { + $item->input['date_mod'] = $_SESSION['glpi_currenttime']; + continue; + } + continue; } - - return false; } - return true; } @@ -1672,69 +1743,57 @@ public static function preItemUpdate(CommonDBTM $item) */ public static function preItem(CommonDBTM $item) { - //find container (if not exist, do nothing) - if (isset($item->input['c_id'])) { - $c_id = $item->input['c_id']; - } elseif (isset($_REQUEST['c_id'])) { - $c_id = $_REQUEST['c_id']; - } else { - $type = 'dom'; - if (isset($_REQUEST['_plugin_fields_type'])) { - $type = $_REQUEST['_plugin_fields_type']; - } - $subtype = ''; - if ($type == 'domtab') { - $subtype = $_REQUEST['_plugin_fields_subtype']; - } - if (false === ($c_id = self::findContainer(get_Class($item), $type, $subtype))) { - // tries for 'tab' - if (false === ($c_id = self::findContainer(get_Class($item)))) { - return false; - } - } - } + $type = $_REQUEST['_plugin_fields_type'] ?? 'dom'; + $subtype = ($type === 'domtab') ? ($_REQUEST['_plugin_fields_subtype'] ?? '') : ''; - $loc_c = new PluginFieldsContainer(); - $loc_c->getFromDB($c_id); + $itemEntityId = $item->getEntityID(); + $entityId = ($itemEntityId === -1) ? ($_SESSION['glpiactive_entity'] ?? 0) : $itemEntityId; - // check rights on $c_id + $containers = self::findContainers($item->getType(), $type, $subtype, $entityId); - if (isset($_SESSION['glpiactiveprofile']['id']) && $_SESSION['glpiactiveprofile']['id'] != null && $c_id > 0) { - $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $c_id); - if (($right > READ) === false) { - return false; - } - } else { - return false; - } + $all_data = []; + foreach ($containers as $c_id) { - // need to check if container is usable on this object entity - $entities = [$loc_c->fields['entities_id']]; - if ($loc_c->fields['is_recursive']) { - $entities = getSonsOf(getTableForItemType('Entity'), $loc_c->fields['entities_id']); - } + // check rights on $c_id + if (isset($_SESSION['glpiactiveprofile']['id'])) { + $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $c_id); + if ($right < READ) { + continue; // insufficient rights + } + } - if (count($item->fields) === 0) { - $item->fields = $item->input; - } + $loc_c = new self(); + $loc_c->getFromDB($c_id); - if ($item->isEntityAssign() && !in_array($item->getEntityID(), $entities)) { - return false; - } + // need to check if container is usable on this object entity + $entities = [$loc_c->fields['entities_id']]; + if ($loc_c->fields['is_recursive']) { + $entities = getSonsOf(getTableForItemType('Entity'), $loc_c->fields['entities_id']); + } - if (false !== ($data = self::populateData($c_id, $item))) { - if (self::validateValues($data, $item::getType(), isset($_REQUEST['massiveaction'])) === false) { - $item->input = []; + if (count($item->fields) === 0) { + $item->fields = $item->input; + } - return false; + if ($item->isEntityAssign() && !in_array($item->getEntityID(), $entities)) { + continue; // not the right entity } - $item->input['_plugin_fields_data'] = $data; - return true; + if (false !== ($data = self::populateData($c_id, $item))) { + if (!self::validateValues($data, $item->getType(), isset($_REQUEST['massiveaction']))) { + // if validation fails, we need to remove the data from the item input + $item->input = []; + + return false; + } + $all_data[] = $data; + } } - return false; + $item->input['_plugin_fields_data_multi'] = $all_data; + + return true; } /** @@ -1745,7 +1804,7 @@ public static function preItem(CommonDBTM $item) * * @return array|false */ - private static function populateData($c_id, CommonDBTM $item) + public static function populateData($c_id, CommonDBTM $item) { //find fields associated to found container $field_obj = new PluginFieldsField(); @@ -1758,103 +1817,105 @@ private static function populateData($c_id, CommonDBTM $item) ); //prepare data to update - $data = ['plugin_fields_containers_id' => $c_id]; - if (!$item->isNewItem()) { - //no ID yet while creating - $data['items_id'] = $item->getID(); - } + $data = [ + 'plugin_fields_containers_id' => $c_id, + 'items_id' => $item->getID(), + 'itemtype' => $item->getType(), + 'entities_id' => $item->getEntityID(), + ]; // Add status so it can be used with status overrides - $status_field_name = PluginFieldsStatusOverride::getStatusFieldName($item->getType()); - $data[$status_field_name] = null; - if (array_key_exists($status_field_name, $item->input) && $item->input[$status_field_name] !== '') { - $data[$status_field_name] = (int) $item->input[$status_field_name]; - } elseif (array_key_exists($status_field_name, $item->fields) && $item->fields[$status_field_name] !== '') { - $data[$status_field_name] = (int) $item->fields[$status_field_name]; - } + $statusField = PluginFieldsStatusOverride::getStatusFieldName($item->getType()); + $data[$statusField] = $item->input[$statusField] ?? $item->fields[$statusField] ?? null; $has_fields = false; + // Prefix for input names + $prefix = "plugin_fields_{$c_id}_"; + foreach ($fields as $field) { + $base_name = $field['name']; + $isMulti = (bool) $field['multiple']; if ($field['type'] == 'glpi_item') { - $itemtype_key = sprintf('itemtype_%s', $field['name']); - $items_id_key = sprintf('items_id_%s', $field['name']); + $itemtype_key = "itemtype_{$base_name}"; + $items_id_key = "items_id_{$base_name}"; if (!isset($item->input[$itemtype_key], $item->input[$items_id_key])) { continue; // not a valid input } - $has_fields = true; - $data[$itemtype_key] = $item->input[$itemtype_key]; - $data[$items_id_key] = $item->input[$items_id_key]; + $data[$itemtype_key] = $item->input[$itemtype_key]; + $data[$items_id_key] = $item->input[$items_id_key]; + $has_fields = true; continue; // bypass unique field handling } - if (isset($item->input[$field['name']])) { - //standard field - $input = $field['name']; - } else { - //dropdown field - $input = 'plugin_fields_' . $field['name'] . 'dropdowns_id'; - } - if (isset($item->input[$input])) { - $has_fields = true; - // Before is_number check, help user to have a number correct, during a massive action of a number field - if ($field['type'] == 'number') { - $item->input[$input] = str_replace(',', '.', $item->input[$input]); + // For other fields, the input name to be prefixed with "plugin_fields_{$c_id}_" and the field name + // "plugin_fields_{$c_id}_{$base_name}" + if ($field['type'] === 'dropdown') { + // For dropdown fields, the input name is "plugin_fields_{$c_id}_{$base_name}dropdowns_id" + $htmlKeyWithId = $prefix . $base_name . "dropdowns_id"; // html key in POST data with id + $htmlKeyNoId = "plugin_fields_{$base_name}dropdowns_id"; // html key in POST data without id + $colKey = 'plugin_fields_' . $base_name . 'dropdowns_id'; // column key in DB + + if (array_key_exists($htmlKeyWithId, $item->input)) { + $data[$colKey] = $item->input[$htmlKeyWithId]; + $has_fields = true; + } elseif (array_key_exists($htmlKeyNoId, $item->input)) { + $data[$colKey] = $item->input[$htmlKeyNoId]; + $has_fields = true; + } elseif ($isMulti) { + $definedKeyWithId = '_' . $htmlKeyWithId . '_defined'; + $definedKeyNoId = '_' . $htmlKeyNoId . '_defined'; + if (!empty($item->input[$definedKeyWithId]) || !empty($item->input[$definedKeyNoId])) { + $data[$colKey] = []; + $has_fields = true; + } } - $data[$input] = $item->input[$input]; + continue; + } - if ($field['type'] === 'richtext') { - $filename_input = sprintf('_%s', $input); - $prefix_input = sprintf('_prefix_%s', $input); - $tag_input = sprintf('_tag_%s', $input); + // For fields standard, the input name is "plugin_fields_{$c_id}_{$base_name}" + $htmlKeyWithId = $prefix . $base_name; + $htmlKeyNoId = "plugin_fields_{$base_name}"; - $data[$filename_input] = $item->input[$filename_input] ?? []; - $data[$prefix_input] = $item->input[$prefix_input] ?? []; - $data[$tag_input] = $item->input[$tag_input] ?? []; + $valuePresent = false; + $value = null; + if (array_key_exists($htmlKeyWithId, $item->input)) { + $value = $item->input[$htmlKeyWithId]; + $valuePresent = true; + } elseif (array_key_exists($htmlKeyNoId, $item->input)) { + $value = $item->input[$htmlKeyNoId]; + $valuePresent = true; + } elseif ($isMulti) { + $definedKeyWithId = '_' . $htmlKeyWithId . '_defined'; + $definedKeyNoId = '_' . $htmlKeyNoId . '_defined'; + if (!empty($item->input[$definedKeyWithId]) || !empty($item->input[$definedKeyNoId])) { + $value = []; + $valuePresent = true; } - } else { - //the absence of the field in the input may be due to the fact that the input allows multiple selection - // ex my_dom[] - //in these conditions, the input is never sent by the browser - if ($field['multiple']) { - //handle multi dropdown field - if ($field['type'] == 'dropdown') { - $multiple_key = sprintf('plugin_fields_%sdropdowns_id', $field['name']); - $multiple_key_defined = '_' . $multiple_key . '_defined'; - //values are defined by user - if (isset($item->input[$multiple_key])) { - $data[$multiple_key] = $item->input[$multiple_key]; - $has_fields = true; - } elseif ( - isset($item->input[$multiple_key_defined]) - && $item->input[$multiple_key_defined] - ) { //multi dropdown is empty or has been emptied - $data[$multiple_key] = []; - $has_fields = true; - } - } + } - //managed multi GLPI item dropdown field - if (preg_match('/^dropdown-(?.+)$/', $field['type'], $match) === 1) { - //values are defined by user - if (isset($item->input[$field['name']])) { - $data[$field['name']] = $item->input[$field['name']]; - $has_fields = true; - } else { //multi dropdown is empty or has been emptied - $data[$field['name']] = []; - } - } + if (!$valuePresent) { + continue; // not a valid input + } + + if ($field['type'] === 'number') { + $value = str_replace(',', '.', $value); + } + + $data[$base_name] = $value; + $has_fields = true; + + // If the field is a richtext + if ($field['type'] === 'richtext') { + foreach (['_' . $htmlKeyWithId, '_prefix_' . $htmlKeyWithId, '_tag_' . $htmlKeyWithId] as $extra) { + $data[$extra] = $item->input[$extra]; } } } - if ($has_fields === true) { - return $data; - } else { - return false; - } + return $has_fields ? $data : false; } public static function getAddSearchOptions($itemtype, $containers_id = false) diff --git a/inc/field.class.php b/inc/field.class.php index 4f3a4d92..539e26fd 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -871,8 +871,13 @@ public static function showDomContainer($id, $item, $type = 'dom', $subtype = '' $fields = []; } + $fieldTypeName = '_plugin_fields_type_' . $id; + $fieldSubTypeName = '_plugin_fields_subtype_' . $id; + echo Html::hidden('_plugin_fields_type', ['value' => $type]); echo Html::hidden('_plugin_fields_subtype', ['value' => $subtype]); + echo Html::hidden($fieldTypeName, ['value' => $type]); + echo Html::hidden($fieldSubTypeName, ['value' => $subtype]); echo self::prepareHtmlFields($fields, $item, true, true, false, $field_options); } @@ -904,105 +909,85 @@ public static function showForTab($params) $subtype = ''; } - //find container (if not exist, do nothing) - if (isset($_REQUEST['c_id'])) { - $c_id = $_REQUEST['c_id']; - } elseif (!$c_id = PluginFieldsContainer::findContainer(get_Class($item), $type, $subtype)) { - return; - } + $itemEntityId = $item->getEntityID(); + $entityId = ($itemEntityId === -1) ? ($_SESSION['glpiactive_entity'] ?? 0) : $itemEntityId; - $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $c_id); - if ($right < READ) { - return; - } + $container_ids = PluginFieldsContainer::findContainers(get_class($item), $type, $subtype, $entityId); - Html::requireJs('tinymce'); + foreach ($container_ids as $container_id) { - //need to check if container is usable on this object entity - $loc_c = new PluginFieldsContainer(); - $loc_c->getFromDB($c_id); - $entities = [$loc_c->fields['entities_id']]; - if ($loc_c->fields['is_recursive']) { - $entities = getSonsOf(getTableForItemType('Entity'), $loc_c->fields['entities_id']); - } - - if ($item->isEntityAssign()) { - $current_entity = $item->getEntityID(); - if (!in_array($current_entity, $entities)) { - return; + $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $container_id); + if ($right < READ) { + continue; } - } - - //parse REQUEST_URI - if (!isset($_SERVER['REQUEST_URI'])) { - return; - } - $current_url = $_SERVER['REQUEST_URI']; - if ( - strpos($current_url, '.form.php') === false - && strpos($current_url, '.injector.php') === false - && strpos($current_url, '.public.php') === false - && strpos($current_url, 'ajax/timeline.php') === false // ITILSolution load from timeline - ) { - return; - } - //Retrieve dom container - $itemtypes = PluginFieldsContainer::getUsedItemtypes($type, true); + //parse REQUEST_URI + if (!isset($_SERVER['REQUEST_URI'])) { + continue; + } + $current_url = $_SERVER['REQUEST_URI']; + if ( + strpos($current_url, '.form.php') === false + && strpos($current_url, '.injector.php') === false + && strpos($current_url, '.public.php') === false + && strpos($current_url, 'ajax/timeline.php') === false // ITILSolution load from timeline + ) { + continue; + } - //if no dom containers defined for this itemtype, do nothing (in_array case insensitive) - if (!in_array(strtolower($item::getType()), array_map('strtolower', $itemtypes))) { - return; - } + //Retrieve dom container + $itemtypes = PluginFieldsContainer::getUsedItemtypes($type, true); + + $html_id = 'plugin_fields_container_' . $container_id; + $in_helpdesk = (strpos($current_url, 'helpdesk.public.php') !== false); + if ($in_helpdesk) { + echo "
"; + echo "
"; + $field_options = [ + 'label_class' => 'col-lg-3', + 'input_class' => 'col-lg-9', + ]; + } else { + echo "
"; + } + $display_condition = new PluginFieldsContainerDisplayCondition(); + if ($display_condition->computeDisplayContainer($item, $container_id)) { + self::showDomContainer( + $container_id, + $item, + $type, + $subtype, + $field_options ?? [], + ); + } - $html_id = 'plugin_fields_container_' . mt_rand(); - if (strpos($current_url, 'helpdesk.public.php') !== false) { - echo "
"; - echo "
"; - $field_options = [ - 'label_class' => 'col-lg-3', - 'input_class' => 'col-lg-9', - ]; - } else { - echo "
"; - } - $display_condition = new PluginFieldsContainerDisplayCondition(); - if ($display_condition->computeDisplayContainer($item, $c_id)) { - self::showDomContainer( - $c_id, - $item, - $type, - $subtype, - $field_options ?? [], - ); - } - if (strpos($current_url, 'helpdesk.public.php') !== false) { + if ($in_helpdesk) { + echo '
'; + } echo '
'; - } - echo '
'; - //JS to trigger any change and check if container need to be display or not - $ajax_url = Plugin::getWebDir('fields') . '/ajax/container.php'; - $items_id = !$item->isNewItem() ? $item->getID() : 0; - echo Html::scriptBlock( - <<isNewItem() ? $item->getID() : 0; + echo Html::scriptBlock( + << 0) { @@ -1045,31 +1030,32 @@ function(evt) { } ); - var refresh_timeout = null; - form.find('textarea').each( - function () { - const editor = tinymce.get(this.id); - if (editor !== null) { - editor.on( - 'change', - function(evt) { - if ($(evt.target.targetElm).closest('#{$html_id}').length > 0) { - return; // Do nothing if element is inside fields container - } - - if (refresh_timeout !== null) { - window.clearTimeout(refresh_timeout); + var refresh_timeout = null; + form.find('textarea').each( + function () { + const editor = tinymce.get(this.id); + if (editor !== null) { + editor.on( + 'change', + function(evt) { + if ($(evt.target.targetElm).closest('#{$html_id}').length > 0) { + return; // Do nothing if element is inside fields container + } + + if (refresh_timeout !== null) { + window.clearTimeout(refresh_timeout); + } + refresh_timeout = window.setTimeout(refreshContainer, 1000); } - refresh_timeout = window.setTimeout(refreshContainer, 1000); - } - ); + ); + } } - } - ); - } + ); + } + ); + JAVASCRIPT ); -JAVASCRIPT - ); + } } public static function prepareHtmlFields( diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..9e9e7e21 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,8 @@ + + + + ./tests/ + + \ No newline at end of file diff --git a/templates/fields.html.twig b/templates/fields.html.twig index b1a770f7..b02a3732 100644 --- a/templates/fields.html.twig +++ b/templates/fields.html.twig @@ -40,8 +40,9 @@ {% for field in fields %} + {% set cid = container.fields.id %} {% set type = field['type'] %} - {% set name = field['name'] %} + {% set name = 'plugin_fields_' ~ cid ~ '_' ~ field['name'] %} {% set label = field['label'] %} {% set value = item.input[name] ?: field['value'] %} {% set readonly = field['is_readonly'] %} @@ -107,9 +108,9 @@ {% set dropdown_options = dropdown_options|merge({'entity_sons': true}) %} {% endif %} {% if "dropdowns_id" in name %} - {% set dropdown_itemtype = call("getItemtypeForForeignKeyField", [name]) %} + {% set dropdown_itemtype = call("getItemtypeForForeignKeyField", ['plugin_fields_' ~ field['name'] ~ 'dropdowns_id']) %} {% else %} - {% set dropdown_itemtype = call("PluginFieldsDropdown::getClassname", [name]) %} + {% set dropdown_itemtype = call("PluginFieldsDropdown::getClassname", [field['name']]) %} {% endif %} {% set name_fk = call("getForeignKeyFieldForItemType", [dropdown_itemtype]) %} {{ macros.dropdownField(dropdown_itemtype, name_fk, value, label, field_options|merge(dropdown_options)) }} diff --git a/tests/PluginFieldsContainerTest.php b/tests/PluginFieldsContainerTest.php new file mode 100644 index 00000000..087e0eaa --- /dev/null +++ b/tests/PluginFieldsContainerTest.php @@ -0,0 +1,731 @@ +deleteAllContainers(); + + // Delete all created tickets + foreach ($this->createdTickets as $ticketId) { + $ticket = new Ticket(); + $ticket->delete(['id' => $ticketId]); + } + } + + /** + * * Delete all created containers and their associated fields and profiles + */ + private function deleteAllContainers(): void + { + // Delete all created containers + foreach ($this->createdContainers as $containerId) { + $fieldsProfile = new PluginFieldsProfile(); + $fieldsObj = new PluginFieldsField(); + $fieldscontainer = new PluginFieldsContainer(); + + $fieldsProfile->deleteByCriteria(['plugin_fields_containers_id' => $containerId]); + $fieldsObj->deleteByCriteria(['plugin_fields_containers_id' => $containerId]); + $fieldscontainer->delete(['id' => $containerId]); + } + + $this->createdContainers = []; + $this->createdTickets = []; + } + + /** + * * Add container + * @param array $input + * @return false|int + */ + private function addContainer(array $input): false|int + { + $container = new PluginFieldsContainer(); + $input['is_active'] = 1; + $containerId = $container->add($input, [], false); + if (is_int($containerId) && $containerId > 0) { + $this->createdContainers[] = $containerId; + } + return $containerId; + } + + /** + * * Get container by ID + * @param int $id + * @return PluginFieldsContainer + */ + private function getContainer(int $id): PluginFieldsContainer + { + $container = new PluginFieldsContainer(); + $container->getFromDB($id); + return $container; + } + + /** + * * Add field to container + * @param int $containerId + * @param string $fieldName + * @param string $type + * @return false|int + */ + private function addFieldToContainer(int $containerId, string $fieldName, string $type = 'text', bool $multiple = false): false|int + { + $field = new PluginFieldsField(); + $id = $field->add([ + 'name' => $fieldName, + 'label' => ucfirst($fieldName), + 'type' => $type, + 'plugin_fields_containers_id' => $containerId, + 'ranking' => 1, + 'default_value' => '', + 'is_active' => 1, + 'is_readonly' => 0, + 'mandatory' => 1, + 'multiple' => $multiple ? 1 : 0, + 'allowed_values' => null, + ]); + + $container = new PluginFieldsContainer(); + $container->getFromDB($containerId); + $className = PluginFieldsContainer::getClassname('Ticket', $container->fields['name']); + $className::addField($fieldName, $type, ['multiple' => $multiple]); + + return $id; + } + + /** + * * Add ticket + * @param array $input + * @return Ticket|false + */ + private function addTicket(array $input): Ticket|false + { + $ticket = new Ticket(); + $ticketId = $ticket->add($input); + if (is_int($ticketId)) { + $this->createdTickets[] = $ticketId; + } else { + return false; + } + return $this->getTicket($ticketId); + } + + /** + * * Get ticket by ID + * @param int $id + * @return Ticket + */ + private function getTicket(int $id): Ticket + { + $ticket = new Ticket(); + $ticket->getFromDB($id); + return $ticket; + } + + /** + * * Test adding and reading a container + */ + public function testAddAndReadContainer(): void + { + $containerOne = $this->addContainer([ + 'name' => 'testcontainerone', + 'label' => 'Test container 1', + 'itemtypes' => ['Computer', 'Ticket'], + 'type' => 'tab', + 'subtype' => null, + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->assertNotFalse($containerOne); + $this->assertIsInt($containerOne); + + $containerOne = $this->getContainer($containerOne); + + $this->assertSame('Test container 1', $containerOne->fields['label']); + $this->assertStringContainsString('Computer', $containerOne->fields['itemtypes']); + $this->assertStringContainsString('Ticket', $containerOne->fields['itemtypes']); + + $this->deleteAllContainers(); + } + + /** + * * Test find containers returns correct containers for given itemtype and entity + */ + public function testFindContainersReturnsCorrectContainersForGivenItemtypeAndEntity() + { + // Container A should be found + $idExactMatchContainer = $this->addContainer([ + 'name' => 'containerca', + 'label' => 'Container CA', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + // Container B should be found (recursive and same entity) + $idRecursiveParentContainer = $this->addContainer([ + 'name' => 'containercb', + 'label' => 'Container CB', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + // Container C should not be found (wrong entity) + $idWrongEntityContainer = $this->addContainer([ + 'name' => 'containercc', + 'label' => 'Container CC', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 1, + 'is_recursive' => 0, + ]); + + // Container D should not be found (recursive but wrong entity) + $idRecursiveWrongEntityContainer = $this->addContainer([ + 'name' => 'containercd', + 'label' => 'Container CD', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 1, + 'is_recursive' => 1, + ]); + + // Container E should not be found (wrong itemtype) + $idWrongItemtypeContainer = $this->addContainer([ + 'name' => 'containerce', + 'label' => 'Container CE', + 'itemtypes' => ['Computer'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + // Container F should be found (multiple itemtypes) + $idMultipleItemtypesContainer = $this->addContainer([ + 'name' => 'containercf', + 'label' => 'Container CF', + 'itemtypes' => ['Computer', 'Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + $containersIds = PluginFieldsContainer::findContainers('Ticket', 'dom', '', 0); + + $this->assertContains($idExactMatchContainer, $containersIds, 'Container A should be found'); + $this->assertContains($idRecursiveParentContainer, $containersIds, 'Container B should be found (recursive and same entity)'); + $this->assertContains($idMultipleItemtypesContainer, $containersIds, 'Container F should be found (multiple itemtypes)'); + + $this->assertNotContains($idWrongEntityContainer, $containersIds, 'Container C should not be found (wrong entity)'); + $this->assertNotContains($idRecursiveWrongEntityContainer, $containersIds, 'Container D should not be found (recursive but wrong entity)'); + $this->assertNotContains($idWrongItemtypeContainer, $containersIds, 'Container E should not be found (wrong itemtype)'); + + $this->deleteAllContainers(); + } + + public function testFindContainersWithSubtypeInExpectedFormat(): void + { + // Container with a subtype (itemtype "Entity", type "domtab" and subtype "Entity$1") should be found + $idValidSubtypeContainer = $this->addContainer([ + 'name' => 'containerentitytabone', + 'label' => 'Container Entity Tab 1', + 'itemtypes' => ['Entity'], + 'type' => 'domtab', + 'subtype' => 'Entity$1', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + // Container with a another subtype (itemtype "Entity", type "domtab" and subtype "Entity$2") should not be found + $idDifferentSubtypeContainer = $this->addContainer([ + 'name' => 'containerentitytabtwo', + 'label' => 'Container Entity Tab 2', + 'itemtypes' => ['Entity'], + 'type' => 'domtab', + 'subtype' => 'Entity$2', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + // Same subtype but itemtype is "Ticket" => should not be found + $idWrongItemtypeWithSubtype = $this->addContainer([ + 'name' => 'containerwrongitemtype', + 'label' => 'Container Wrong itemType', + 'itemtypes' => ['Ticket'], + 'type' => 'domtab', + 'subtype' => 'Entity$1', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + // Same subtype and itemtype, but wrong type "dom" => should not be found + $idWrongTypeWithValidSubtype = $this->addContainer([ + 'name' => 'containerwrongType', + 'label' => 'Container Wrong Type', + 'itemtypes' => ['Entity'], + 'type' => 'dom', + 'subtype' => 'Entity$1', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + $containersIds = PluginFieldsContainer::findContainers('Entity', 'domtab', 'Entity$1', 0); + + $this->assertIsArray($containersIds); + $this->assertNotEmpty($containersIds); + + $this->assertContains($idValidSubtypeContainer, $containersIds, 'Container with a subtype (itemtype "Entity", type "domtab" and subtype "Entity$1") should be found'); + + $this->assertNotContains($idDifferentSubtypeContainer, $containersIds, 'Container with a another subtype (itemtype "Entity", type "domtab" and subtype "Entity$2") should not be found'); + $this->assertNotContains($idWrongItemtypeWithSubtype, $containersIds, 'Same subtype but itemtype is "Ticket" => should not be found'); + $this->assertNotContains($idWrongTypeWithValidSubtype, $containersIds, 'Same subtype and itemtype, but wrong type "dom" => should not be found'); + + $this->deleteAllContainers(); + } + + public function testFindContainersConsidersRecursiveEntitiesFromChild(): void + { + // Container defined in parent entity (0) with recursion => should be visible from child + $idContainerParentRecursive = $this->addContainer([ + 'name' => 'containerparentrecursive', + 'label' => 'Container parent recursive', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'subtype' => null, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + // Container defined in parent entity (0) without recursion => should not be visible from child + $idContainerParentNotRecursive = $this->addContainer([ + 'name' => 'containerparentnotrecursive', + 'label' => 'Container parent not recursive', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'subtype' => null, + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + $containersIds = PluginFieldsContainer::findContainers('Ticket', 'dom', '', 1); + + $this->assertIsArray($containersIds); + $this->assertNotEmpty($containersIds); + + $this->assertContains($idContainerParentRecursive, $containersIds, 'Container defined in parent entity (0) with recursion => should be visible from child'); + $this->assertNotContains($idContainerParentNotRecursive, $containersIds, 'Container defined in parent entity (0) without recursion => should not be visible from child'); + $this->deleteAllContainers(); + } + + public function testParentCannotSeeChildRecursiveContainers(): void + { + // Container defined in child (1), recursive + $idContainerChildRecursive = $this->addContainer([ + 'name' => 'containerchildrecursive', + 'label' => 'Container child recursive', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'subtype' => null, + 'entities_id' => 1, + 'is_recursive' => 1, + ]); + + $containersIds = PluginFieldsContainer::findContainers('Ticket', 'dom', '', 0); + + $this->assertNotContains($idContainerChildRecursive, $containersIds, 'Container defined in child (1), recursive, should not be visible from parent (0)'); + $this->deleteAllContainers(); + } + + public function testPreItemHandlesMultipleValidContainers(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; + + $id1 = $this->addContainer([ + 'name' => 'container1', + 'label' => 'Container 1', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($id1, 'testfield1'); + + $id2 = $this->addContainer([ + 'name' => 'container2', + 'label' => 'Container 2', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + $this->addFieldToContainer($id2, 'testfield2'); + + $ticket = new Ticket(); + $ticket->input = [ + 'name' => 'Test ticket', + 'content' => 'Test content', + 'entities_id' => 0, + 'plugin_fields_' . $id1 . '_testfield1' => 'foo', + 'plugin_fields_' . $id2 . '_testfield2' => 'bar', + 'status' => 1, + ]; + + $preItemReturn = PluginFieldsContainer::preItem($ticket); + + $this->assertTrue($preItemReturn); + $this->assertArrayHasKey('_plugin_fields_data_multi', $ticket->input); + $this->assertCount(2, $ticket->input['_plugin_fields_data_multi']); + + $data = $ticket->input['_plugin_fields_data_multi']; + + $this->assertSame('foo', $data[0]['testfield1']); + $this->assertSame('bar', $data[1]['testfield2']); + $this->deleteAllContainers(); + } + + public function testPostItemAddHandlesMultipleContainers(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; + $_REQUEST['massiveaction'] = false; + + // add containers + fields + $containerId1 = $this->addContainer([ + 'name' => 'containerpostitemaddone', + 'label' => 'Container postItemAdd 1', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId1, 'field1'); + + $containerId2 = $this->addContainer([ + 'name' => 'containerpostitemaddtwo', + 'label' => 'Container postItemAdd 2', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId2, 'field2'); + + require_once GLPI_ROOT . "/files/_plugins/fields/inc/ticketcontainerpostitemaddone.class.php"; + require_once GLPI_ROOT . "/files/_plugins/fields/inc/ticketcontainerpostitemaddtwo.class.php"; + + // add a ticket + $ticket = $this->addTicket([ + 'name' => 'Test ticket postItemAdd', + 'content' => 'test content', + 'entities_id' => 0, + ]); + + // 3. Injecter les données comme si elles venaient du formulaire + $ticket->input += [ + '_plugin_fields_data_multi' => [ + [ + 'plugin_fields_containers_id' => $containerId1, + 'field1' => 'value1', + ], + [ + 'plugin_fields_containers_id' => $containerId2, + 'field2' => 'value2', + ], + ], + ]; + + // check if postItemAdd return true + $this->assertTrue(PluginFieldsContainer::postItemAdd($ticket)); + + // check if the data is correctly saved in database + $className1 = PluginFieldsContainer::getClassname('Ticket', 'containerpostitemaddone'); + $objClass1 = new $className1(); + $objClass1->getFromDBByCrit([ + 'items_id' => $ticket->getID(), + 'plugin_fields_containers_id' => $containerId1, + ]); + $this->assertEquals('value1', $objClass1->fields['field1']); + + $className2 = PluginFieldsContainer::getClassname('Ticket', 'containerpostitemaddtwo'); + $objClass2 = new $className2(); + $objClass2->getFromDBByCrit([ + 'items_id' => $ticket->getID(), + 'plugin_fields_containers_id' => $containerId2, + ]); + $this->assertEquals('value2', $objClass2->fields['field2']); + $this->deleteAllContainers(); + } + + public function testPreItemUpdateHandlesMultipleContainers(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; + $_REQUEST['massiveaction'] = false; + + // add containers + fields + $containerId1 = $this->addContainer([ + 'name' => 'containerupdateone', + 'label' => 'Container update 1', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId1, 'field1'); + + $containerId2 = $this->addContainer([ + 'name' => 'containerupdatetwo', + 'label' => 'Container update 2', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId2, 'field2'); + + require_once GLPI_ROOT . "/files/_plugins/fields/inc/ticketcontainerupdateone.class.php"; + require_once GLPI_ROOT . "/files/_plugins/fields/inc/ticketcontainerupdatetwo.class.php"; + + // add a ticket + $ticket = $this->addTicket([ + 'name' => 'Ticket to update', + 'content' => 'test content', + 'entities_id' => 0, + ]); + + $ticket->input['_plugin_fields_data_multi'] = [ + [ + 'plugin_fields_containers_id' => $containerId1, + 'field1' => 'value1', + ], + [ + 'plugin_fields_containers_id' => $containerId2, + 'field2' => 'value2', + ], + ]; + PluginFieldsContainer::postItemAdd($ticket); + + // update the fields + $ticket->input += [ + 'plugin_fields_' . $containerId1 . '_field1' => 'value1 updated', + 'plugin_fields_' . $containerId2 . '_field2' => 'value2 updated', + ]; + PluginFieldsContainer::preItemUpdate($ticket); + + // check if the data is correctly updated in database + $className1 = PluginFieldsContainer::getClassname('Ticket', 'containerupdateone'); + $valueField1 = (new $className1())->find(['items_id' => $ticket->getID(), 'plugin_fields_containers_id' => $containerId1]); + $this->assertSame('value1 updated', current($valueField1)['field1']); + + $className2 = PluginFieldsContainer::getClassname('Ticket', 'containerupdatetwo'); + $valueField2 = (new $className2())->find(['items_id' => $ticket->getID(), 'plugin_fields_containers_id' => $containerId2]); + $this->assertSame('value2 updated', current($valueField2)['field2']); + $this->deleteAllContainers(); + } + + public function testPopulateDataWithPrefix(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; + + // add a container with a field + $containerId = $this->addContainer([ + 'name' => 'containerpopulate', + 'label' => 'Container populate', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId, 'field1'); + + // add a ticket with input data + $ticket = new Ticket(); + $ticket->fields['id'] = 123; + $ticket->fields['entities_id'] = 0; + $ticket->input = [ + 'plugin_fields_' . $containerId . '_field1' => 'hello world', + ]; + + // call populateData + $data = PluginFieldsContainer::populateData($containerId, $ticket); + + // check fields are populated without prefix + $this->assertIsArray($data); + $this->assertSame(123, $data['items_id']); + $this->assertSame('Ticket', $data['itemtype']); + $this->assertSame(0, $data['entities_id']); + $this->assertSame('hello world', $data['field1']); + $this->deleteAllContainers(); + } + + public function testPopulateDataWithMultipleSelectionFields(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; + + // add a container with a dropdown field that allows multiple selections + $containerId = $this->addContainer([ + 'name' => 'containermulti', + 'label' => 'Container Multi', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + $fieldName = 'comboonemulti'; + $this->addFieldToContainer($containerId, $fieldName, 'dropdown', true); + + // add options to the dropdown field + $dropdownClass = PluginFieldsDropdown::getClassname($fieldName); + $dropdown = new $dropdownClass(); + $idOptA = $dropdown->add(['name' => 'Option A']); + $idOptB = $dropdown->add(['name' => 'Option B']); + + // add a ticket with input data + $ticket = new Ticket(); + $ticket->getEmpty(); + $ticket->fields['id'] = 1001; + $ticket->fields['entities_id'] = 0; + + $ticket->input = [ + "plugin_fields_{$containerId}_{$fieldName}dropdowns_id" => [$idOptA, $idOptB], + "_plugin_fields_{$containerId}_{$fieldName}dropdowns_id_defined" => true, + ]; + + // call populateData + $data = PluginFieldsContainer::populateData($containerId, $ticket); + $this->assertIsArray($data); + + $col = "plugin_fields_{$fieldName}dropdowns_id"; + $this->assertArrayHasKey($col, $data); + $this->assertSame([$idOptA, $idOptB], $data[$col]); + $this->deleteAllContainers(); + } + + public function testShowForTabDisplaysMultipleContainers(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; // profil with right to read fields + $_SERVER['REQUEST_URI'] = '/front/ticket.form.php'; + $_SESSION['glpi_tabs']['ticket'] = 'ticket$main'; + + // add two containers with fields + $idContainer1 = $this->addContainer([ + 'name' => 'containeroneone', + 'label' => 'Container 11', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($idContainer1, 'field1'); + + $idContainer2 = $this->addContainer([ + 'name' => 'containertwotwo', + 'label' => 'Container 22', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($idContainer2, 'field2'); + + // add a ticket to test + $ticket = $this->addTicket([ + 'name' => 'ShowForTab test', + 'content' => 'dummy', + 'entities_id' => 0, + ]); + + // capture the output of showForTab + ob_start(); + PluginFieldsField::showForTab(['item' => $ticket, 'options' => []]); + $html = ob_get_clean(); + + // check if the HTML contains both containers + $this->assertStringContainsString( + 'id=\'plugin_fields_container_'.$idContainer1.'\'', + $html, + "Container 1 (id=$idContainer1) not displayed" + ); + $this->assertStringContainsString( + 'id=\'plugin_fields_container_'.$idContainer2.'\'', + $html, + "Container 2 (id=$idContainer2) not displayed" + ); + $this->deleteAllContainers(); + } + + public function testShowForTabRightsAreEnforced(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SERVER['REQUEST_URI'] = '/front/ticket.form.php'; + $_SESSION['glpi_tabs']['ticket'] = -1; + + // add a container with a field + $containerId = $this->addContainer([ + 'name' => 'rightTest', + 'label' => 'Container Right Test', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId, 'visiblefield'); + + // set profiles + $profileWithRight = 4; + $profileNoRight = -1; + + // add a ticket + $ticket = new Ticket(); + $ticket->getEmpty(); + $ticket->fields['id'] = 42; + $ticket->fields['entities_id'] = 0; + + // case 1 : profile with right + $_SESSION['glpiactiveprofile']['id'] = $profileWithRight; + + ob_start(); + PluginFieldsField::showForTab(['item' => $ticket]); + $htmlWithRight = trim(ob_get_clean()); + + $this->assertNotSame( + '', + $htmlWithRight, + 'Container should be visible for a profile with right.' + ); + // end case 1 + + // case 2 : profile without right + $_SESSION['glpiactiveprofile']['id'] = $profileNoRight; + + ob_start(); + PluginFieldsField::showForTab(['item' => $ticket]); + $htmlNoRight = trim(ob_get_clean()); + + $this->assertSame( + '', + $htmlNoRight, + 'Container should not be visible for a profile without right.' + ); + // end case 2 + $this->deleteAllContainers(); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..6b9d7c13 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,30 @@ +checkPluginState('fields'); +$plugin->getFromDBbyDir('fields'); + +if (!plugin_fields_check_prerequisites()) { + echo "\nPrerequisites are not met!"; + die(1); +} + +if (!$plugin->isInstalled('fields')) { + $plugin->install($plugin->getID()); +} +if (!$plugin->isActivated('fields')) { + $plugin->activate($plugin->getID()); +}