From ba1a12a591edbb7c29e3b4a4cd3e66e9017801c2 Mon Sep 17 00:00:00 2001 From: btry Date: Tue, 1 Sep 2026 14:40:51 +0200 Subject: [PATCH 01/35] feat: glpi 12 compatibility --- .github/workflows/continuous-integration.yml | 2 +- composer.json | 2 +- package-lock.json | 4 +- package.json | 2 +- setup.php | 11 +- src/AbstractChildDropdown.php | 2 +- src/AbstractImpact.php | 6 +- .../View/Extension/DataHelpersExtension.php | 303 ------------------ src/CarbonEmission.php | 4 +- src/CarbonIntensity.php | 2 +- src/Command/CollectCarbonIntensityCommand.php | 2 +- .../CreateFakeCarbonIntensityCommand.php | 2 +- src/Command/CreateTestInventoryCommand.php | 2 +- src/Command/ExportDashboardCommand.php | 2 +- src/ComputerModel.php | 4 +- src/ComputerType.php | 4 +- .../ElectricityMaps/Client.php | 6 +- src/DataSource/CarbonIntensity/Rte/Client.php | 6 +- src/Location.php | 4 +- src/MonitorModel.php | 4 +- src/MonitorType.php | 4 +- src/NetworkEquipmentModel.php | 4 +- src/NetworkEquipmentType.php | 4 +- src/Report.php | 4 +- src/Source_Zone.php | 12 +- src/UsageInfo.php | 6 +- tests/install/PluginInstallTest.php | 2 - tests/units/HookTest.php | 2 +- 28 files changed, 50 insertions(+), 362 deletions(-) delete mode 100644 src/Application/View/Extension/DataHelpersExtension.php diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index dc3f4f9a..6530865b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -22,7 +22,7 @@ jobs: name: "Generate CI matrix" uses: "glpi-project/plugin-ci-workflows/.github/workflows/generate-ci-matrix.yml@v1" with: - glpi-version: "11.0.x" + glpi-version: "12.0.x" ci: name: "GLPI ${{ matrix.glpi-version }} - php:${{ matrix.php-version }} - ${{ matrix.db-image }}" needs: "generate-ci-matrix" diff --git a/composer.json b/composer.json index e9a9a0b1..a0241e08 100644 --- a/composer.json +++ b/composer.json @@ -35,5 +35,5 @@ "license-headers": "vendor/bin/licence-headers-check --ansi --no-interaction", "build-schema": "tools/build-db-schema.php carbon | plantuml -p -tpng > docs/db-schema.png" }, - "version": "1.3.0-dev" + "version": "1.4.0-dev" } diff --git a/package-lock.json b/package-lock.json index ce930f9d..91ab1294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "carbon", - "version": "1.3.0-dev", + "version": "1.4.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "carbon", - "version": "1.3.0-dev", + "version": "1.4.0-dev", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 831cb333..dcf9a0ed 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "carbon", "description": "Measurement of carbon emissions of GLPI devices", "license": "GPL-3.0-or-later", - "version": "1.3.0-dev", + "version": "1.4.0-dev", "engines": { "node": ">= 18.12.0" }, diff --git a/setup.php b/setup.php index 5017da4b..5cd7eaf3 100644 --- a/setup.php +++ b/setup.php @@ -47,9 +47,9 @@ use Profile as GlpiProfile; // Version of the plugin (major.minor.bugfix) -define('PLUGIN_CARBON_VERSION', '1.3.0-dev'); +define('PLUGIN_CARBON_VERSION', '1.4.0-dev'); // Schema version of this version (major.minor.bugfix) -define('PLUGIN_CARBON_SCHEMA_VERSION', '1.3.0'); +define('PLUGIN_CARBON_SCHEMA_VERSION', '1.4.0'); // Version compatibility check -- from GLPI developer documentation // > A bug in GLPI prior to 11.0.7 caused plugin routes with method constraints other than GET to never match. @@ -63,9 +63,9 @@ // Watch it when adding new controllers. // Minimal GLPI version, inclusive -define('PLUGIN_CARBON_MIN_GLPI_VERSION', '11.0.0'); +define('PLUGIN_CARBON_MIN_GLPI_VERSION', '12.0.0'); // Maximum GLPI version, exclusive -define('PLUGIN_CARBON_MAX_GLPI_VERSION', '12.0.0'); +define('PLUGIN_CARBON_MAX_GLPI_VERSION', '13.0.0'); define('PLUGIN_CARBON_DECIMALS', 3); define('EMBER_DATASET_DATE', '2025-07-30'); @@ -223,8 +223,9 @@ function plugin_carbon_check_prerequisites() $prerequisitesSuccess = true; + // In case GLPI is so old that the modern version checker is not implemented /** @phpstan-ignore if.alwaysFalse */ - if (version_compare(GLPI_VERSION, PLUGIN_CARBON_MIN_GLPI_VERSION, 'lt')) { + if (version_compare(GLPI_VERSION, "10.0.0", 'lt')) { echo "This plugin requires GLPI >= " . PLUGIN_CARBON_MIN_GLPI_VERSION . " and GLPI < " . PLUGIN_CARBON_MAX_GLPI_VERSION . "
"; $prerequisitesSuccess = false; } diff --git a/src/AbstractChildDropdown.php b/src/AbstractChildDropdown.php index 64675fb5..54994398 100644 --- a/src/AbstractChildDropdown.php +++ b/src/AbstractChildDropdown.php @@ -41,7 +41,7 @@ abstract class AbstractChildDropdown extends CommonDBChild { - public static $rightname = 'dropdown'; + public static string $rightname = 'dropdown'; #[Override] public static function getIcon(): string diff --git a/src/AbstractImpact.php b/src/AbstractImpact.php index 103ddc09..f5c4a4fc 100644 --- a/src/AbstractImpact.php +++ b/src/AbstractImpact.php @@ -44,10 +44,10 @@ abstract class AbstractImpact extends CommonDBChild { - public static $itemtype = 'itemtype'; - public static $items_id = 'items_id'; + public static string $itemtype = 'itemtype'; + public static string $items_id = 'items_id'; - public static $rightname = 'carbon:report'; + public static string $rightname = 'carbon:report'; #[Override] public function canEdit($ID): bool diff --git a/src/Application/View/Extension/DataHelpersExtension.php b/src/Application/View/Extension/DataHelpersExtension.php deleted file mode 100644 index 2ccec211..00000000 --- a/src/Application/View/Extension/DataHelpersExtension.php +++ /dev/null @@ -1,303 +0,0 @@ -. - * - * ------------------------------------------------------------------------- - */ - -namespace GlpiPlugin\Carbon\Application\View\Extension; - -use Glpi\RichText\RichText; -use Glpi\Toolbox\Sanitizer; -use Html; -use Override; -use Toolbox; -use Twig\Extension\AbstractExtension; -use Twig\TwigFilter; - -/** - * @since 10.0.0 - */ -class DataHelpersExtension extends AbstractExtension -{ - #[Override] - public function getFilters(): array - { - return [ - new TwigFilter('formatted_datetime', [$this, 'getFormattedDatetime']), - new TwigFilter('formatted_date', [$this, 'getFormattedDate']), - new TwigFilter('formatted_duration', [$this, 'getFormattedDuration']), - new TwigFilter('formatted_integer', [$this, 'getFormattedInteger']), - new TwigFilter('formatted_number', [$this, 'getFormattedNumber']), - new TwigFilter('formatted_size', [$this, 'getFormattedSize']), - new TwigFilter('html_to_text', [$this, 'getTextFromHtml']), - new TwigFilter('long2ip', 'long2ip'), - new TwigFilter('picture_url', [$this, 'getPictureUrl']), - new TwigFilter('relative_datetime', [$this, 'getRelativeDatetime']), - new TwigFilter('safe_html', [$this, 'getSafeHtml'], ['is_safe' => ['html']]), - new TwigFilter('verbatim_value', [$this, 'getVerbatimValue']), - new TwigFilter('shortcut', [$this, 'underlineShortcutLetter'], ['is_safe' => ['html']]), - new TwigFilter('enhanced_html', [$this, 'getEnhancedHtml'], ['is_safe' => ['html']]), - new TwigFilter('truncate_left', [$this, 'truncateLeft']), - ]; - } - - /** - * Return date formatted to user preferred format. - * - * @param mixed $datetime - * @param bool $with_seconds - * - * @return string|null - */ - public function getFormattedDatetime($datetime, bool $with_seconds = false): ?string - { - if (!is_string($datetime)) { - return null; - } - return Html::convDateTime($datetime, null, $with_seconds); - } - - /** - * Return date formatted to user preferred format. - * - * @param mixed $date - * - * @return string|null - */ - public function getFormattedDate($date): ?string - { - if (!is_string($date)) { - return null; - } - return Html::convDate($date); - } - - /** - * Return relative representation of given date. - * - * @param mixed $datetime - * - * @return string|null - */ - public function getRelativeDatetime($datetime): ?string - { - if (!is_string($datetime)) { - return null; - } - return Html::timestampToRelativeStr($datetime); - } - - /** - * Return human readable duration. - * - * @param mixed $duration - * @param bool $display_seconds (default: true) - * - * @return string|null - */ - public function getFormattedDuration( - $duration, - bool $display_seconds = true - ): ?string { - if (!is_numeric($duration)) { - return null; - } - return Html::timestampToString($duration, $display_seconds); - } - - /** - * Return integer formatted to user preferred format. - * - * @param mixed $number Number to display - * - * @return string - */ - public function getFormattedInteger($number): string - { - return Html::formatNumber($number, false, 0); - } - - /** - * Return number formatted to user preferred format. - * - * @param mixed $number Number to display - * - * @return string - */ - public function getFormattedNumber($number): string - { - return Html::formatNumber($number); - } - - /** - * Return size formatted in a compact way (mo, ko, etc). - * - * @param mixed $number - * - * @return string - */ - public function getFormattedSize($number): string - { - if (!is_numeric($number)) { - return ''; - } - return Toolbox::getSize($number); - } - - /** - * Return URL for given picture. - * - * @param mixed $path - * - * @return null|string - */ - public function getPictureUrl($path): ?string - { - if (!is_string($path)) { - return null; - } - - return Toolbox::getPictureUrl($path, true); - } - - /** - * Return string having its shortcut letter underlined. - * - * @param string $string - * @param string $shortcut_letter - * - * @return string - */ - public function underlineShortcutLetter(string $string, string $shortcut_letter): string - { - if (empty($shortcut_letter)) { - return $string; - } - return Toolbox::shortcut($string, $shortcut_letter); - } - - /** - * Return plain text from HTML (rich text). - * - * @param mixed $string HTML string to be made safe - * @param bool $keep_presentation Indicates whether the presentation elements have to be replaced by plaintext equivalents - * @param bool $compact Indicates whether the output should be compact (limited line length, no links URL, ...) - * - * @return mixed - */ - public function getTextFromHtml($string, bool $keep_presentation = true, bool $compact = false) - { - if (!is_string($string)) { - return $string; - } - - return RichText::getTextFromHtml($string, $keep_presentation, $compact); - } - - /** - * Return safe HTML (rich text). - * Value will be made safe, whenever it has been sanitize (value fetched from DB), - * or not (value computed during runtime). - * Result will not be escaped, to prevent having to use `|raw` filter. - * - * @param mixed $string - * - * @return mixed - */ - public function getSafeHtml($string) - { - if (!is_string($string)) { - return $string; - } - - return RichText::getSafeHtml($string); - } - - /** - * Return enhanced HTML (rich text). - * Value will be made safe, whenever it has been sanitize (value fetched from DB), - * or not (value computed during runtime). - * Result will not be escaped, to prevent having to use `|raw` filter. - * - * @param mixed $string - * - * @return mixed - */ - public function getEnhancedHtml($string, array $params = []) - { - if (!is_string($string)) { - return $string; - } - - return RichText::getEnhancedHtml($string, $params); - } - - /** - * Return verbatim value for an itemtype field. - * Returned value will be unsanitized if it has been transformed by GLPI sanitizing process (value fetched from DB). - * Twig autoescaping system will then ensure that value is correctly escaped in rendered HTML. - * - * @param mixed $string - * - * @return mixed - * - * @deprecated 11.0 - */ - public function getVerbatimValue($string) - { - Toolbox::deprecated(); - - if (!is_string($string)) { - return $string; - } - - return Sanitizer::unsanitize($string); - } - - - /** - * return the provided string truncated on the left and prepend a prefix separator if length is reached - * - * @param string $string the string to left truncate - * @param int $length number of char to preserve - * @param string $separator prefix to prepend to the string - * - * @return string truncated string - */ - public function truncateLeft(string $string = "", int $length = 30, string $separator = "...") - { - if (mb_strlen($string) <= $length) { - return $string; - } - - return $separator . mb_substr($string, -$length); - } -} diff --git a/src/CarbonEmission.php b/src/CarbonEmission.php index 95619e15..bc2dea3d 100644 --- a/src/CarbonEmission.php +++ b/src/CarbonEmission.php @@ -44,8 +44,8 @@ class CarbonEmission extends CommonDBChild { - public static $itemtype = 'itemtype'; - public static $items_id = 'items_id'; + public static string $itemtype = 'itemtype'; + public static string $items_id = 'items_id'; #[Override] public static function getTypeName($nb = 0) diff --git a/src/CarbonIntensity.php b/src/CarbonIntensity.php index 4c9fc7ec..99d3a7b3 100644 --- a/src/CarbonIntensity.php +++ b/src/CarbonIntensity.php @@ -59,7 +59,7 @@ class CarbonIntensity extends CommonDropdown { private const MIN_HISTORY_LENGTH = '13 months ago'; - public static $rightname = 'carbon:report'; + public static string $rightname = 'carbon:report'; #[Override] public static function getTypeName($nb = 0) diff --git a/src/Command/CollectCarbonIntensityCommand.php b/src/Command/CollectCarbonIntensityCommand.php index 09941081..90cdfab2 100644 --- a/src/Command/CollectCarbonIntensityCommand.php +++ b/src/Command/CollectCarbonIntensityCommand.php @@ -117,7 +117,7 @@ protected function interact(InputInterface $input, OutputInterface $output) } #[Override] - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { if (count($this->zones) > 1 && $input->getArgument('zone') === null) { // Null is not a valid key if there are several zones available for the source diff --git a/src/Command/CreateFakeCarbonIntensityCommand.php b/src/Command/CreateFakeCarbonIntensityCommand.php index 516b9d1e..afb5855e 100644 --- a/src/Command/CreateFakeCarbonIntensityCommand.php +++ b/src/Command/CreateFakeCarbonIntensityCommand.php @@ -63,7 +63,7 @@ protected function configure() } #[Override] - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->output = $output; diff --git a/src/Command/CreateTestInventoryCommand.php b/src/Command/CreateTestInventoryCommand.php index 1ea9c7cf..882f4d50 100644 --- a/src/Command/CreateTestInventoryCommand.php +++ b/src/Command/CreateTestInventoryCommand.php @@ -141,7 +141,7 @@ protected function configure() } #[Override] - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->output = $output; diff --git a/src/Command/ExportDashboardCommand.php b/src/Command/ExportDashboardCommand.php index 9c36d94b..988250bb 100644 --- a/src/Command/ExportDashboardCommand.php +++ b/src/Command/ExportDashboardCommand.php @@ -60,7 +60,7 @@ protected function configure() } #[Override] - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { /** @var DBmysql $DB */ global $DB; diff --git a/src/ComputerModel.php b/src/ComputerModel.php index f08fb727..85ce4795 100644 --- a/src/ComputerModel.php +++ b/src/ComputerModel.php @@ -36,6 +36,6 @@ class ComputerModel extends AbstractModel { - public static $itemtype = GlpiComputerModel::class; - public static $items_id = 'computermodels_id'; + public static string $itemtype = GlpiComputerModel::class; + public static string $items_id = 'computermodels_id'; } diff --git a/src/ComputerType.php b/src/ComputerType.php index c20955d7..2ef95b35 100644 --- a/src/ComputerType.php +++ b/src/ComputerType.php @@ -41,8 +41,8 @@ class ComputerType extends AbstractChildDropdown { - public static $itemtype = GlpiComputerType::class; - public static $items_id = 'computertypes_id'; + public static string $itemtype = GlpiComputerType::class; + public static string $items_id = 'computertypes_id'; public const CATEGORY_UNDEFINED = 0; public const CATEGORY_DESKTOP = 1; diff --git a/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php b/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php index 1c9a6c08..16d5ae6e 100644 --- a/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php +++ b/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php @@ -37,7 +37,6 @@ use DateTimeImmutable; use DateTimeInterface; use DateTimeZone; -use DBmysql; use GLPIKey; use GlpiPlugin\Carbon\CarbonIntensity; use GlpiPlugin\Carbon\DataSource\CarbonIntensity\AbortException; @@ -412,11 +411,8 @@ protected function formatOutput(array $response, int $step): array */ protected function shiftToLocalTimezone(array $response): array { - /** @var DBmysql $DB */ - global $DB; - $shifted_response = []; - $local_timezone = new DateTimeZone($DB->guessTimezone()); + $local_timezone = new DateTimeZone(date_default_timezone_get()); array_walk($response, function ($item, $key) use (&$shifted_response, $local_timezone) { $shifted_date_object = DateTime::createFromFormat('Y-m-d\TH:i:s.vp', $item['datetime']) ->setTimezone($local_timezone); diff --git a/src/DataSource/CarbonIntensity/Rte/Client.php b/src/DataSource/CarbonIntensity/Rte/Client.php index ec7b66c7..8b16ff62 100644 --- a/src/DataSource/CarbonIntensity/Rte/Client.php +++ b/src/DataSource/CarbonIntensity/Rte/Client.php @@ -37,7 +37,6 @@ use DateTimeImmutable; use DateTimeInterface; use DateTimeZone; -use DBmysql; use GlpiPlugin\Carbon\DataSource\CarbonIntensity\AbstractClient; use GlpiPlugin\Carbon\DataSource\RestApiClientInterface; use GlpiPlugin\Carbon\DataTracking\AbstractTracked; @@ -387,11 +386,8 @@ protected function formatOutput(array $response, int $step): array */ protected function shiftToLocalTimezone(array $response): array { - /** @var DBmysql $DB */ - global $DB; - $shifted_response = []; - $local_timezone = new DateTimeZone($DB->guessTimezone()); + $local_timezone = new DateTimeZone(date_default_timezone_get()); array_walk($response, function ($item, $key) use (&$shifted_response, $local_timezone) { $shifted_date_object = DateTime::createFromFormat('Y-m-d\TH:i:sP', $item['date_heure']) ->setTimezone($local_timezone); diff --git a/src/Location.php b/src/Location.php index 8d3af569..bf3e86aa 100644 --- a/src/Location.php +++ b/src/Location.php @@ -60,8 +60,8 @@ class Location extends CommonDBChild { // From CommonDBRelation - public static $itemtype = GlpiLocation::class; - public static $items_id = 'locations_id'; + public static string $itemtype = GlpiLocation::class; + public static string $items_id = 'locations_id'; #[Override] public static function getIcon() diff --git a/src/MonitorModel.php b/src/MonitorModel.php index d929e3d2..f144bb5f 100644 --- a/src/MonitorModel.php +++ b/src/MonitorModel.php @@ -36,6 +36,6 @@ class MonitorModel extends AbstractModel { - public static $itemtype = GlpiMonitorModel::class; - public static $items_id = 'monitormodels_id'; + public static string $itemtype = GlpiMonitorModel::class; + public static string $items_id = 'monitormodels_id'; } diff --git a/src/MonitorType.php b/src/MonitorType.php index 9d104059..e4e994bb 100644 --- a/src/MonitorType.php +++ b/src/MonitorType.php @@ -40,8 +40,8 @@ class MonitorType extends AbstractChildDropdown { - public static $itemtype = GlpiMonitorType::class; - public static $items_id = 'monitortypes_id'; + public static string $itemtype = GlpiMonitorType::class; + public static string $items_id = 'monitortypes_id'; #[Override] public static function showMassiveActionsSubForm(MassiveAction $ma) diff --git a/src/NetworkEquipmentModel.php b/src/NetworkEquipmentModel.php index 70bde975..1ed55dfe 100644 --- a/src/NetworkEquipmentModel.php +++ b/src/NetworkEquipmentModel.php @@ -36,6 +36,6 @@ class NetworkEquipmentModel extends AbstractModel { - public static $itemtype = GlpiNetworkEquipmentModel::class; - public static $items_id = 'networkequipmentmodels_id'; + public static string $itemtype = GlpiNetworkEquipmentModel::class; + public static string $items_id = 'networkequipmentmodels_id'; } diff --git a/src/NetworkEquipmentType.php b/src/NetworkEquipmentType.php index 64e0cb3e..c49e9d33 100644 --- a/src/NetworkEquipmentType.php +++ b/src/NetworkEquipmentType.php @@ -40,8 +40,8 @@ class NetworkEquipmentType extends AbstractChildDropdown { - public static $itemtype = GlpiNetworkEquipmentType::class; - public static $items_id = 'networkequipmenttypes_id'; + public static string $itemtype = GlpiNetworkEquipmentType::class; + public static string $items_id = 'networkequipmenttypes_id'; #[Override] public static function showMassiveActionsSubForm(MassiveAction $ma) diff --git a/src/Report.php b/src/Report.php index c0511dfa..46ccf202 100644 --- a/src/Report.php +++ b/src/Report.php @@ -42,8 +42,8 @@ class Report extends CommonDBTM { - public static $rightname = 'carbon:report'; - protected static $notable = true; + public static string $rightname = 'carbon:report'; + protected static bool $notable = true; #[Override] public static function getTypeName($nb = 0) diff --git a/src/Source_Zone.php b/src/Source_Zone.php index c485486f..61fc3bb9 100644 --- a/src/Source_Zone.php +++ b/src/Source_Zone.php @@ -46,13 +46,13 @@ class Source_Zone extends CommonDBRelation { - public static $itemtype_1 = Source::class; // Type ref or field name (must start with itemtype) - public static $items_id_1 = 'plugin_carbon_sources_id'; // Field name - public static $checkItem_1_Rights = self::HAVE_SAME_RIGHT_ON_ITEM; + public static ?string $itemtype_1 = Source::class; // Type ref or field name (must start with itemtype) + public static ?string $items_id_1 = 'plugin_carbon_sources_id'; // Field name + public static int $checkItem_1_Rights = self::HAVE_SAME_RIGHT_ON_ITEM; - public static $itemtype_2 = Zone::class; // Type ref or field name (must start with itemtype) - public static $items_id_2 = 'plugin_carbon_zones_id'; // Field name - public static $checkItem_2_Rights = self::HAVE_SAME_RIGHT_ON_ITEM; + public static ?string $itemtype_2 = Zone::class; // Type ref or field name (must start with itemtype) + public static ?string $items_id_2 = 'plugin_carbon_zones_id'; // Field name + public static int $checkItem_2_Rights = self::HAVE_SAME_RIGHT_ON_ITEM; #[Override] public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) diff --git a/src/UsageInfo.php b/src/UsageInfo.php index 06274f96..f0736434 100644 --- a/src/UsageInfo.php +++ b/src/UsageInfo.php @@ -53,10 +53,10 @@ */ class UsageInfo extends CommonDBChild { - public static $itemtype = 'itemtype'; - public static $items_id = 'items_id'; + public static string $itemtype = 'itemtype'; + public static string $items_id = 'items_id'; - public static $rightname = 'carbon:report'; + public static string $rightname = 'carbon:report'; #[Override] public static function getTypeName($nb = 0) diff --git a/tests/install/PluginInstallTest.php b/tests/install/PluginInstallTest.php index c6a40cea..b9159b45 100644 --- a/tests/install/PluginInstallTest.php +++ b/tests/install/PluginInstallTest.php @@ -170,7 +170,6 @@ public function testInstallPlugin() $this->checkRegisteredClasses(); } - #[CoversNothing()] public function testConfigurationExists() { $config = Config::getConfigurationValues(TEST_PLUGIN_NAME); @@ -181,7 +180,6 @@ public function testConfigurationExists() return $config; } - #[CoversNothing()] private function checkSchema( string $version, bool $strict = true, diff --git a/tests/units/HookTest.php b/tests/units/HookTest.php index cb5b2ffd..ae6e5877 100644 --- a/tests/units/HookTest.php +++ b/tests/units/HookTest.php @@ -103,7 +103,7 @@ public function testCarbonAssetTypeIsPurgedOnAssetTypePurge() 'computertypes_id' => $computer_type->getID(), ]); - $computer_type->delete($computer_type->fields, 1); + $computer_type->delete($computer_type->fields, true); $count = (new DbUtils())->countElementsInTable($carbon_computer_type::getTable(), [ 'computertypes_id' => $computer_type->getID(), ]); From c6a06dfe3ab6c8602d1cac93c9e4a8cfa54fe77a Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 3 Sep 2026 09:35:50 +0200 Subject: [PATCH 02/35] feat: drop deprecated CSRF token --- src/Config.php | 1 - templates/config.html.twig | 5 ++--- templates/environmentalimpact-item.html.twig | 8 ++++---- tests/units/ConfigTest.php | 2 -- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Config.php b/src/Config.php index e9b38b9e..35aca4ca 100644 --- a/src/Config.php +++ b/src/Config.php @@ -123,7 +123,6 @@ public function showForm($ID, $options = []) $current_config = array_diff_key($current_config, array_flip($secured_config)); $reset_args = json_encode([ - '_glpi_csrf_token' => Session::getNewCSRFToken(), 'reset_all' => '', ]); $usage_impact_action_url = 'submitGetLink("' . $CFG_GLPI['root_doc'] . '/plugins/carbon/front/usageimpact.form.php", ' . $reset_args . ')'; diff --git a/templates/config.html.twig b/templates/config.html.twig index 3f688a71..d199f31d 100644 --- a/templates/config.html.twig +++ b/templates/config.html.twig @@ -35,7 +35,6 @@
- {{ fields.largeTitle( __('Impact engine', 'carbon'), @@ -79,7 +78,7 @@ {{ __('Reset usage impacts. This will delete all calculated usage impacts for all assets.', 'carbon') }} - {% set reset_args = "{_glpi_csrf_token: '" ~ csrf_token() ~ "', reset_all: ''}" %} + {% set reset_args = "{reset_all: ''}" %} {{ _x('button', 'Reset usage impacts') }} @@ -89,7 +88,7 @@ {{ __('Reset embodied impacts. This will delete all calculated embodied impacts for all assets.', 'carbon') }} - {% set reset_args = "{_glpi_csrf_token: '" ~ csrf_token() ~ "', reset_all: ''}" %} + {% set reset_args = "{reset_all: ''}" %} {{ _x('button', 'Reset embodied impacts') }} diff --git a/templates/environmentalimpact-item.html.twig b/templates/environmentalimpact-item.html.twig index a499c1ec..436e6525 100644 --- a/templates/environmentalimpact-item.html.twig +++ b/templates/environmentalimpact-item.html.twig @@ -61,10 +61,10 @@
{% if not usage_impact.isNewItem() or usage_carbon_emission_count > 0 %} - {% set reset_args = "{_glpi_csrf_token: '" ~ csrf_token() ~ "', reset: '', itemtype: '" ~ asset.getType() ~ "', items_id: '" ~ asset.getID() ~ "'}" %} + {% set reset_args = "{reset: '', itemtype: '" ~ asset.getType() ~ "', items_id: '" ~ asset.getID() ~ "'}" %} {{ __('Reset data', 'carbon') }} {% endif %} - {% set calculate_args = "{_glpi_csrf_token: '" ~ csrf_token() ~ "', calculate: '', itemtype: '" ~ asset.getType() ~ "', items_id: '" ~ asset.getID() ~ "'}" %} + {% set calculate_args = "{calculate: '', itemtype: '" ~ asset.getType() ~ "', items_id: '" ~ asset.getID() ~ "'}" %} {{ __('Calculate data', 'carbon') }}
@@ -96,10 +96,10 @@
{% if not embodied_impact.isNewItem() %} - {% set reset_args = "{_glpi_csrf_token: '" ~ csrf_token() ~ "', reset: '', id: '" ~ embodied_impact.getID() ~ "'}" %} + {% set reset_args = "{reset: '', id: '" ~ embodied_impact.getID() ~ "'}" %} {{ __('Reset data', 'carbon') }} {% endif %} - {% set calculate_args = "{_glpi_csrf_token: '" ~ csrf_token() ~ "', calculate: '', itemtype: '" ~ asset.getType() ~ "', items_id: '" ~ asset.getID() ~ "'}" %} + {% set calculate_args = "{calculate: '', itemtype: '" ~ asset.getType() ~ "', items_id: '" ~ asset.getID() ~ "'}" %} {{ __('Calculate data', 'carbon') }}
diff --git a/tests/units/ConfigTest.php b/tests/units/ConfigTest.php index 5f0c5d25..c13a76e6 100644 --- a/tests/units/ConfigTest.php +++ b/tests/units/ConfigTest.php @@ -99,10 +99,8 @@ public function testShowForm() $crawler = new Crawler($output); $config_class = $crawler->filter('input[type="hidden"][name="config_class"]'); $config_context = $crawler->filter('input[type="hidden"][name="config_context"]'); - $csrf = $crawler->filter('input[type="hidden"][name="_glpi_csrf_token"]'); $this->assertEquals(1, $config_class->count()); $this->assertEquals(1, $config_context->count()); - $this->assertEquals(1, $csrf->count()); $electricitymaps_api = $crawler->filter('input[name="electricitymap_api_key"]'); $impact_engine = $crawler->filter('select[name="impact_engine"]'); $this->assertEquals(1, $electricitymaps_api->count()); From dd77614562240f970c299876c03b77b82ed57647 Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 3 Sep 2026 10:02:11 +0200 Subject: [PATCH 03/35] fix: truncate table with bad WHERE clause --- src/AbstractImpact.php | 2 +- src/CarbonEmission.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AbstractImpact.php b/src/AbstractImpact.php index f5c4a4fc..95a9fa71 100644 --- a/src/AbstractImpact.php +++ b/src/AbstractImpact.php @@ -255,6 +255,6 @@ public function truncate(): bool return false; } - return $DB->delete(static::getTable(), [1]); + return $DB->delete(static::getTable(), ['NOT' => ['id' => null]]); } } diff --git a/src/CarbonEmission.php b/src/CarbonEmission.php index bc2dea3d..5c717a49 100644 --- a/src/CarbonEmission.php +++ b/src/CarbonEmission.php @@ -229,6 +229,6 @@ public function truncate(): bool return false; } - return $DB->delete(static::getTable(), [1]); + return $DB->delete(static::getTable(), ['NOT' => ['id' => null]]); } } From 2a92c85e99f67139d7e1b0e35e81d11fee9773d5 Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 3 Sep 2026 10:15:38 +0200 Subject: [PATCH 04/35] feat: disable useless DB version check --- setup.php | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/setup.php b/setup.php index 5cd7eaf3..cdd158e7 100644 --- a/setup.php +++ b/setup.php @@ -240,22 +240,23 @@ function plugin_carbon_check_prerequisites() $prerequisitesSuccess = false; } - if (getenv('CI') === false) { - // only when not under test - $version_string = $DB->getVersion(); - - $server = preg_match('/-MariaDB/', $version_string) ? 'MariaDB' : 'MySQL'; - $version = preg_replace('/^((\d+\.?)+).*$/', '$1', $version_string); - if ($server === 'MySQL' && version_compare($version, '8.0.0', '<')) { - echo 'This plugin requires MySQL >= 8.0 or MariaDB >= 10.2
'; - $prerequisitesSuccess = false; - } - - if ($server === 'MariaDB' && version_compare($version, '10.2.0', '<')) { - echo 'This plugin requires MySQL >= 8.0 or MariaDB >= 10.2
'; - $prerequisitesSuccess = false; - } - } + // GLPI 12 requires MySQL >= 8.0 or MariaDB >= 10.2, then the check below can be disabled + // if (getenv('CI') === false) { + // // only when not under test + // $version_string = $DB->getVersion(); + + // $server = preg_match('/-MariaDB/', $version_string) ? 'MariaDB' : 'MySQL'; + // $version = preg_replace('/^((\d+\.?)+).*$/', '$1', $version_string); + // if ($server === 'MySQL' && version_compare($version, '8.0.0', '<')) { + // echo 'This plugin requires MySQL >= 8.0 or MariaDB >= 10.2
'; + // $prerequisitesSuccess = false; + // } + + // if ($server === 'MariaDB' && version_compare($version, '10.2.0', '<')) { + // echo 'This plugin requires MySQL >= 8.0 or MariaDB >= 10.2
'; + // $prerequisitesSuccess = false; + // } + // } return $prerequisitesSuccess; } From c62f8c6d92f20de76400a646e454c8ba7f685df0 Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 3 Sep 2026 10:24:58 +0200 Subject: [PATCH 05/35] docs(security): update --- SECURITY.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 0484177c..32d96350 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,6 +19,8 @@ Thank you for improving the security of glpi-agent. | Version | Supported | | ------- | ------------------ | -| 1.0.x | :heavy_check_mark: | +| 1.0.x | :x: | | 1.2.x | :x: | | 1.3.x | :heavy_check_mark: | +| 1.3.x | :heavy_check_mark: | +| 1.4.x | :heavy_check_mark: | From a14d8395b308d2709ce335c64d7e3e0b5b1954c8 Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 3 Sep 2026 11:09:48 +0200 Subject: [PATCH 06/35] test: remove useless attributes --- tests/install/PluginInstallTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/install/PluginInstallTest.php b/tests/install/PluginInstallTest.php index b9159b45..f0bc92c0 100644 --- a/tests/install/PluginInstallTest.php +++ b/tests/install/PluginInstallTest.php @@ -67,7 +67,6 @@ use NetworkEquipmentModel; use NetworkEquipmentType; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\Depends; use Plugin; use Profile; @@ -937,7 +936,6 @@ public function checkRegisteredClasses() $this->assertEquals($expected, $result); } - #[CoversNothing()] #[Depends('testInstallPlugin')] public function test_version_is_consistent_across_files() { @@ -980,7 +978,6 @@ public function test_version_is_consistent_across_files() $this->assertStringContainsString($setup_version, $supported_versions_table, "Current version '$setup_version' not found in Supported Versions table in SECURITY.md"); } - #[CoversNothing()] #[Depends('testInstallPlugin')] public function test_tagged_version_is_declared_in_plugin_xml() { From 5087ed804d6c1d7033b6320112d11f8d7aeb08ab Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 3 Sep 2026 11:10:13 +0200 Subject: [PATCH 07/35] test: fix wrong function name --- tests/units/CarbonEmissionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/units/CarbonEmissionTest.php b/tests/units/CarbonEmissionTest.php index 84ad9d3c..50369df5 100644 --- a/tests/units/CarbonEmissionTest.php +++ b/tests/units/CarbonEmissionTest.php @@ -209,7 +209,7 @@ public function test_truncate_fails_when_not_logged_in() $this->assertEquals(1, countElementsInTable($table)); } - public function test_truncate_fails_when_user_has_all_required_rights() + public function test_truncate_succeeds_when_user_has_all_required_rights() { /** @var DBmysql $DB */ global $DB; From a62bc9999e059c4b1b5cbd6149c934b261942517 Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 3 Sep 2026 11:19:05 +0200 Subject: [PATCH 08/35] docs(test): document array --- tests/install/PluginInstallTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/install/PluginInstallTest.php b/tests/install/PluginInstallTest.php index f0bc92c0..3d4a0401 100644 --- a/tests/install/PluginInstallTest.php +++ b/tests/install/PluginInstallTest.php @@ -607,6 +607,10 @@ public function test_dashboard_is_configured() } } + /** + * Summary of zones + * @var array + */ private $zones = [ 'Afghanistan', 'Albania', From dc5a739252345913083beb36bcea37cd34f03e01 Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 3 Sep 2026 12:04:09 +0200 Subject: [PATCH 09/35] feat(Config): compatibility with GLPI 12 --- src/Config.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Config.php b/src/Config.php index 35aca4ca..03e728d6 100644 --- a/src/Config.php +++ b/src/Config.php @@ -147,6 +147,32 @@ public function showForm($ID, $options = []) return true; } + /** + * Set config values : create or update entry + * + * @param string $context context to get values (default for glpi is core) + * @param array $values config names to set + * + * @return void + */ + public static function setConfigurationValues($context, array $values = []) + { + GlpiConfig::setConfigurationValues($context, $values); + } + + /** + * Get config value + * + * @param $context string context to get values (default for glpi is core) + * @param $name string config name + * + * @return mixed + */ + public static function getConfigurationValue(string $context, string $name) + { + return GlpiConfig::getConfigurationValue($context, $name); + } + /** * Prepare input for configuration update * From 4a4139bdd416c4f9dfaca92b7b51674c9971e83b Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 4 Sep 2026 10:05:19 +0200 Subject: [PATCH 10/35] fix(SearchOptions): use subquery object instead of raw SQL requred to properly pass parameters for prepared queries --- src/SearchOptions.php | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/SearchOptions.php b/src/SearchOptions.php index 738952e7..774204f4 100644 --- a/src/SearchOptions.php +++ b/src/SearchOptions.php @@ -220,18 +220,22 @@ public static function getCoreSearchOptions(string $itemtype): array $fallback_carbon_intensity_subquery = Location::getCarbonIntensityDataSourceRequest([ Location::getTableField('id') => new QueryExpression('glpi_plugin_carbon_locations_3d6da7fccf9233a3f1a4e41183391a41.id'), ]); - $fallback_carbon_intensity_subquery = (new QuerySubQuery($fallback_carbon_intensity_subquery))->getQuery(); - $computation = "IF(`glpi_computers_id_963cd5e903dddc7ab00a3b70933369df`.`is_deleted` = 0 - AND `glpi_computers_id_963cd5e903dddc7ab00a3b70933369df`.`is_template` = 0 - AND `glpi_plugin_carbon_locations_3d6da7fccf9233a3f1a4e41183391a41`.`plugin_carbon_sources_zones_id` > 0 - AND `glpi_plugin_carbon_computerusageprofiles_09f8403aa14af64cd70f350288a0331b`.`id` > 0" - // Do not check if an asset is ignored - // . "AND COALESCE(`glpi_plugin_carbon_computertypes_a643ab3ffd70abf99533ed214da87d60`.`is_ignore`, 0) = 0" - . " AND ( - `glpi_plugin_carbon_computertypes_a643ab3ffd70abf99533ed214da87d60`.`power_consumption` > 0 - OR `glpi_computermodels`.`power_consumption` > 0 - ) - AND ($fallback_carbon_intensity_subquery) > 0, 1, 0)"; + $fallback_carbon_intensity_subquery = (new QuerySubQuery($fallback_carbon_intensity_subquery)); + $computation = new QueryExpression( + "IF(`glpi_computers_id_963cd5e903dddc7ab00a3b70933369df`.`is_deleted` = 0 + AND `glpi_computers_id_963cd5e903dddc7ab00a3b70933369df`.`is_template` = 0 + AND `glpi_plugin_carbon_locations_3d6da7fccf9233a3f1a4e41183391a41`.`plugin_carbon_sources_zones_id` > 0 + AND `glpi_plugin_carbon_computerusageprofiles_09f8403aa14af64cd70f350288a0331b`.`id` > 0" + // Do not check if an asset is ignored + // . "AND COALESCE(`glpi_plugin_carbon_computertypes_a643ab3ffd70abf99533ed214da87d60`.`is_ignore`, 0) = 0" + . " AND ( + `glpi_plugin_carbon_computertypes_a643ab3ffd70abf99533ed214da87d60`.`power_consumption` > 0 + OR `glpi_computermodels`.`power_consumption` > 0 + ) + AND ($fallback_carbon_intensity_subquery) > 0, 1, 0)", + null, + $fallback_carbon_intensity_subquery->getParams() + ); $sopt[] = [ 'id' => SearchOptions::IS_HISTORIZABLE, 'table' => getTableForItemType($itemtype), From 509673a882f45d888167920b160704ce811e26a5 Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 4 Sep 2026 10:44:34 +0200 Subject: [PATCH 11/35] chore(dependencies): update dependencies --- composer.json | 2 +- composer.lock | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index a0241e08..364efb73 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "php-http/message-factory": "^1.1" }, "require-dev": { - "glpi-project/tools": "^0.8.1", + "glpi-project/tools": "^0.8", "marcocesarato/php-conventional-changelog": "^1.17" }, "config": { diff --git a/composer.lock b/composer.lock index 3a534dbe..f5b53d20 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": "a821ef4c2ae32d2c6dce28a1a3b4ceb9", + "content-hash": "21e2edb08d5c5905a3ed659864e7682c", "packages": [ { "name": "clue/stream-filter", @@ -826,16 +826,16 @@ }, { "name": "symfony/console", - "version": "v6.4.44", + "version": "v6.4.45", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "60e3944c4859c487aa6ea2f0f7754917f70f7524" + "reference": "3b8473e0d14157f2d22b0a0d7259ad23483d1e6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/60e3944c4859c487aa6ea2f0f7754917f70f7524", - "reference": "60e3944c4859c487aa6ea2f0f7754917f70f7524", + "url": "https://api.github.com/repos/symfony/console/zipball/3b8473e0d14157f2d22b0a0d7259ad23483d1e6d", + "reference": "3b8473e0d14157f2d22b0a0d7259ad23483d1e6d", "shasum": "" }, "require": { @@ -900,7 +900,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.44" + "source": "https://github.com/symfony/console/tree/v6.4.45" }, "funding": [ { @@ -920,7 +920,7 @@ "type": "tidelift" } ], - "time": "2026-08-21T07:42:12+00:00" + "time": "2026-08-25T13:08:31+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1330,16 +1330,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.7.1", + "version": "v3.7.3", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", "shasum": "" }, "require": { @@ -1393,7 +1393,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" }, "funding": [ { @@ -1413,7 +1413,7 @@ "type": "tidelift" } ], - "time": "2026-06-16T09:55:08+00:00" + "time": "2026-07-27T15:39:01+00:00" }, { "name": "symfony/string", From af0ff6cf619a18d1909c42cf6e7f7dd6cb8d2f3c Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 4 Sep 2026 13:53:54 +0200 Subject: [PATCH 12/35] test: fix phpunit notices replace mocks with stubs in many places, because no expectations defined for these objects --- tests/units/CarbonIntensityTest.php | 12 ++++------- tests/units/ComputerTypeTest.php | 20 +++++-------------- tests/units/ComputerUsageProfileTest.php | 4 +--- .../CarbonIntensity/AbstractClientTest.php | 4 ++-- tests/units/Impact/Usage/EngineTest.php | 3 +-- tests/units/LocationTest.php | 6 ++---- tests/units/MonitorTypeTest.php | 12 +++-------- tests/units/NetworkEquipmentTypeTest.php | 12 +++-------- 8 files changed, 21 insertions(+), 52 deletions(-) diff --git a/tests/units/CarbonIntensityTest.php b/tests/units/CarbonIntensityTest.php index 932014d5..859df289 100644 --- a/tests/units/CarbonIntensityTest.php +++ b/tests/units/CarbonIntensityTest.php @@ -299,9 +299,7 @@ public function testGetDownloadStartDate() { $instance = new CarbonIntensity(); - $data_source = $this->getMockBuilder(AbstractClient::class) - ->getMock(); - $result = $instance->getDownloadStartDate('foo', $data_source); + $result = $instance->getDownloadStartDate(); $expected = (new DateTime('13 months ago'))->setTime(0, 0, 0); // CarbonIntensity::MIN_HISTORY_LENGTH $this->assertEquals($expected, $result); @@ -312,7 +310,7 @@ public function testGetDownloadStartDate() 'buy_date' => '2022-02-01', ]); - $result = $instance->getDownloadStartDate('foo', $data_source); + $result = $instance->getDownloadStartDate(); $expected = (new DateTime('2022-02-01'))->setTime(0, 0, 0); // CarbonIntensity::MIN_HISTORY_LENGTH $this->assertEquals($expected, $result); } @@ -366,8 +364,7 @@ public function testDownloadOneZone() $zone::getForeignKeyField() => $zone->getID(), ]); - $data_source = $this->getMockBuilder(AbstractClient::class) - ->getMock(); + $data_source = $this->createStub(AbstractClient::class); $hours = null; $data_source->method('fullDownload')->willReturnCallback( function ($zone_name, $gap_start, $gap_end, $carbon_intensity, $limit, $progress_bar) use (&$hours) { @@ -379,8 +376,7 @@ function ($zone_name, $gap_start, $gap_end, $carbon_intensity, $limit, $progress $data_source->method('getHardStartDate')->willReturn( DateTimeImmutable::createFromFormat(DateTimeInterface::ATOM, '2021-01-01T00:00:00+00:00'), ); - $output = $this->getMockBuilder(Output::class) - ->getMock(); + $output = $this->createStub(Output::class); $progress_bar = new ProgressBar($output); $instance = new CarbonIntensity(); diff --git a/tests/units/ComputerTypeTest.php b/tests/units/ComputerTypeTest.php index 89db56c3..d44560af 100644 --- a/tests/units/ComputerTypeTest.php +++ b/tests/units/ComputerTypeTest.php @@ -124,9 +124,7 @@ public function testUpdateCategory() public function testShowMassiveActionsSubForm() { // Test power consumption update form - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn('MassUpdatePower'); $massive_action->method('getItems')->willReturn([ ComputerType::class => $this->createItem(GlpiComputerType::class), @@ -153,9 +151,7 @@ public function testShowMassiveActionsSubForm() $this->assertTrue($result); // Test category update form - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn('MassUpdateCategory'); $massive_action->method('getItems')->willReturn([ ComputerType::class => $this->createItem(GlpiComputerType::class), @@ -181,9 +177,7 @@ public function testShowMassiveActionsSubForm() $this->assertTrue($result); // Test invalid action - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn(''); $massive_action->method('getItems')->willReturn([ ComputerType::class => $this->createItem(GlpiComputerType::class), @@ -200,9 +194,7 @@ public function testShowMassiveActionsSubForm() public function testProcessMassiveActionForOneItemtype() { // Test create power consumption - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn('MassUpdatePower'); $glpi_computer_type = $this->createItem(GlpiComputerType::class); $massive_action->POST = [ @@ -222,9 +214,7 @@ public function testProcessMassiveActionForOneItemtype() $this->assertEquals(25, $computer_type->fields['power_consumption']); // Test update category - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn('MassUpdateCategory'); $glpi_computer_type = $this->createItem(GlpiComputerType::class); $massive_action->POST = [ diff --git a/tests/units/ComputerUsageProfileTest.php b/tests/units/ComputerUsageProfileTest.php index b63e38a4..cd4a7503 100644 --- a/tests/units/ComputerUsageProfileTest.php +++ b/tests/units/ComputerUsageProfileTest.php @@ -180,9 +180,7 @@ public function testAssignToItem() public function testShowMassiveActionsSubForm() { // Test power consumption update form - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn('MassAssociateItems'); $massive_action->method('getItems')->willReturn([ GlpiComputer::class => $this->createItem(GlpiComputer::class), diff --git a/tests/units/DataSource/CarbonIntensity/AbstractClientTest.php b/tests/units/DataSource/CarbonIntensity/AbstractClientTest.php index 2ffe9ca7..aa8d7719 100644 --- a/tests/units/DataSource/CarbonIntensity/AbstractClientTest.php +++ b/tests/units/DataSource/CarbonIntensity/AbstractClientTest.php @@ -126,7 +126,7 @@ public function testSliceDateRangeByMonth() { foreach ($this->sliceDateRangeByMonthProvider() as $data) { [$start, $stop, $expected] = $data; - $stub = $this->getMockBuilder(AbstractClient::class)->getMock(); + $stub = $this->createStub(AbstractClient::class); $output = $this->callPrivateMethod($stub, 'sliceDateRangeByMonth', $start, $stop); if (count($expected) === 0) { @@ -175,7 +175,7 @@ public function testSliceDateRangeByDay() { foreach ($this->sliceDateRangeByDayProvider() as $data) { [$start, $stop, $expected] = $data; - $stub = $this->getMockBuilder(AbstractClient::class)->getMock(); + $stub = $this->createStub(AbstractClient::class); $output = $this->callPrivateMethod($stub, 'sliceDateRangeByDay', $start, $stop); if (count($expected) === 0) { diff --git a/tests/units/Impact/Usage/EngineTest.php b/tests/units/Impact/Usage/EngineTest.php index 69ba8e39..adbc725b 100644 --- a/tests/units/Impact/Usage/EngineTest.php +++ b/tests/units/Impact/Usage/EngineTest.php @@ -54,8 +54,7 @@ public function testGetEngineFromItemtypeForBoavizta() $version_response = [ '1.3.11', ]; - $client_stub = $this->getMockBuilder(RestApiClient::class) - ->getMock(); + $client_stub = $this->createStub(RestApiClient::class); $client_stub->method('request')->willReturn($version_response); $item = $this->createItem(GlpiComputer::class); diff --git a/tests/units/LocationTest.php b/tests/units/LocationTest.php index 45a87514..99bc93b4 100644 --- a/tests/units/LocationTest.php +++ b/tests/units/LocationTest.php @@ -116,8 +116,7 @@ public function testOnGlpiLocationAdd() $this->assertEquals('FRA', $location->fields['boavizta_zone']); // Test the geocoding feature - $geocoder = $this->getMockBuilder(Geocoder::class) - ->getMock(); + $geocoder = $this->createStub(Geocoder::class); $geocoder->method('geocodeQuery')->willReturn( new AddressCollection([ new NominatimAddress( @@ -169,8 +168,7 @@ public function testOnGlpiLocationPreUpdate() $this->assertEquals('FRA', $location->fields['boavizta_zone']); // Test the geocoding feature - $geocoder = $this->getMockBuilder(Geocoder::class) - ->getMock(); + $geocoder = $this->createStub(Geocoder::class); $geocoder->method('geocodeQuery')->willReturn( new AddressCollection([ new NominatimAddress( diff --git a/tests/units/MonitorTypeTest.php b/tests/units/MonitorTypeTest.php index 49010bfc..ccd4fa83 100644 --- a/tests/units/MonitorTypeTest.php +++ b/tests/units/MonitorTypeTest.php @@ -78,9 +78,7 @@ public function testUpdatePowerConsumption() public function testShowMassiveActionsSubForm() { - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn('MassUpdatePower'); $massive_action->method('getItems')->willReturn([ MonitorType::class => $this->createItem(GlpiMonitorType::class), @@ -106,9 +104,7 @@ public function testShowMassiveActionsSubForm() }); $this->assertTrue($result); - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn(''); $massive_action->method('getItems')->willReturn([ MonitorType::class => $this->createItem(GlpiMonitorType::class), @@ -125,9 +121,7 @@ public function testShowMassiveActionsSubForm() public function testProcessMassiveActionForOneItemtype() { // Test update power consumption - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn('MassUpdatePower'); $glpi_monitor_type = $this->createItem(GlpiMonitorType::class); $massive_action->POST = [ diff --git a/tests/units/NetworkEquipmentTypeTest.php b/tests/units/NetworkEquipmentTypeTest.php index c502edee..782dbf6d 100644 --- a/tests/units/NetworkEquipmentTypeTest.php +++ b/tests/units/NetworkEquipmentTypeTest.php @@ -78,9 +78,7 @@ public function testUpdatePowerConsumption() public function testShowMassiveActionsSubForm() { - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn('MassUpdatePower'); $massive_action->method('getItems')->willReturn([ NetworkEquipmentType::class => $this->createItem(GlpiNetworkEquipmentType::class), @@ -106,9 +104,7 @@ public function testShowMassiveActionsSubForm() }); $this->assertTrue($result); - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn(''); $massive_action->method('getItems')->willReturn([ NetworkEquipmentType::class => $this->createItem(GlpiNetworkEquipmentType::class), @@ -125,9 +121,7 @@ public function testShowMassiveActionsSubForm() public function testProcessMassiveActionForOneItemtype() { // Test update power consumption - $massive_action = $this->getMockBuilder(MassiveAction::class) - ->disableOriginalConstructor() - ->getMock(); + $massive_action = $this->createStub(MassiveAction::class); $massive_action->method('getAction')->willReturn('MassUpdatePower'); $glpi_networkequipment_type = $this->createItem(GlpiNetworkEquipmentType::class); $networkequipment_type = $this->createItem(NetworkEquipmentType::class, [ From c6858fe2c193022a1e45de08e30552c0405d6eec Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 4 Sep 2026 14:08:01 +0200 Subject: [PATCH 13/35] test: remove deprecated attributes fir phpunit --- tests/migration/migration_to_1_2_0_Test.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/migration/migration_to_1_2_0_Test.php b/tests/migration/migration_to_1_2_0_Test.php index b2af6b9f..ab29691d 100644 --- a/tests/migration/migration_to_1_2_0_Test.php +++ b/tests/migration/migration_to_1_2_0_Test.php @@ -38,7 +38,6 @@ use GlpiPlugin\Carbon\Uninstall; use Location as GlpiLocation; use Migration; -use PHPUnit\Framework\Attributes\CoversNothing; use Plugin; class migration_to_1_2_0_Test extends CommonTestCase @@ -60,7 +59,6 @@ public function setUp(): void $success = $DB->runFile(realpath($sql_file)); } - #[CoversNothing] public function testUpdateCountryLocationZoneRelation() { /** @var DBmysql $DB */ @@ -110,7 +108,6 @@ public function testUpdateCountryLocationZoneRelation() $this->assertEquals($expected, $result->current()); } - #[CoversNothing] public function testUpdateStateLocationZoneRelation() { /** @var DBmysql $DB */ @@ -159,7 +156,6 @@ public function testUpdateStateLocationZoneRelation() $this->assertEquals($expected, $result->current()); } - #[CoversNothing] public function testTimeformatInUsageProfile() { global $DB; From 28bdb74ff746aa506df172df11fa0e0c7df74479 Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 4 Sep 2026 13:19:12 +0000 Subject: [PATCH 14/35] fix(install): newline after completing fallback data installation --- install/install/init_datasources.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install/install/init_datasources.php b/install/install/init_datasources.php index a195c0fa..b70935e7 100644 --- a/install/install/init_datasources.php +++ b/install/install/init_datasources.php @@ -125,7 +125,9 @@ $progress_bar->setProgress($rows_count); } $file = null; // close the file - + if (isset($output)) { + $output->writeln(""); + } Config::setPluginConfigurationValues(['ember_dataset_date' => EMBER_DATASET_DATE]); } From 251c8f08fe7d90a95998e85f51e508992744d70a Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 4 Sep 2026 15:26:45 +0200 Subject: [PATCH 15/35] chore: remove obsolete dependency --- composer.json | 1 - composer.lock | 184 +++++++------------------------------------------- phpstan.neon | 7 +- 3 files changed, 29 insertions(+), 163 deletions(-) diff --git a/composer.json b/composer.json index 364efb73..8cba91a4 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,6 @@ "php-http/message-factory": "^1.1" }, "require-dev": { - "glpi-project/tools": "^0.8", "marcocesarato/php-conventional-changelog": "^1.17" }, "config": { diff --git a/composer.lock b/composer.lock index f5b53d20..547f38e4 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": "21e2edb08d5c5905a3ed659864e7682c", + "content-hash": "108f5d181fd03cee818d694c22fa7d8b", "packages": [ { "name": "clue/stream-filter", @@ -637,62 +637,6 @@ } ], "packages-dev": [ - { - "name": "glpi-project/tools", - "version": "0.8.3", - "source": { - "type": "git", - "url": "https://github.com/glpi-project/tools.git", - "reference": "8ea2a7d4702a858f4b0360ba7d4f1841a5e77026" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/glpi-project/tools/zipball/8ea2a7d4702a858f4b0360ba7d4f1841a5e77026", - "reference": "8ea2a7d4702a858f4b0360ba7d4f1841a5e77026", - "shasum": "" - }, - "require": { - "symfony/console": "^5.4 || ^6.0", - "twig/twig": "^3.3" - }, - "require-dev": { - "nikic/php-parser": "^4.13", - "phpstan/phpstan-src": "^1.10" - }, - "bin": [ - "bin/extract-locales", - "bin/licence-headers-check", - "tools/plugin-release" - ], - "type": "library", - "autoload": { - "psr-4": { - "GlpiProject\\Tools\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-3.0-or-later" - ], - "authors": [ - { - "name": "Teclib'", - "email": "glpi@teclib.com", - "homepage": "http://teclib-group.com" - } - ], - "description": "Various tools for GLPI and its plugins", - "keywords": [ - "glpi", - "plugins", - "tools" - ], - "support": { - "issues": "https://github.com/glpi-project/tools/issues", - "source": "https://github.com/glpi-project/tools" - }, - "time": "2025-10-14T10:26:06+00:00" - }, { "name": "marcocesarato/php-conventional-changelog", "version": "1.17.3", @@ -826,47 +770,47 @@ }, { "name": "symfony/console", - "version": "v6.4.45", + "version": "v7.4.18", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3b8473e0d14157f2d22b0a0d7259ad23483d1e6d" + "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3b8473e0d14157f2d22b0a0d7259ad23483d1e6d", - "reference": "3b8473e0d14157f2d22b0a0d7259ad23483d1e6d", + "url": "https://api.github.com/repos/symfony/console/zipball/23d6f88a29f6d0eac45bd77d70307adf83ba7ab0", + "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^7.2|^8.0" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -900,7 +844,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.45" + "source": "https://github.com/symfony/console/tree/v7.4.18" }, "funding": [ { @@ -920,7 +864,7 @@ "type": "tidelift" } ], - "time": "2026-08-25T13:08:31+00:00" + "time": "2026-08-25T14:18:37+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1505,86 +1449,6 @@ } ], "time": "2026-07-28T07:33:02+00:00" - }, - { - "name": "twig/twig", - "version": "v3.28.0", - "source": { - "type": "git", - "url": "https://github.com/twigphp/Twig.git", - "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", - "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", - "shasum": "" - }, - "require": { - "php": ">=8.1.0", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3" - }, - "require-dev": { - "php-cs-fixer/shim": "^3.0@stable", - "phpstan/phpstan": "^2.0@stable", - "psr/container": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" - }, - "type": "library", - "autoload": { - "files": [ - "src/Resources/core.php", - "src/Resources/debug.php", - "src/Resources/escaper.php", - "src/Resources/string_loader.php" - ], - "psr-4": { - "Twig\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Twig Team", - "role": "Contributors" - }, - { - "name": "Armin Ronacher", - "email": "armin.ronacher@active-4.com", - "role": "Project Founder" - } - ], - "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "https://twig.symfony.com", - "keywords": [ - "templating" - ], - "support": { - "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.28.0" - }, - "funding": [ - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/twig/twig", - "type": "tidelift" - } - ], - "time": "2026-07-03T20:44:34+00:00" } ], "aliases": [], diff --git a/phpstan.neon b/phpstan.neon index c02769b3..2d67de9b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,8 @@ +includes: + - ../../vendor/glpi-project/phpstan-glpi/extension.neon + - ../../vendor/phpstan/phpstan-deprecation-rules/rules.neon + - ../../vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon + parameters: parallel: maximumNumberOfProcesses: 2 @@ -17,5 +22,3 @@ parameters: - ../../src stubFiles: - ../../stubs/glpi_constants.php -rules: - - GlpiProject\Tools\PHPStan\Rules\GlobalVarTypeRule From e94e3a7f0b0fc09a7545cb788497e833492d3873 Mon Sep 17 00:00:00 2001 From: btry Date: Tue, 8 Sep 2026 14:52:37 +0200 Subject: [PATCH 16/35] chore: add Makefile --- Makefile | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..d4cfadbf --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +include ../../PluginsMakefile.mk + From 617e0f773c980c88d2902bef58052d15198232c7 Mon Sep 17 00:00:00 2001 From: btry Date: Tue, 8 Sep 2026 14:52:22 +0200 Subject: [PATCH 17/35] feat: GLPI 12 compatibility --- ajax/dropdownZone.php | 13 ++--- ajax/toggleZoneDownload.php | 44 +++++---------- front/embodiedimpact.form.php | 6 +- front/report.php | 3 +- front/usageimpact.form.php | 6 +- hook.php | 6 ++ install/Install.php | 5 ++ install/install/create_automatic_actions.php | 2 +- install/install/create_dashboard.php | 4 +- install/install/init_datasources.php | 3 +- install/migration/update_1.0.0_to_1.0.1.php | 3 + install/migration/update_1.0.1_to_1.1.0.php | 3 + install/migration/update_1.1.1_to_1.2.0.php | 2 + .../09_add_impact_criterias.php | 3 + install/migration/update_x.x.x_to_y.y.y.php | 3 + phpstan.neon | 1 + setup.php | 5 +- src/CarbonIntensity.php | 4 +- src/Command/CollectCarbonIntensityCommand.php | 2 + .../CreateFakeCarbonIntensityCommand.php | 2 +- src/Command/CreateTestInventoryCommand.php | 4 ++ src/Command/ExportDashboardCommand.php | 19 +++++-- src/Command/ImportDashboardCommand.php | 2 +- src/CommonAsset.php | 4 ++ src/ComputerUsageProfile.php | 9 ++- src/Config.php | 2 + src/CronTask.php | 15 ++++- src/Dashboard/DemoProvider.php | 4 +- src/Dashboard/Grid.php | 15 +++-- src/Dashboard/Provider.php | 6 +- src/Dashboard/Widget.php | 4 +- .../CarbonIntensity/AbstractClient.php | 4 +- .../CarbonIntensity/ClientFactory.php | 5 ++ .../CarbonIntensity/ClientInterface.php | 2 +- .../ElectricityMaps/Client.php | 16 ++++-- src/DataSource/CarbonIntensity/Rte/Client.php | 8 ++- src/DataSource/CronTaskInterface.php | 7 +++ src/DataSource/CronTaskProvider.php | 40 +++++++++++--- src/DataSource/Lca/Boaviztapi/Client.php | 2 + src/DataSource/Lca/Boaviztapi/Config.php | 9 ++- src/DataSource/Lca/ClientFactory.php | 5 ++ src/DataSource/RestApiClient.php | 2 + src/EmbodiedImpact.php | 37 ++++++++----- .../Embodied/Boavizta/AbstractAsset.php | 2 + src/Impact/History/AbstractAsset.php | 2 +- src/Impact/Usage/Boavizta/AbstractAsset.php | 2 + src/Report.php | 7 ++- src/Toolbox.php | 31 +++++++---- src/UsageInfo.php | 5 +- tests/fixtures/FakeDataSources.php | 36 ++++++++++++ tests/units/CarbonIntensityTest.php | 4 +- .../Command/ImportDashboardCommandTest.php | 2 +- .../CarbonIntensity/AbstractClientTest.php | 2 +- .../ElectricityMaps/ClientTest.php | 2 +- .../CarbonIntensity/Rte/ClientTest.php | 2 +- .../units/DataSource/CronTaskProviderTest.php | 55 +++++++++++++++---- tests/units/Impact/History/ComputerTest.php | 5 +- tests/units/Impact/History/MonitorTest.php | 3 +- .../Impact/History/NetworkEquipmentTest.php | 2 +- tests/units/ToolboxTest.php | 4 +- 60 files changed, 366 insertions(+), 141 deletions(-) diff --git a/ajax/dropdownZone.php b/ajax/dropdownZone.php index 1ed765d2..0e252231 100644 --- a/ajax/dropdownZone.php +++ b/ajax/dropdownZone.php @@ -29,7 +29,8 @@ * * ------------------------------------------------------------------------- */ - +use Glpi\Exception\Http\AccessDeniedHttpException; +use Glpi\Exception\Http\NotFoundHttpException; use GlpiPlugin\Carbon\Source_Zone; use GlpiPlugin\Carbon\Zone; @@ -37,13 +38,9 @@ // Check if plugin is activated... if (!Plugin::isPluginActive('carbon')) { - http_response_code(404); - die(); -} - -if (!Zone::canView()) { - http_response_code(403); - die(); + throw new NotFoundHttpException(); +} elseif (!Zone::canView()) { + throw new AccessDeniedHttpException(); } $source_zone_table = Source_Zone::getTable(); diff --git a/ajax/toggleZoneDownload.php b/ajax/toggleZoneDownload.php index a3f8fe0a..4239da82 100644 --- a/ajax/toggleZoneDownload.php +++ b/ajax/toggleZoneDownload.php @@ -29,8 +29,10 @@ * * ------------------------------------------------------------------------- */ - use Config as GlpiConfig; +use Glpi\Exception\Http\AccessDeniedHttpException; +use Glpi\Exception\Http\BadRequestHttpException; +use Glpi\Exception\Http\NotFoundHttpException; use GlpiPlugin\Carbon\Source; use GlpiPlugin\Carbon\Source_Zone; @@ -38,32 +40,16 @@ // Check if plugin is activated... if (!Plugin::isPluginActive('carbon')) { - echo __('Not found.', 'carbon'); - http_response_code(404); - die(); -} - -if (!Source::canView() || ! GlpiConfig::canUpdate()) { - // Will die - echo __('Access denied.', 'carbon'); - http_response_code(403); - die(); -} - -if (!isset($_GET['id'])) { - echo __('Bad request.', 'carbon'); - http_response_code(400); - die(); -} - -$source_zone = new Source_Zone(); -if (!$source_zone->getFromDB($_GET['id'])) { - echo __('Item not found.', 'carbon'); - http_response_code(403); - die(); -} -if (!$source_zone->toggleZone()) { - echo __('Update failed.', 'carbon'); - http_response_code(500); - die(); + throw new NotFoundHttpException(); +} elseif (!Source::canView() || !GlpiConfig::canUpdate()) { + throw new AccessDeniedHttpException(); +} elseif (!isset($_GET['id'])) { + throw new BadRequestHttpException(); +} else { + $source_zone = new Source_Zone(); + if (!$source_zone->getFromDB($_GET['id'])) { + throw new BadRequestHttpException(); + } elseif (!$source_zone->toggleZone()) { + throw new BadRequestHttpException(); + } } diff --git a/front/embodiedimpact.form.php b/front/embodiedimpact.form.php index 9f402902..688a3d1a 100644 --- a/front/embodiedimpact.form.php +++ b/front/embodiedimpact.form.php @@ -75,6 +75,10 @@ $embodied_impact->check($_POST['id'], PURGE); $itemtype = $embodied_impact->fields['itemtype']; + if (!is_a($itemtype, CommonDBTM::class, true)) { + Session::addMessageAfterRedirect(__('Bad arguments.', 'carbon'), false, ERROR); + Html::back(); + } $item = new $itemtype(); $item->getFromDB($embodied_impact->fields['items_id']); if (!$item->canUpdateItem()) { @@ -92,7 +96,7 @@ } $itemtype = $_POST['itemtype']; - if (!Toolbox::isCommonDBTM($itemtype)) { + if (!is_a($itemtype, CommonDBTM::class, true)) { Session::addMessageAfterRedirect(__('Bad arguments.', 'carbon'), false, ERROR); Html::back(); } diff --git a/front/report.php b/front/report.php index b309e0e8..ad519ab3 100644 --- a/front/report.php +++ b/front/report.php @@ -30,6 +30,7 @@ * ------------------------------------------------------------------------- */ +use Config as GlpiConfig; use Glpi\Exception\Http\AccessDeniedHttpException; use Glpi\Exception\Http\NotFoundHttpException; use GlpiPlugin\Carbon\Config; @@ -48,7 +49,7 @@ } if (isset($_GET['disable_demo'])) { - Session::checkRight('config', UPDATE); + Session::checkRight(GlpiConfig::$rightname, UPDATE); Config::exitDemoMode(); Html::back(); } diff --git a/front/usageimpact.form.php b/front/usageimpact.form.php index 94d3650b..97105cef 100644 --- a/front/usageimpact.form.php +++ b/front/usageimpact.form.php @@ -92,6 +92,10 @@ /** @var AbstractAsset $history */ $gwp_impact = new $gwp_impact_class(); $itemtype = $gwp_impact->getItemtype(); + if (!is_a($itemtype, CommonDBTM::class, true)) { + Session::addMessageAfterRedirect(__('Bad arguments.', 'carbon'), false, ERROR); + Html::back(); + } $item = new $itemtype(); $item->getFromDB($_POST['items_id']); if (!$item->canUpdateItem()) { @@ -110,7 +114,7 @@ } $itemtype = $_POST['itemtype']; - if (!Toolbox::isCommonDBTM($itemtype)) { + if (!is_a($itemtype, CommonDBTM::class, true)) { Session::addMessageAfterRedirect(__('Bad arguments.', 'carbon'), false, ERROR); Html::back(); } diff --git a/hook.php b/hook.php index 54d99277..c0578f0c 100644 --- a/hook.php +++ b/hook.php @@ -60,6 +60,9 @@ use Profile as GlpiProfile; use Toolbox as GlpiToolbox; +use function Safe\ob_end_clean; +use function Safe\ob_start; + /** * Plugin install process * supported arguments for upgrade process @@ -289,6 +292,9 @@ function plugin_carbon_hook_pre_purge_assettype(CommonDBTM $item) } $carbon_type_itemtype = 'GlpiPlugin\\Carbon\\' . $itemtype; + if (!is_a($carbon_type_itemtype, CommonDBTM::class, true)) { + return; + } $carbon_type = new $carbon_type_itemtype(); $carbon_type->deleteByCriteria([ $item->getForeignKeyField() => $item->getID(), diff --git a/install/Install.php b/install/Install.php index bee40254..9e55c689 100644 --- a/install/Install.php +++ b/install/Install.php @@ -39,6 +39,11 @@ use Plugin; use RuntimeException; +use function Safe\ini_set; +use function Safe\preg_match; +use function Safe\preg_replace; +use function Safe\scandir; + class Install { /** diff --git a/install/install/create_automatic_actions.php b/install/install/create_automatic_actions.php index edef8bb2..9d48cd74 100644 --- a/install/install/create_automatic_actions.php +++ b/install/install/create_automatic_actions.php @@ -34,7 +34,7 @@ use GlpiPlugin\Carbon\CronTask; use GlpiPlugin\Carbon\DataSource\CronTaskProvider; -$cron_task_classes = CronTaskProvider::getCronTaskTypes(); +$cron_task_classes = CronTaskProvider::getCronTaskTypes(CronTaskProvider::getCronTaskDirectories()); $automatic_actions = []; foreach ($cron_task_classes as $cron_task_class) { $automatic_actions = array_merge($automatic_actions, $cron_task_class::enumerateTasks()); diff --git a/install/install/create_dashboard.php b/install/install/create_dashboard.php index ff2b2ec6..28e4287d 100644 --- a/install/install/create_dashboard.php +++ b/install/install/create_dashboard.php @@ -29,13 +29,15 @@ * * ------------------------------------------------------------------------- */ - use Glpi\Dashboard\Dashboard; use Glpi\Dashboard\Item as DashboardItem; use Glpi\Dashboard\Right as DashboardRight; use Glpi\DBAL\QueryExpression; use GlpiPlugin\Carbon\Report; +use function Safe\file_get_contents; +use function Safe\json_decode; + /** @var DBmysql $DB */ global $DB; diff --git a/install/install/init_datasources.php b/install/install/init_datasources.php index b70935e7..fd033c36 100644 --- a/install/install/init_datasources.php +++ b/install/install/init_datasources.php @@ -29,7 +29,6 @@ * * ------------------------------------------------------------------------- */ - use GlpiPlugin\Carbon\CarbonIntensity; use GlpiPlugin\Carbon\Config; use GlpiPlugin\Carbon\Install; @@ -39,6 +38,8 @@ use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\StreamOutput; +use function Safe\fopen; + /** @var DBmysql $DB */ global $DB; diff --git a/install/migration/update_1.0.0_to_1.0.1.php b/install/migration/update_1.0.0_to_1.0.1.php index ba4ba692..c11f8b44 100644 --- a/install/migration/update_1.0.0_to_1.0.1.php +++ b/install/migration/update_1.0.0_to_1.0.1.php @@ -30,6 +30,9 @@ * ------------------------------------------------------------------------- */ +use function Safe\preg_match; +use function Safe\scandir; + function update100to101(Migration $migration) { /** @var DBmysql $DB */ diff --git a/install/migration/update_1.0.1_to_1.1.0.php b/install/migration/update_1.0.1_to_1.1.0.php index 19714136..2b16446d 100644 --- a/install/migration/update_1.0.1_to_1.1.0.php +++ b/install/migration/update_1.0.1_to_1.1.0.php @@ -30,6 +30,9 @@ * ------------------------------------------------------------------------- */ +use function Safe\preg_match; +use function Safe\scandir; + function update101to110(Migration $migration) { /** @var DBmysql $DB */ diff --git a/install/migration/update_1.1.1_to_1.2.0.php b/install/migration/update_1.1.1_to_1.2.0.php index fd3d0e80..9050252f 100644 --- a/install/migration/update_1.1.1_to_1.2.0.php +++ b/install/migration/update_1.1.1_to_1.2.0.php @@ -29,6 +29,8 @@ * * ------------------------------------------------------------------------- */ +use function Safe\preg_match; +use function Safe\scandir; function update111to120(Migration $migration) { diff --git a/install/migration/update_1.1.1_to_1.2.0/09_add_impact_criterias.php b/install/migration/update_1.1.1_to_1.2.0/09_add_impact_criterias.php index 6b3d5a93..079261e8 100644 --- a/install/migration/update_1.1.1_to_1.2.0/09_add_impact_criterias.php +++ b/install/migration/update_1.1.1_to_1.2.0/09_add_impact_criterias.php @@ -35,6 +35,9 @@ use Glpi\Dashboard\Item as DashboardItem; +use function Safe\json_decode; +use function Safe\json_encode; + $new_criteria = [ 'gwppb' => '(unit g CO2 eq) Biogenic climate change potential', 'gwppf' => '(unit g CO2 eq) Fossil climate change potential', diff --git a/install/migration/update_x.x.x_to_y.y.y.php b/install/migration/update_x.x.x_to_y.y.y.php index 3c5899ff..8deb05c6 100644 --- a/install/migration/update_x.x.x_to_y.y.y.php +++ b/install/migration/update_x.x.x_to_y.y.y.php @@ -30,6 +30,9 @@ * ------------------------------------------------------------------------- */ +use function Safe\preg_match; +use function Safe\scandir; + function update001to100(Migration $migration) { /** @var DBmysql $DB */ diff --git a/phpstan.neon b/phpstan.neon index 2d67de9b..8580ab79 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,6 +10,7 @@ parameters: bootstrapFiles: - ../../stubs/glpi_constants.php - ../../vendor/autoload.php + - setup.php paths: - src - front diff --git a/setup.php b/setup.php index cdd158e7..1138b3db 100644 --- a/setup.php +++ b/setup.php @@ -46,6 +46,9 @@ use Location as GlpiLocation; use Profile as GlpiProfile; +use function Safe\define; +use function Safe\preg_match; + // Version of the plugin (major.minor.bugfix) define('PLUGIN_CARBON_VERSION', '1.4.0-dev'); // Schema version of this version (major.minor.bugfix) @@ -123,7 +126,7 @@ function plugin_carbon_setupHooks() $PLUGIN_HOOKS[Hooks::DASHBOARD_DEFAULTS]['carbon'] = [Grid::class, 'getDefaults']; } - if (Session::haveRight('config', UPDATE)) { + if (Session::haveRight(GlpiConfig::$rightname, UPDATE)) { $PLUGIN_HOOKS['config_page']['carbon'] = 'front/config.form.php'; } diff --git a/src/CarbonIntensity.php b/src/CarbonIntensity.php index 99d3a7b3..7851a359 100644 --- a/src/CarbonIntensity.php +++ b/src/CarbonIntensity.php @@ -34,14 +34,14 @@ use CommonDropdown; use DateInterval; -use DateTime; -use DateTimeImmutable; use DateTimeInterface; use DBmysql; use Exception; use Glpi\DBAL\QueryParam; use GlpiPlugin\Carbon\DataSource\CarbonIntensity\ClientInterface; use Override; +use Safe\DateTime; +use Safe\DateTimeImmutable; use Symfony\Component\Console\Helper\ProgressBar; /** diff --git a/src/Command/CollectCarbonIntensityCommand.php b/src/Command/CollectCarbonIntensityCommand.php index 90cdfab2..0b92bd2e 100644 --- a/src/Command/CollectCarbonIntensityCommand.php +++ b/src/Command/CollectCarbonIntensityCommand.php @@ -52,6 +52,8 @@ use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; +use function Safe\define; + // 6 months define('DATE_MIN', 'P6M'); diff --git a/src/Command/CreateFakeCarbonIntensityCommand.php b/src/Command/CreateFakeCarbonIntensityCommand.php index afb5855e..339de6d7 100644 --- a/src/Command/CreateFakeCarbonIntensityCommand.php +++ b/src/Command/CreateFakeCarbonIntensityCommand.php @@ -33,12 +33,12 @@ namespace GlpiPlugin\Carbon\Command; use DateInterval; -use DateTime; use DateTimeZone; use GlpiPlugin\Carbon\CarbonIntensity; use GlpiPlugin\Carbon\Source; use GlpiPlugin\Carbon\Zone; use Override; +use Safe\DateTime; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; diff --git a/src/Command/CreateTestInventoryCommand.php b/src/Command/CreateTestInventoryCommand.php index 882f4d50..bccd34c0 100644 --- a/src/Command/CreateTestInventoryCommand.php +++ b/src/Command/CreateTestInventoryCommand.php @@ -40,6 +40,7 @@ use GlpiPlugin\Carbon\ComputerType; use GlpiPlugin\Carbon\ComputerUsageProfile; use GlpiPlugin\Carbon\UsageInfo; +use InvalidArgumentException; use Location; use Override; use Symfony\Component\Console\Command\Command; @@ -161,6 +162,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function createItemIfNotExist(string $item_type, array $crit, ?array $input = null): CommonDBTM { + if (!is_a($item_type, CommonDBTM::class, true)) { + throw new InvalidArgumentException("Item type $item_type is not a subclass of CommonDBTM"); + } $item = new $item_type(); $ret = $item->getFromDBByCrit($crit); diff --git a/src/Command/ExportDashboardCommand.php b/src/Command/ExportDashboardCommand.php index 988250bb..061e14a2 100644 --- a/src/Command/ExportDashboardCommand.php +++ b/src/Command/ExportDashboardCommand.php @@ -38,10 +38,15 @@ use Glpi\Dashboard\Item; use Override; use Plugin; +use Safe\Exceptions\FilesystemException; +use Safe\Exceptions\JsonException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function Safe\file_put_contents; +use function Safe\json_encode; + class ExportDashboardCommand extends Command { private OutputInterface $output; @@ -93,10 +98,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->dashboard_description[] = $row; } - file_put_contents( - $this->output_path, - json_encode($this->dashboard_description, JSON_PRETTY_PRINT) - ); + try { + file_put_contents( + $this->output_path, + json_encode($this->dashboard_description, JSON_PRETTY_PRINT) + ); + } catch (FilesystemException|JsonException $e) { + $message = __('Error while saving dashboard description', 'carbon'); + $this->output->writeln("$message"); + return Command::FAILURE; + } $message = sprintf(__('Dashboard description saved to %s', 'carbon'), $this->output_path); $this->output->writeln("$message"); diff --git a/src/Command/ImportDashboardCommand.php b/src/Command/ImportDashboardCommand.php index fc605bf2..1d2e986b 100644 --- a/src/Command/ImportDashboardCommand.php +++ b/src/Command/ImportDashboardCommand.php @@ -52,7 +52,7 @@ protected function configure() } #[Override] - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->output = $output; diff --git a/src/CommonAsset.php b/src/CommonAsset.php index bc50614f..fa1df555 100644 --- a/src/CommonAsset.php +++ b/src/CommonAsset.php @@ -33,6 +33,7 @@ namespace GlpiPlugin\Carbon; use CommonDBTM; +use GlpiPlugin\Carbon\Impact\History\AbstractAsset; use Html; use MassiveAction; use Override; @@ -108,6 +109,9 @@ public static function deleteEmbodiedImpact(CommonDBTM $item): bool public static function deleteUsageImpact(CommonDBTM $item): bool { $gwp_impact_class = '\\GlpiPlugin\\Carbon\\Impact\\History\\' . get_class($item); + if (!is_a($gwp_impact_class, AbstractAsset::class, true)) { + return false; + } $gwp_impact = new $gwp_impact_class(); $success = $gwp_impact->resetForItem($item->getID()); diff --git a/src/ComputerUsageProfile.php b/src/ComputerUsageProfile.php index 9b84a490..a16444ea 100644 --- a/src/ComputerUsageProfile.php +++ b/src/ComputerUsageProfile.php @@ -40,8 +40,11 @@ use Html; use MassiveAction; use Override; +use Safe\Exceptions\PcreException; use Session; +use function Safe\preg_match; + /** * Usage profile of a computer */ @@ -141,7 +144,11 @@ protected function inputIntegrityCheck(array $input): array protected function isValidTime(string $time): bool { $time_pattern = '/^(([01]\d|2[0-3]):[0-5]\d)|(24:00)$/'; - $found = preg_match($time_pattern, $time, $matches); + try { + $found = preg_match($time_pattern, $time, $matches); + } catch (PcreException $e) { + return false; + } return ($found === 1); } diff --git a/src/Config.php b/src/Config.php index 03e728d6..2266713b 100644 --- a/src/Config.php +++ b/src/Config.php @@ -52,6 +52,8 @@ use Session; use Twig\Extension\StringLoaderExtension; +use function Safe\json_encode; + class Config extends GlpiConfig { /** diff --git a/src/CronTask.php b/src/CronTask.php index 674a11a1..12b9d86f 100644 --- a/src/CronTask.php +++ b/src/CronTask.php @@ -39,6 +39,7 @@ use Geocoder\Exception\QuotaExceeded; use GlpiPlugin\Carbon\DataSource\CarbonIntensity\ClientFactory; use GlpiPlugin\Carbon\DataSource\CarbonIntensity\ClientInterface; +use GlpiPlugin\Carbon\DataSource\CronTaskInterface; use GlpiPlugin\Carbon\DataSource\CronTaskProvider; use GlpiPlugin\Carbon\Impact\Embodied\Engine as EmbodiedEngine; use GlpiPlugin\Carbon\Impact\History\AssetInterface; @@ -60,7 +61,10 @@ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) if (!$item instanceof GlpiCronTask) { return ''; } - if (!in_array($item->fields['itemtype'], CronTaskProvider::getCronTaskTypes())) { + if (!in_array($item->fields['itemtype'], CronTaskProvider::getCronTaskTypes(CronTaskProvider::getCronTaskDirectories()))) { + return ''; + } + if (!is_a($item->fields['itemtype'], CronTaskInterface::class, true)) { return ''; } $client_cron_task = new $item->fields['itemtype'](); @@ -81,7 +85,10 @@ public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $ public function showForCronTask(CommonDBTM $item) { $itemtype = $item->fields['itemtype']; - if (!in_array($itemtype, CronTaskProvider::getCronTaskTypes())) { + if (!in_array($itemtype, CronTaskProvider::getCronTaskTypes(CronTaskProvider::getCronTaskDirectories()))) { + return; + } + if (!is_a($itemtype, CronTaskInterface::class, true)) { return; } $crontask = new $itemtype(); @@ -151,7 +158,9 @@ public static function cronUsageImpact(GlpiCronTask $task): int // Calculate GWP $count = 0; foreach ($usage_impacts as $usage_impact_type) { - /** @var AssetInterface $usage_impact */ + if (!is_a($usage_impact_type, AssetInterface::class, true)) { + continue; + } $usage_impact = new $usage_impact_type(); $usage_impact->setLimit($limit_per_type); $count = $usage_impact->evaluateItems($usage_impact->getItemsToEvaluate()); diff --git a/src/Dashboard/DemoProvider.php b/src/Dashboard/DemoProvider.php index 183c1e46..0103fe00 100644 --- a/src/Dashboard/DemoProvider.php +++ b/src/Dashboard/DemoProvider.php @@ -34,12 +34,12 @@ use Computer; use DateInterval; -use DateTime; -use DateTimeImmutable; use GlpiPlugin\Carbon\Impact\Type; use GlpiPlugin\Carbon\Toolbox; use Monitor; use NetworkEquipment; +use Safe\DateTime; +use Safe\DateTimeImmutable; use Session; class DemoProvider diff --git a/src/Dashboard/Grid.php b/src/Dashboard/Grid.php index e1fd1c19..a4569e78 100644 --- a/src/Dashboard/Grid.php +++ b/src/Dashboard/Grid.php @@ -37,9 +37,14 @@ use Glpi\Dashboard\Filter; use GlpiPlugin\Carbon\Config; use GlpiPlugin\Carbon\Impact\Type; +use JsonException; use Plugin; +use Safe\Exceptions\FilesystemException; use Session; +use function Safe\file_get_contents; +use function Safe\json_decode; + /** * @phpstan-import-type DashboardConfigDescription from Dashboard */ @@ -353,12 +358,14 @@ protected static function getReportCards(): array public static function getDefaults(?array $defaults = null): ?array { $cards_path = Plugin::getPhpDir('carbon') . '/install/data/report_dashboard.json'; - $cards = file_get_contents($cards_path); - if ($cards === false) { + try { + $cards = file_get_contents($cards_path); + } catch (FilesystemException $e) { return $defaults; } - $cards = json_decode($cards, true); - if ($cards === null) { + try { + $cards = json_decode($cards, true); + } catch (JsonException $e) { return $defaults; } diff --git a/src/Dashboard/Provider.php b/src/Dashboard/Provider.php index 6d928dc7..733e4c1f 100644 --- a/src/Dashboard/Provider.php +++ b/src/Dashboard/Provider.php @@ -37,8 +37,6 @@ use ComputerModel as GlpiComputerModel; use ComputerType as GlpiComputerType; use DateInterval; -use DateTime; -use DateTimeImmutable; use DBmysql; use DbUtils; use Glpi\Dashboard\Filter; @@ -55,6 +53,8 @@ use GlpiPlugin\Carbon\UsageImpact; use GlpiPlugin\Carbon\Zone; use RuntimeException; +use Safe\DateTime; +use Safe\DateTimeImmutable; use Search; use Session; use Toolbox as GlpiToolbox; @@ -761,7 +761,7 @@ public static function getUsageCarbonEmissionYearToDate(array $params = []): arr $end_date->format('Y-m-d\TH:i:s.v\Z'), ], ]; - return self::getUsageCarbonEmission($params); + return self::getImpactOfUsageCriteria('gwp', $params, ['itemtype' => PLUGIN_CARBON_TYPES]); } public static function getCarbonIntensity(array $params): array diff --git a/src/Dashboard/Widget.php b/src/Dashboard/Widget.php index e0674257..62629b5a 100644 --- a/src/Dashboard/Widget.php +++ b/src/Dashboard/Widget.php @@ -34,8 +34,6 @@ use Computer; use DateInterval; -use DateTime; -use DateTimeImmutable; use Glpi\Application\View\TemplateRenderer; use Glpi\Dashboard\Widget as GlpiDashboardWidget; use GlpiPlugin\Carbon\Impact\Type; @@ -43,6 +41,8 @@ use Html; use Monitor; use NetworkEquipment; +use Safe\DateTime; +use Safe\DateTimeImmutable; use Toolbox as GlpiToolbox; class Widget extends GlpiDashboardWidget diff --git a/src/DataSource/CarbonIntensity/AbstractClient.php b/src/DataSource/CarbonIntensity/AbstractClient.php index 658b3794..d2952eab 100644 --- a/src/DataSource/CarbonIntensity/AbstractClient.php +++ b/src/DataSource/CarbonIntensity/AbstractClient.php @@ -34,8 +34,6 @@ use Config as GlpiConfig; use DateInterval; -use DateTime; -use DateTimeImmutable; use DateTimeZone; use DBmysql; use Generator; @@ -45,6 +43,8 @@ use GlpiPlugin\Carbon\Toolbox; use GlpiPlugin\Carbon\Zone; use Override; +use Safe\DateTime; +use Safe\DateTimeImmutable; use Symfony\Component\Console\Helper\ProgressBar; abstract class AbstractClient implements ClientInterface diff --git a/src/DataSource/CarbonIntensity/ClientFactory.php b/src/DataSource/CarbonIntensity/ClientFactory.php index 38c30aab..4b8d5310 100644 --- a/src/DataSource/CarbonIntensity/ClientFactory.php +++ b/src/DataSource/CarbonIntensity/ClientFactory.php @@ -36,6 +36,8 @@ use GlpiPlugin\Carbon\DataSource\RestApiClient; use InvalidArgumentException; +use function Safe\glob; + class ClientFactory { /** @@ -143,6 +145,9 @@ public static function createByName(string $name): AbstractClient $class_name = array_search($name, $names); $rest_api_client = new RestApiClient([]); + if (!is_a($class_name, AbstractClient::class, true)) { + throw new InvalidArgumentException("Class $class_name is not a subclass of AbstractClient"); + } return new $class_name($rest_api_client); } } diff --git a/src/DataSource/CarbonIntensity/ClientInterface.php b/src/DataSource/CarbonIntensity/ClientInterface.php index 16bdae9d..8d05517c 100644 --- a/src/DataSource/CarbonIntensity/ClientInterface.php +++ b/src/DataSource/CarbonIntensity/ClientInterface.php @@ -32,9 +32,9 @@ namespace GlpiPlugin\Carbon\DataSource\CarbonIntensity; -use DateTimeImmutable; use GlpiPlugin\Carbon\CarbonIntensity; use GlpiPlugin\Carbon\Source_Zone; +use Safe\DateTimeImmutable; use Symfony\Component\Console\Helper\ProgressBar; /** diff --git a/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php b/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php index 16d5ae6e..0564e95a 100644 --- a/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php +++ b/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php @@ -33,8 +33,6 @@ namespace GlpiPlugin\Carbon\DataSource\CarbonIntensity\ElectricityMaps; use DateInterval; -use DateTime; -use DateTimeImmutable; use DateTimeInterface; use DateTimeZone; use GLPIKey; @@ -49,9 +47,18 @@ use GlpiPlugin\Carbon\Zone; use Override; use RuntimeException; +use Safe\DateTime; +use Safe\DateTimeImmutable; use Safe\Exceptions\FilesystemException; use Symfony\Component\Console\Helper\ProgressBar; +use function Safe\file_get_contents; +use function Safe\file_put_contents; +use function Safe\json_decode; +use function Safe\json_encode; +use function Safe\mkdir; +use function Safe\preg_match; + /** * Query carbon intensity data from Electricity map * @@ -238,7 +245,7 @@ public function fetchDay(DateTimeImmutable $day, Source_Zone $source_zone): arra if ($response['message'] === 'Invalid auth-token') { throw new AbortException('Invalid auth-token'); } - if (preg_match("#^Zone '[^']*' does not exist.$#", $response['message']) !== false) { + if (preg_match("#^Zone '[^']*' does not exist.$#", $response['message']) === 1) { throw new AbortException($response['message']); } return []; @@ -248,9 +255,6 @@ public function fetchDay(DateTimeImmutable $day, Source_Zone $source_zone): arra $timezone = new DateTimeZone('UTC'); foreach ($response['history'] as $record) { $datetime = DateTime::createFromFormat('Y-m-d\TH:i:s+', $record['datetime'], $timezone); - if (!$datetime instanceof DateTimeInterface) { - continue; - } $data_quality = $this->getDataQuality($record); $intensities[] = [ 'datetime' => $datetime->format('Y-m-d\TH:i:s'), diff --git a/src/DataSource/CarbonIntensity/Rte/Client.php b/src/DataSource/CarbonIntensity/Rte/Client.php index 8b16ff62..2cee7b1a 100644 --- a/src/DataSource/CarbonIntensity/Rte/Client.php +++ b/src/DataSource/CarbonIntensity/Rte/Client.php @@ -33,8 +33,6 @@ namespace GlpiPlugin\Carbon\DataSource\CarbonIntensity\Rte; use DateInterval; -use DateTime; -use DateTimeImmutable; use DateTimeInterface; use DateTimeZone; use GlpiPlugin\Carbon\DataSource\CarbonIntensity\AbstractClient; @@ -46,8 +44,14 @@ use GlpiPlugin\Carbon\Zone; use Override; use RuntimeException; +use Safe\DateTime; +use Safe\DateTimeImmutable; use Safe\Exceptions\FilesystemException; +use function Safe\file_get_contents; +use function Safe\file_put_contents; +use function Safe\json_decode; +use function Safe\json_encode; use function Safe\mkdir; /** diff --git a/src/DataSource/CronTaskInterface.php b/src/DataSource/CronTaskInterface.php index 7fec58c5..d4b7150d 100644 --- a/src/DataSource/CronTaskInterface.php +++ b/src/DataSource/CronTaskInterface.php @@ -32,7 +32,14 @@ namespace GlpiPlugin\Carbon\DataSource; +use CommonDBTM; +use CommonGLPI; + interface CronTaskInterface { public static function enumerateTasks(): array; + + public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0); + + public function showForCronTask(CommonDBTM $item); } diff --git a/src/DataSource/CronTaskProvider.php b/src/DataSource/CronTaskProvider.php index cf6166b7..0dc65b6b 100644 --- a/src/DataSource/CronTaskProvider.php +++ b/src/DataSource/CronTaskProvider.php @@ -32,17 +32,30 @@ namespace GlpiPlugin\Carbon\DataSource; +use DirectoryIterator; + class CronTaskProvider { - public static function getCronTaskTypes(): array + /** + * Get all the cron task types available in the plugin. + * + * @param array $subdirs The subdirectories to search for cron tasks. + * @return array> An associative array where the keys are the cron task names and the values are the fully qualified class names of the cron tasks. + */ + public static function getCronTaskTypes(array $subdirs): array { - $subdirs = ['CarbonIntensity', 'Lca']; - $types = []; + static $types = []; + if (!empty($types)) { + return $types; + } foreach ($subdirs as $subdir) { - foreach (glob(__DIR__ . '/' . $subdir . '/*', GLOB_ONLYDIR) as $connector_dir) { - $dir = basename($connector_dir); - $class_name = 'GlpiPlugin\\Carbon\\DataSource\\' . $subdir . '\\' - . $dir . '\\CronTask'; + foreach ($subdir as $connector_dir) { + if ($connector_dir->isDot() || !$connector_dir->isDir()) { + continue; + } + $type_dir = basename(dirname($connector_dir->getPathname())); + $dir = $connector_dir->getBasename(); + $class_name = 'GlpiPlugin\\Carbon\\DataSource\\' . $type_dir . '\\' . $dir . '\\CronTask'; if (!class_exists($class_name)) { continue; } @@ -55,4 +68,17 @@ public static function getCronTaskTypes(): array return $types; } + + /** + * Get the directories containing cron tasks. + * + * @return array The directories containing cron tasks. + */ + public static function getCronTaskDirectories(): array + { + return [ + new DirectoryIterator(__DIR__ . '/CarbonIntensity'), + new DirectoryIterator(__DIR__ . '/Lca'), + ]; + } } diff --git a/src/DataSource/Lca/Boaviztapi/Client.php b/src/DataSource/Lca/Boaviztapi/Client.php index 583e66d5..330f1812 100644 --- a/src/DataSource/Lca/Boaviztapi/Client.php +++ b/src/DataSource/Lca/Boaviztapi/Client.php @@ -45,6 +45,8 @@ use Override; use RuntimeException; +use function Safe\json_encode; + class Client extends AbstractClient { private RestApiClientInterface $client; diff --git a/src/DataSource/Lca/Boaviztapi/Config.php b/src/DataSource/Lca/Boaviztapi/Config.php index 1b38f9f4..6fae7656 100644 --- a/src/DataSource/Lca/Boaviztapi/Config.php +++ b/src/DataSource/Lca/Boaviztapi/Config.php @@ -37,8 +37,11 @@ use GlpiPlugin\Carbon\DataSource\ConfigInterface; use GlpiPlugin\Carbon\DataSource\RestApiClient; use Override; +use Safe\Exceptions\UrlException; use Session; +use function Safe\parse_url; + class Config implements ConfigInterface { public const ENV_BOAVIZTAPI_BASE_URL = 'GLPI_PLUGIN_CARBON_BOAVIZTAPI_BASE_URL'; @@ -139,7 +142,11 @@ protected function validateBaseUrl(string $url): bool } // Check if the URL has a valid scheme (http or https) - $parsed_url = parse_url($url); + try { + $parsed_url = parse_url($url); + } catch (UrlException $e) { + return false; + } if (!isset($parsed_url['scheme']) || !in_array($parsed_url['scheme'], ['http', 'https'])) { return false; } diff --git a/src/DataSource/Lca/ClientFactory.php b/src/DataSource/Lca/ClientFactory.php index 6426e137..1c861557 100644 --- a/src/DataSource/Lca/ClientFactory.php +++ b/src/DataSource/Lca/ClientFactory.php @@ -36,6 +36,8 @@ use GlpiPlugin\Carbon\DataSource\RestApiClient; use InvalidArgumentException; +use function Safe\glob; + class ClientFactory { /** @@ -144,6 +146,9 @@ public static function createByName(string $name): AbstractClient $class_name = array_search($name, $names); $rest_api_client = new RestApiClient([]); + if (!is_a($class_name, AbstractClient::class, true)) { + throw new InvalidArgumentException("Class $class_name is not a subclass of AbstractClient"); + } return new $class_name($rest_api_client); } } diff --git a/src/DataSource/RestApiClient.php b/src/DataSource/RestApiClient.php index 9edf7ab7..0a98ee19 100644 --- a/src/DataSource/RestApiClient.php +++ b/src/DataSource/RestApiClient.php @@ -39,6 +39,8 @@ use Override; use Toolbox; +use function Safe\json_decode; + class RestApiClient implements RestApiClientInterface { public const DEFAULT_TIMEOUT = 5; diff --git a/src/EmbodiedImpact.php b/src/EmbodiedImpact.php index 757f8c5b..b46c3c01 100644 --- a/src/EmbodiedImpact.php +++ b/src/EmbodiedImpact.php @@ -47,20 +47,27 @@ public static function getTypeName($nb = 0) return _n("Embodied impact", "Embodied impacts", $nb, 'carbon'); } - public function calculateImpact(string $lca_type, int $limit = 0): int - { - $crit = []; - if ($limit > 0) { - $crit['LIMIT'] = $limit; - } - $iterator = self::getItemsToEvaluate($lca_type::getItemtype(), $crit); - $count = 0; - foreach ($iterator as $item) { - $lca = new $lca_type($item['id']); - $lca::calculate($item); - $count++; - } + // /** + // * Calculate the embodied impact of assets + // * + // * @param class-string $lca_type + // * @param int $limit + // * @return int + // */ + // public function calculateImpact(string $lca_type, int $limit = 0): int + // { + // $crit = []; + // if ($limit > 0) { + // $crit['LIMIT'] = $limit; + // } + // $iterator = self::getItemsToEvaluate($lca_type::getItemtype(), $crit); + // $count = 0; + // foreach ($iterator as $item) { + // $lca = new $lca_type($item['id']); + // $lca::calculate($item); + // $count++; + // } - return $iterator->count(); - } + // return $iterator->count(); + // } } diff --git a/src/Impact/Embodied/Boavizta/AbstractAsset.php b/src/Impact/Embodied/Boavizta/AbstractAsset.php index 8cf253c0..bf889620 100644 --- a/src/Impact/Embodied/Boavizta/AbstractAsset.php +++ b/src/Impact/Embodied/Boavizta/AbstractAsset.php @@ -38,6 +38,8 @@ use Override; use RuntimeException; +use function Safe\json_encode; + abstract class AbstractAsset extends AbstractEmbodiedImpact implements AssetInterface { /** @var string $engine Name of the calculation engine */ diff --git a/src/Impact/History/AbstractAsset.php b/src/Impact/History/AbstractAsset.php index 4eab5059..3aec2e15 100644 --- a/src/Impact/History/AbstractAsset.php +++ b/src/Impact/History/AbstractAsset.php @@ -35,7 +35,6 @@ use CommonDBTM; use DateInterval; -use DateTime; use DateTimeImmutable; use DateTimeInterface; use DBmysql; @@ -50,6 +49,7 @@ use GlpiPlugin\Carbon\UsageImpact; use LogicException; use Override; +use Safe\DateTime; use Session; abstract class AbstractAsset extends CommonDBTM implements AssetInterface diff --git a/src/Impact/Usage/Boavizta/AbstractAsset.php b/src/Impact/Usage/Boavizta/AbstractAsset.php index c5b8c6b3..9fd1f851 100644 --- a/src/Impact/Usage/Boavizta/AbstractAsset.php +++ b/src/Impact/Usage/Boavizta/AbstractAsset.php @@ -45,6 +45,8 @@ use Override; use RuntimeException; +use function Safe\json_encode; + abstract class AbstractAsset extends AbstractUsageImpact implements AssetInterface { protected static string $itemtype = ''; diff --git a/src/Report.php b/src/Report.php index 46ccf202..572f0f29 100644 --- a/src/Report.php +++ b/src/Report.php @@ -33,12 +33,15 @@ namespace GlpiPlugin\Carbon; use CommonDBTM; -use DateTime; -use DateTimeImmutable; use Glpi\Application\View\TemplateRenderer; use Glpi\Dashboard\Grid as DashboardGrid; use GlpiPlugin\Carbon\Dashboard\Provider; use Override; +use Safe\DateTime; +use Safe\DateTimeImmutable; + +use function Safe\ob_get_clean; +use function Safe\ob_start; class Report extends CommonDBTM { diff --git a/src/Toolbox.php b/src/Toolbox.php index f8c85078..6e6159c2 100644 --- a/src/Toolbox.php +++ b/src/Toolbox.php @@ -33,8 +33,6 @@ namespace GlpiPlugin\Carbon; use DateInterval; -use DateTime; -use DateTimeImmutable; use DateTimeInterface; use DBmysql; use Glpi\Dashboard\Dashboard as GlpiDashboard; @@ -45,6 +43,9 @@ use InvalidArgumentException; use Location; use Mexitek\PHPColors\Color; +use Safe\DateTime; +use Safe\DateTimeImmutable; +use Safe\Exceptions\DatetimeException; use Toolbox as GlpiToolbox; class Toolbox @@ -107,10 +108,15 @@ public function getOldestAssetDate(array $crit = []): ?DateTimeImmutable if ($oldest_date === null) { return null; } - if (($output = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $oldest_date)) === false) { - // Infocom dates are date (without time) - $output = DateTimeImmutable::createFromFormat('Y-m-d', $oldest_date); - $output = $output->setTime(0, 0, 0, 0); + try { + $output = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $oldest_date); + } catch (DatetimeException $e) { + try { + $output = DateTimeImmutable::createFromFormat('Y-m-d', $oldest_date); + $output = $output->setTime(0, 0, 0, 0); + } catch (DatetimeException $e) { + throw $e; + } } return $output; @@ -165,10 +171,15 @@ public function getLatestAssetDate(array $crit = []): ?DateTimeImmutable if ($latest_date === null) { return null; } - if (($output = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $latest_date)) === false) { - // Infocom dates are date (without time) - $output = DateTimeImmutable::createFromFormat('Y-m-d', $latest_date); - $output = $output->setTime(23, 59, 59, 0); + try { + $output = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $latest_date); + } catch (DatetimeException $e) { + try { + $output = DateTimeImmutable::createFromFormat('Y-m-d', $latest_date); + $output = $output->setTime(23, 59, 59, 0); + } catch (DatetimeException $e) { + throw $e; + } } return $output; diff --git a/src/UsageInfo.php b/src/UsageInfo.php index f0736434..4fb4f955 100644 --- a/src/UsageInfo.php +++ b/src/UsageInfo.php @@ -36,7 +36,6 @@ use CommonDBTM; use CommonGLPI; use Computer as GlpiComputer; -use DateTime; use Glpi\Application\View\TemplateRenderer; use GlpiPlugin\Carbon\Dashboard\Provider; use GlpiPlugin\Carbon\Dashboard\Widget; @@ -46,7 +45,7 @@ use Monitor as GlpiMonitor; use NetworkEquipment as GlpiNetworkEquipment; use Override; -use Toolbox as GlpiToolbox; +use Safe\DateTime; /** * Relation between a computer and a usage profile @@ -161,7 +160,7 @@ public function showForItem($ID, $withtemplate = '') ]; $this->initForm($this->getID(), $options); $asset_itemtype = $this->fields['itemtype']; - if (!GlpiToolbox::isCommonDBTM($asset_itemtype)) { + if (!is_a($asset_itemtype, CommonDBTM::class, true)) { return; } $asset = new $asset_itemtype(); diff --git a/tests/fixtures/FakeDataSources.php b/tests/fixtures/FakeDataSources.php index d931c1f1..35274023 100644 --- a/tests/fixtures/FakeDataSources.php +++ b/tests/fixtures/FakeDataSources.php @@ -32,6 +32,8 @@ namespace GlpiPlugin\Carbon\DataSource\CarbonIntensity\Foo; +use CommonDBTM; +use CommonGLPI; use GlpiPlugin\Carbon\DataSource\CronTaskInterface; class CronTask implements CronTaskInterface @@ -40,10 +42,22 @@ public static function enumerateTasks(): array { return []; } + + public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) + { + return 'Foo'; + } + + public function showForCronTask(CommonDBTM $item) + { + return true; + } } namespace GlpiPlugin\Carbon\DataSource\Lca\Bar; +use CommonDBTM; +use CommonGLPI; use GlpiPlugin\Carbon\DataSource\CronTaskInterface; class CronTask implements CronTaskInterface @@ -52,10 +66,22 @@ public static function enumerateTasks(): array { return []; } + + public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) + { + return 'Bar'; + } + + public function showForCronTask(CommonDBTM $item) + { + return true; + } } namespace GlpiPlugin\Carbon\DataSource\Lca\Baz; +use CommonDBTM; +use CommonGLPI; use GlpiPlugin\Carbon\DataSource\CronTaskInterface; class CronTask implements CronTaskInterface @@ -64,4 +90,14 @@ public static function enumerateTasks(): array { return []; } + + public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) + { + return 'Baz'; + } + + public function showForCronTask(CommonDBTM $item) + { + return true; + } } diff --git a/tests/units/CarbonIntensityTest.php b/tests/units/CarbonIntensityTest.php index 859df289..16081814 100644 --- a/tests/units/CarbonIntensityTest.php +++ b/tests/units/CarbonIntensityTest.php @@ -33,8 +33,6 @@ namespace GlpiPlugin\Carbon\Tests; use Computer; -use DateTime; -use DateTimeImmutable; use DateTimeInterface; use DBmysql; use GlpiPlugin\Carbon\CarbonIntensity; @@ -44,6 +42,8 @@ use GlpiPlugin\Carbon\Zone; use Infocom; use PHPUnit\Framework\Attributes\CoversClass; +use Safe\DateTime; +use Safe\DateTimeImmutable; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\Output; diff --git a/tests/units/Command/ImportDashboardCommandTest.php b/tests/units/Command/ImportDashboardCommandTest.php index 76174746..104ce551 100644 --- a/tests/units/Command/ImportDashboardCommandTest.php +++ b/tests/units/Command/ImportDashboardCommandTest.php @@ -133,7 +133,7 @@ public function test_import_dashboard_command() $this->fail("Failed to get dashboard item with id {$row['id']}"); } $item_fields = array_filter($item->fields, function ($key) { - return !in_array($key, ['id', 'dashboards_dashboards_id']); + return !in_array($key, ['id', 'dashboards_dashboards_id', 'date_creation', 'date_mod']); }, ARRAY_FILTER_USE_KEY); $item_fields['card_options'] = json_decode($item_fields['card_options'], true); $found = false; diff --git a/tests/units/DataSource/CarbonIntensity/AbstractClientTest.php b/tests/units/DataSource/CarbonIntensity/AbstractClientTest.php index aa8d7719..d0598d9a 100644 --- a/tests/units/DataSource/CarbonIntensity/AbstractClientTest.php +++ b/tests/units/DataSource/CarbonIntensity/AbstractClientTest.php @@ -32,10 +32,10 @@ namespace GlpiPlugin\Carbon\DataSource\CarbonIntensity\Tests; -use DateTimeImmutable; use GlpiPlugin\Carbon\DataSource\CarbonIntensity\AbstractClient; use GlpiPlugin\Carbon\Tests\DbTestCase; use PHPUnit\Framework\Attributes\CoversClass; +use Safe\DateTimeImmutable; #[CoversClass(AbstractClient::class)] class AbstractClientTest extends DbTestCase diff --git a/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ClientTest.php b/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ClientTest.php index 2c79a293..59496c87 100644 --- a/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ClientTest.php +++ b/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ClientTest.php @@ -34,7 +34,6 @@ use DateInterval; use DateTime; -use DateTimeImmutable; use DateTimeInterface; use GlpiPlugin\Carbon\DataSource\RestApiClientInterface; use GlpiPlugin\Carbon\Source; @@ -42,6 +41,7 @@ use GlpiPlugin\Carbon\Tests\DbTestCase; use GlpiPlugin\Carbon\Zone; use PHPUnit\Framework\Attributes\CoversClass; +use Safe\DateTimeImmutable; #[CoversClass(Client::class)] class ClientTest extends DbTestCase diff --git a/tests/units/DataSource/CarbonIntensity/Rte/ClientTest.php b/tests/units/DataSource/CarbonIntensity/Rte/ClientTest.php index 477e9c64..4283aebe 100644 --- a/tests/units/DataSource/CarbonIntensity/Rte/ClientTest.php +++ b/tests/units/DataSource/CarbonIntensity/Rte/ClientTest.php @@ -33,7 +33,6 @@ namespace GlpiPlugin\Carbon\DataSource\CarbonIntensity\Rte; use DateTime; -use DateTimeImmutable; use GlpiPlugin\Carbon\CarbonIntensity; use GlpiPlugin\Carbon\DataSource\RestApiClientInterface; use GlpiPlugin\Carbon\Source; @@ -41,6 +40,7 @@ use GlpiPlugin\Carbon\Tests\DbTestCase; use GlpiPlugin\Carbon\Zone; use PHPUnit\Framework\Attributes\CoversClass; +use Safe\DateTimeImmutable; #[CoversClass(Client::class)] class ClientTest extends DbTestCase diff --git a/tests/units/DataSource/CronTaskProviderTest.php b/tests/units/DataSource/CronTaskProviderTest.php index 35ae0f38..0d186048 100644 --- a/tests/units/DataSource/CronTaskProviderTest.php +++ b/tests/units/DataSource/CronTaskProviderTest.php @@ -37,22 +37,55 @@ require_once dirname(__DIR__, 2) . '/fixtures/FakeDataSources.php'; -// Redefine glob() in the namespace context to mock -function glob($pattern, $flags = 0) -{ - return [ - '/var/www/glpi/plugins/carbon/src/DataSource/Carbonintensity/Foo', - '/var/www/glpi/plugins/carbon/src/DataSource/Lca/Bar', - '/var/www/glpi/plugins/carbon/src/DataSource/Lca/Baz', - ]; -} - #[CoversClass(CronTaskProvider::class)] class CronTaskProviderTest extends CommonTestCase { public function test_getCronTaskTypes_returns_() { - $result = CronTaskProvider::getCronTaskTypes(); + $stub_carbon_intensity_dir = $this->createStub(\DirectoryIterator::class); + $stub_carbon_intensity_dir->method('getBasename')->willReturn('CarbonIntensity'); + $foo_item = $this->createStub(\DirectoryIterator::class); + $foo_item->method('isDot')->willReturn(false); + $foo_item->method('isDir')->willReturn(true); + $foo_item->method('getBasename')->willReturn('Foo'); + $foo_item->method('getPathName')->willReturn('/path/to/CarbonIntensity/Foo'); + $internal_iterator = new \ArrayIterator([ + $foo_item, + ]); + $stub_carbon_intensity_dir->method('rewind')->willReturnCallback(fn() => $internal_iterator->rewind()); + $stub_carbon_intensity_dir->method('valid')->willReturnCallback(fn() => $internal_iterator->valid()); + $stub_carbon_intensity_dir->method('current')->willReturnCallback(fn() => $internal_iterator->current()); + $stub_carbon_intensity_dir->method('key')->willReturnCallback(fn() => $internal_iterator->key()); + $stub_carbon_intensity_dir->method('next')->willReturnCallback(fn() => $internal_iterator->next()); + + $stub_lca_dir = $this->createStub(\DirectoryIterator::class); + $stub_lca_dir->method('getBasename')->willReturn('Lca'); + $bar_item = $this->createStub(\DirectoryIterator::class); + $bar_item->method('isDot')->willReturn(false); + $bar_item->method('isDir')->willReturn(true); + $bar_item->method('getBasename')->willReturn('Bar'); + $bar_item->method('getPathName')->willReturn('/path/to/Lca/Bar'); + $baz_item = $this->createStub(\DirectoryIterator::class); + $baz_item->method('isDot')->willReturn(false); + $baz_item->method('isDir')->willReturn(true); + $baz_item->method('getBasename')->willReturn('Baz'); + $baz_item->method('getPathName')->willReturn('/path/to/Lca/Baz'); + $internal_iterator_lca = new \ArrayIterator([ + $bar_item, + $baz_item, + ]); + $stub_lca_dir->method('rewind')->willReturnCallback(fn() => $internal_iterator_lca->rewind()); + $stub_lca_dir->method('valid')->willReturnCallback(fn() => $internal_iterator_lca->valid()); + $stub_lca_dir->method('current')->willReturnCallback(fn() => $internal_iterator_lca->current()); + $stub_lca_dir->method('key')->willReturnCallback(fn() => $internal_iterator_lca->key()); + $stub_lca_dir->method('next')->willReturnCallback(fn() => $internal_iterator_lca->next()); + + $dirs = [ + $stub_carbon_intensity_dir, + $stub_lca_dir, + ]; + + $result = CronTaskProvider::getCronTaskTypes($dirs); $expected = [ 'Foo' => 'GlpiPlugin\\Carbon\\DataSource\\CarbonIntensity\\Foo\\CronTask', 'Bar' => 'GlpiPlugin\\Carbon\\DataSource\\Lca\\Bar\\CronTask', diff --git a/tests/units/Impact/History/ComputerTest.php b/tests/units/Impact/History/ComputerTest.php index 260850e2..9d4eeda6 100644 --- a/tests/units/Impact/History/ComputerTest.php +++ b/tests/units/Impact/History/ComputerTest.php @@ -36,7 +36,7 @@ use Computer as GlpiComputer; use ComputerModel as GlpiComputerModel; use ComputerType as GlpiComputerType; -use DateTime; +use DBmysql; use GlpiPlugin\Carbon\CarbonEmission; use GlpiPlugin\Carbon\ComputerModel; use GlpiPlugin\Carbon\ComputerType; @@ -51,6 +51,7 @@ use Infocom; use Location as GlpiLocation; use PHPUnit\Framework\Attributes\CoversClass; +use Safe\DateTime; #[CoversClass(Computer::class)] class ComputerTest extends CommonAsset @@ -153,7 +154,7 @@ public function testEvaluateItem() } $this->login('glpi', 'glpi'); - $entities_id = $this->isolateInEntity('glpi', 'glpi'); + $entities_id = $this->isolateInEntity(); $model_power = 55; $glpi_location = $this->createItem(GlpiLocation::class, [ diff --git a/tests/units/Impact/History/MonitorTest.php b/tests/units/Impact/History/MonitorTest.php index 1fdeb0c5..bbefa3fd 100644 --- a/tests/units/Impact/History/MonitorTest.php +++ b/tests/units/Impact/History/MonitorTest.php @@ -34,10 +34,8 @@ use CommonDBTM; use Computer as GlpiComputer; -use Computer_Item; use ComputerModel; use ComputerType as GlpiComputerType; -use DateTime; use DBmysql; use Glpi\Asset\Asset_PeripheralAsset; use GlpiPlugin\Carbon\CarbonEmission; @@ -57,6 +55,7 @@ use MonitorModel as GlpiMonitorModel; use MonitorType as GlpiMonitorType; use PHPUnit\Framework\Attributes\CoversClass; +use Safe\DateTime; #[CoversClass(Monitor::class)] class MonitorTest extends CommonAsset diff --git a/tests/units/Impact/History/NetworkEquipmentTest.php b/tests/units/Impact/History/NetworkEquipmentTest.php index 4186f2ad..76a3192e 100644 --- a/tests/units/Impact/History/NetworkEquipmentTest.php +++ b/tests/units/Impact/History/NetworkEquipmentTest.php @@ -33,7 +33,6 @@ namespace GlpiPlugin\Carbon\Impact\History\Tests; use CommonDBTM; -use DateTime; use GlpiPlugin\Carbon\CarbonEmission; use GlpiPlugin\Carbon\Impact\History\NetworkEquipment; use GlpiPlugin\Carbon\Location; @@ -50,6 +49,7 @@ use NetworkEquipmentModel as GlpiNetworkEquipmentModel; use NetworkEquipmentType as GlpiNetworkEquipmentType; use PHPUnit\Framework\Attributes\CoversClass; +use Safe\DateTime; #[CoversClass(NetworkEquipment::class)] class NetworkEquipmentTest extends CommonAsset diff --git a/tests/units/ToolboxTest.php b/tests/units/ToolboxTest.php index 62cee2c8..94ea0bb4 100644 --- a/tests/units/ToolboxTest.php +++ b/tests/units/ToolboxTest.php @@ -34,8 +34,6 @@ use Computer as GlpiComputer; use DateInterval; -use DateTime; -use DateTimeImmutable; use GlpiPlugin\Carbon\CarbonIntensity; use GlpiPlugin\Carbon\Source; use GlpiPlugin\Carbon\Source_Zone; @@ -44,6 +42,8 @@ use Infocom; use Location; use PHPUnit\Framework\Attributes\CoversClass; +use Safe\DateTime; +use Safe\DateTimeImmutable; #[CoversClass(Toolbox::class)] class ToolboxTest extends DbTestCase From df7587de98fddeea86ff57cd5be2be59c2a9a7c5 Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 11 Sep 2026 08:29:13 +0200 Subject: [PATCH 18/35] fix(Config): set icon visible in GLPI > Setup > General > tab Environmental impact --- src/Config.php | 8 +++++++- tests/units/ComputerTypeTest.php | 13 +++++++++---- tests/units/ConfigTest.php | 10 ++++++++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Config.php b/src/Config.php index 2266713b..b5b1f04b 100644 --- a/src/Config.php +++ b/src/Config.php @@ -69,13 +69,19 @@ public static function getTypeName($nb = 0) return plugin_carbon_getFriendlyName(); } + #[Override] + public static function getIcon(): string + { + return 'fa-solid fa-solar-panel'; + } + #[Override] public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { $tabName = ''; if (!$withtemplate) { if ($item->getType() == GlpiConfig::class) { - $tabName = self::getTypeName(); + $tabName = GlpiConfig::createTabEntry(self::getTypeName(), 0, $item::class, self::getIcon()); } } return $tabName; diff --git a/tests/units/ComputerTypeTest.php b/tests/units/ComputerTypeTest.php index d44560af..fd619a90 100644 --- a/tests/units/ComputerTypeTest.php +++ b/tests/units/ComputerTypeTest.php @@ -44,16 +44,21 @@ class ComputerTypeTest extends AbstractTypeTest protected static string $glpi_type_itemtype = GlpiComputerType::class; protected static string $type_itemtype = ComputerType::class; - public function testGetTabNameForItem() + public function test_GetTabNameForItem_returns_empty_string_for_computer() + { + $glpi_computer_type = $this->createItem(GlpiComputerType::class); + $instance = new ComputerType(); + $result = $instance->getTabNameForItem($glpi_computer_type, 1); + $this->assertEquals('', $result); + } + + public function test_GetTabNameForItem_returns_tab_name_for_computer_type() { $glpi_computer_type = $this->createItem(GlpiComputerType::class); $instance = new ComputerType(); $result = $instance->getTabNameForItem($glpi_computer_type); $crawler = new Crawler($result); $this->assertEquals('Carbon', $crawler->text()); - - $result = $instance->getTabNameForItem($glpi_computer_type, 1); - $this->assertEquals('', $result); } public function testShowForItemType() diff --git a/tests/units/ConfigTest.php b/tests/units/ConfigTest.php index c13a76e6..7917a72b 100644 --- a/tests/units/ConfigTest.php +++ b/tests/units/ConfigTest.php @@ -56,14 +56,20 @@ public function testGetTypeName() $this->assertEquals('Environmental Impact', $result); } - public function testGetTabNameForItem() + public function test_GetTabNameForItem_returns_empty_string_for_computer() { $instance = new Config(); $result = $instance->getTabNameForItem(new GlpiComputer()); $this->assertEquals('', $result); + } + + public function test_GetTabNameForItem_returns_tab_name_for_config() + { + $instance = new Config(); $result = $instance->getTabNameForItem(new GlpiConfig()); - $this->assertEquals('Environmental Impact', $result); + $crawler = new Crawler($result); + $this->assertEquals('Environmental Impact', $crawler->text()); } public function testDisplayTabContentForItem() From fcfccf0b30d4a44854d269e5e1bbeedf73811bfc Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 11 Sep 2026 11:04:29 +0200 Subject: [PATCH 19/35] feat(DataSource/RestApiClient): use GLPI HTTP client instead of Guzzle adds ability to disable private network access and use orbypass proxy settings --- setup.php | 6 ++++ src/DataSource/RestApiClient.php | 56 +++++++++++++++++--------------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/setup.php b/setup.php index 1138b3db..753a6b3c 100644 --- a/setup.php +++ b/setup.php @@ -32,6 +32,7 @@ use Config as GlpiConfig; use CronTask as GlpiCronTask; +use Glpi\Config\ProxyExclusion; use Glpi\Plugin\Hooks; use GlpiPlugin\Carbon\Config; use GlpiPlugin\Carbon\CronTask; @@ -105,6 +106,11 @@ function plugin_init_carbon() plugin_carbon_registerClasses(); $CFG_GLPI['javascript']['tools'][strtolower(Report::class)] = ['dashboard']; + $proxy_exclusions = $CFG_GLPI['possible_proxy_exclusions']; + $proxy_exclusions->addExclusion(new ProxyExclusion( + Config::class, + plugin_carbon_getFriendlyName(), + )); } function plugin_carbon_setupHooks() diff --git a/src/DataSource/RestApiClient.php b/src/DataSource/RestApiClient.php index 0a98ee19..d45cb298 100644 --- a/src/DataSource/RestApiClient.php +++ b/src/DataSource/RestApiClient.php @@ -32,11 +32,15 @@ namespace GlpiPlugin\Carbon\DataSource; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\RequestException; +use Glpi\Toolbox\HttpClient; +use GlpiPlugin\Carbon\Config; use GuzzleHttp\Psr7\Message; use GuzzleHttp\Psr7\Request; use Override; +use RuntimeException; +use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Toolbox; use function Safe\json_decode; @@ -49,54 +53,52 @@ class RestApiClient implements RestApiClientInterface ]; public const DEFAULT_HTTP_VERSION = '2.0'; - protected $api_client = null; - protected $last_error = ''; + protected ?HttpClient $api_client = null; + protected array $last_error = []; public function __construct(array $params = []) { $local_params = [ - 'timeout' => self::DEFAULT_TIMEOUT, - 'connect_timeout' => self::DEFAULT_TIMEOUT, - 'headers' => self::DEFAULT_HEADERS, - 'version' => self::DEFAULT_HTTP_VERSION, - 'http_errors' => false, - 'debug' => false, // ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE), - // This is insecure and not recommanded, but... - // 'verify' => false, + 'timeout' => self::DEFAULT_TIMEOUT, + 'max_connect_duration' => self::DEFAULT_TIMEOUT, + 'headers' => self::DEFAULT_HEADERS, + 'http_version' => self::DEFAULT_HTTP_VERSION, ]; // array_merge_recursive() is used because it merges headers - $this->api_client = new Client(array_merge_recursive($local_params, $params)); + $this->api_client = new HttpClient(Config::class, array_merge_recursive($local_params, $params)); } #[Override] public function request(string $method = 'GET', string $uri = '', array $options = []) { + $request = $this->api_client; try { - $request = $this->api_client; $response = $request->request($method, $uri, $options); - } catch (RequestException $e) { - $cleaned_request = new Request( - $e->getRequest()->getMethod(), - $e->getRequest()->getUri(), - [], - $request->getBody(), - $request->getProtocolVersion() - ); + } catch (RedirectionExceptionInterface|ClientExceptionInterface|ServerExceptionInterface $e) { + // Exception related to HTTP + $this->last_error = [ + 'title' => "Plugins API error", + 'exception' => $e->getMessage(), + 'request' => $method . ' ' . $uri, + ]; + $this->last_error['response'] = $e->getResponse()->getContent(false); + + Toolbox::logDebug($this->last_error); + + return false; + } catch (RuntimeException $e) { + // Other exceptions $this->last_error = [ 'title' => "Plugins API error", 'exception' => $e->getMessage(), - 'request' => Message::toString($cleaned_request), ]; - if ($e->hasResponse()) { - $this->last_error['response'] = Message::toString($e->getResponse()); - } Toolbox::logDebug($this->last_error); return false; } - return json_decode($response->getBody(), true); + return json_decode($response->getContent(), true); } } From f464ec5dd5d399678d477b76b97cc6d39ad55031 Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 11 Sep 2026 14:12:06 +0200 Subject: [PATCH 20/35] feat(Datasource/AbstractCrontask): give link to enable zones --- ajax/toggleZoneDownload.php | 55 ------------------- .../CarbonIntensity/AbstractCronTask.php | 11 +++- src/DataSource/Lca/Boaviztapi/Client.php | 2 +- src/DataSource/RestApiClient.php | 2 - src/Source_Zone.php | 41 ++++++++------ src/Toolbox.php | 4 ++ tests/units/Source_ZoneTest.php | 25 +++++++++ 7 files changed, 62 insertions(+), 78 deletions(-) delete mode 100644 ajax/toggleZoneDownload.php diff --git a/ajax/toggleZoneDownload.php b/ajax/toggleZoneDownload.php deleted file mode 100644 index 4239da82..00000000 --- a/ajax/toggleZoneDownload.php +++ /dev/null @@ -1,55 +0,0 @@ -. - * - * ------------------------------------------------------------------------- - */ -use Config as GlpiConfig; -use Glpi\Exception\Http\AccessDeniedHttpException; -use Glpi\Exception\Http\BadRequestHttpException; -use Glpi\Exception\Http\NotFoundHttpException; -use GlpiPlugin\Carbon\Source; -use GlpiPlugin\Carbon\Source_Zone; - -include(__DIR__ . '/../../../inc/includes.php'); - -// Check if plugin is activated... -if (!Plugin::isPluginActive('carbon')) { - throw new NotFoundHttpException(); -} elseif (!Source::canView() || !GlpiConfig::canUpdate()) { - throw new AccessDeniedHttpException(); -} elseif (!isset($_GET['id'])) { - throw new BadRequestHttpException(); -} else { - $source_zone = new Source_Zone(); - if (!$source_zone->getFromDB($_GET['id'])) { - throw new BadRequestHttpException(); - } elseif (!$source_zone->toggleZone()) { - throw new BadRequestHttpException(); - } -} diff --git a/src/DataSource/CarbonIntensity/AbstractCronTask.php b/src/DataSource/CarbonIntensity/AbstractCronTask.php index 5df751ad..bd672f95 100644 --- a/src/DataSource/CarbonIntensity/AbstractCronTask.php +++ b/src/DataSource/CarbonIntensity/AbstractCronTask.php @@ -39,6 +39,7 @@ use GlpiPlugin\Carbon\CronTask; use GlpiPlugin\Carbon\DataSource\AbstractCronTask as DatasourceAbstractCronTask; use GlpiPlugin\Carbon\DataSource\CronTaskInterface; +use GlpiPlugin\Carbon\Source; use GlpiPlugin\Carbon\Source_Zone; use GlpiPlugin\Carbon\Toolbox; use GlpiPlugin\Carbon\Zone; @@ -86,12 +87,18 @@ public function showGapsReport() {% import "components/form/fields_macros.html.twig" as fields %} {{ fields.largeTitle(__('Gaps in carbon intensity time series', 'carbon')) }}
{{ __('Only zones with download enabled are displayed.', 'carbon') }}
+
 
TWIG; - echo $renderer->renderFromStringTemplate($template); - $oldest_asset_date = (new Toolbox())->getOldestAssetDate(); $client = ClientFactory::create(static::$client_name); $source_name = $client->getSourceName(); + $source = new Source(); + $source->getFromDBByCrit(['name' => $source_name]); + $source_url = $source->getLinkURL(); + echo $renderer->renderFromStringTemplate($template, [ + 'url_to_zones' => $source_url, + ]); + $oldest_asset_date = (new Toolbox())->getOldestAssetDate(); foreach ($client->getSupportedZones() as $zone_name) { $source_zone = new Source_Zone(); if (!$source_zone->getFromDbBySourceAndZone($source_name, $zone_name)) { diff --git a/src/DataSource/Lca/Boaviztapi/Client.php b/src/DataSource/Lca/Boaviztapi/Client.php index 330f1812..9bcf9db4 100644 --- a/src/DataSource/Lca/Boaviztapi/Client.php +++ b/src/DataSource/Lca/Boaviztapi/Client.php @@ -277,7 +277,7 @@ public function parseResponse(array $response, string $scope): array return $impacts; } - protected function parseCriteria(string $name, $impact): ?TrackedFloat + protected function parseCriteria(string $name, string|array $impact): ?TrackedFloat { if ($impact === 'not implemented') { return null; diff --git a/src/DataSource/RestApiClient.php b/src/DataSource/RestApiClient.php index d45cb298..9b4570f8 100644 --- a/src/DataSource/RestApiClient.php +++ b/src/DataSource/RestApiClient.php @@ -34,8 +34,6 @@ use Glpi\Toolbox\HttpClient; use GlpiPlugin\Carbon\Config; -use GuzzleHttp\Psr7\Message; -use GuzzleHttp\Psr7\Request; use Override; use RuntimeException; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; diff --git a/src/Source_Zone.php b/src/Source_Zone.php index 61fc3bb9..c1c89366 100644 --- a/src/Source_Zone.php +++ b/src/Source_Zone.php @@ -63,6 +63,15 @@ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) return self::createTabEntry(Source::getTypeName(), 0); } + public function prepareInputForUpdate($input) + { + if (isset($input['_toggle_is_download_enabled'])) { + $input['is_download_enabled'] = ($this->fields['is_download_enabled'] == 1) ? 0 : 1; + unset($input['_toggle_is_download_enabled']); + } + return parent::prepareInputForUpdate($input); + } + #[Override] public function rawSearchOptions() { @@ -196,16 +205,14 @@ public static function showForSource(CommonDBTM $item) // At least 1 entry then add JS to toggle the state of zones echo Html::scriptBlock(' var plugin_carbon_toggleZone = function (id) { - fetch(CFG_GLPI["root_doc"] + "/plugins/carbon/ajax/toggleZoneDownload.php?id=" + id).then(response => { - if (response.status === 200) { - reloadTab(); - } else { - response.text().then(function (text) { - glpi_toast_error(text) - }); - } + debugger; + var url = CFG_GLPI["root_doc"] + "/plugins/carbon/front/source_zone.form.php?id=" + id; + submitGetLink(url, { + "update": "", + "id": id, + "_toggle_is_download_enabled": 1, }); - }; + } '); } } @@ -295,16 +302,14 @@ public static function showForZone(CommonDBTM $item) // At least 1 entry then add JS to toggle the state of zones echo Html::scriptBlock(' var plugin_carbon_toggleZone = function (id) { - fetch(CFG_GLPI["root_doc"] + "/plugins/carbon/ajax/toggleZoneDownload.php?id=" + id).then(response => { - if (response.status === 200) { - reloadTab(); - } else { - response.text().then(function (text) { - glpi_toast_error(text) - }); - } + debugger; + var url = CFG_GLPI["root_doc"] + "/plugins/carbon/front/source_zone.form.php?id=" + id; + submitGetLink(url, { + "update": "", + "id": id, + "_toggle_is_download_enabled": 1, }); - }; + } '); } } diff --git a/src/Toolbox.php b/src/Toolbox.php index 6e6159c2..1ab35ca0 100644 --- a/src/Toolbox.php +++ b/src/Toolbox.php @@ -574,6 +574,10 @@ public static function findTemporalGapsInTable(string $table, DateTimeInterface // Assume stop date is yesterday at midnight $stop = new DateTime('yesterday midnight'); } + if ($start > $stop) { + // Fix start so that it does not exeeds stop + $start = clone $stop; + } $sql_interval = self::dateIntervalToMySQLInterval($interval); $start_string = $start->format('Y-m-d H:i:s'); diff --git a/tests/units/Source_ZoneTest.php b/tests/units/Source_ZoneTest.php index 9b911743..e1e85909 100644 --- a/tests/units/Source_ZoneTest.php +++ b/tests/units/Source_ZoneTest.php @@ -71,6 +71,31 @@ public function testShowForSource() $this->assertNotEmpty($output); } + public function test_prepareInputForUpdate_toggles_the_download_flag() + { + $source = $this->createItem(Source::class, [ + 'name' => 'foo', + ]); + $zone = $this->createItem(Zone::class, [ + 'name' => 'bar', + ]); + $instance = $this->createItem(Source_Zone::class, [ + $source::getForeignKeyField() => $source->getID(), + $zone::getForeignKeyField() => $zone->getID(), + 'is_download_enabled' => 0, + ]); + + $input = ['_toggle_is_download_enabled' => 1]; + $output = $instance->prepareInputForUpdate($input); + $this->assertEquals(1, $output['is_download_enabled']); + + // Test toggling back to 0 + $instance->fields['is_download_enabled'] = 1; + $input = ['_toggle_is_download_enabled' => 1]; + $output = $instance->prepareInputForUpdate($input); + $this->assertEquals(0, $output['is_download_enabled']); + } + public function test_showForZone_shows_nothing_when_user_cannot_view_the_related_zone() { $source = $this->createItem(Source::class, [ From c8a7c29674b70824e536753cdfc40a82c1eb787d Mon Sep 17 00:00:00 2001 From: btry Date: Tue, 15 Sep 2026 13:50:43 +0200 Subject: [PATCH 21/35] chore: update package --- package-lock.json | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 91ab1294..a5955b40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1930,10 +1930,21 @@ "peer": true }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", "peer": true, "dependencies": { "argparse": "^2.0.1" From 98428ec90acf00787f0da3c6a9bcd3c3b1e7248f Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 24 Sep 2026 08:11:52 +0200 Subject: [PATCH 22/35] docs(SECURITY): fix plugin name mention --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 32d96350..32657117 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,7 +13,7 @@ Once the report will be handled, and if the issue is not yet fixed (or in progre we'll add it to the GitHub security tab, and add you as observer. Meanwhile, you will reserve a CVE for the issue. -Thank you for improving the security of glpi-agent. +Thank you for improving the security of the plugin Carbon. ## Supported Versions From d8383e298bd5571f9ca8dc36a489421f150ec454 Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 24 Sep 2026 08:12:25 +0200 Subject: [PATCH 23/35] refacror(Config): use constant for context --- src/Config.php | 1 + templates/config.html.twig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Config.php b/src/Config.php index b5b1f04b..84609f14 100644 --- a/src/Config.php +++ b/src/Config.php @@ -144,6 +144,7 @@ public function showForm($ID, $options = []) $confirm_message = __('This action cannot be undone. Are you sure?', 'carbon'); $renderer->display('@carbon/config.html.twig', [ 'can_edit' => $canedit, + 'context' => self::CONFIG_CONTEXT, 'current_config' => $current_config, 'impact_engines' => Engine::getAvailableBackends(), 'include_configs' => $include_configs, diff --git a/templates/config.html.twig b/templates/config.html.twig index d199f31d..182e0aa7 100644 --- a/templates/config.html.twig +++ b/templates/config.html.twig @@ -34,7 +34,7 @@ {% if can_edit %} - + {{ fields.largeTitle( __('Impact engine', 'carbon'), From 0d352ed9bd0ff585569e588a124d823f8783650c Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 24 Sep 2026 08:14:16 +0200 Subject: [PATCH 24/35] chore: drop obsolete dependencies --- composer.json | 4 +- composer.lock | 245 +------------------------------------------------- 2 files changed, 2 insertions(+), 247 deletions(-) diff --git a/composer.json b/composer.json index 8cba91a4..4cb38c7a 100644 --- a/composer.json +++ b/composer.json @@ -3,9 +3,7 @@ "php": ">=8.2.0", "geocoder-php/google-maps-provider": "^4.7", "geocoder-php/nominatim-provider": "^5.7", - "league/iso3166": "^4.3", - "php-http/message": "^1.16", - "php-http/message-factory": "^1.1" + "league/iso3166": "^4.3" }, "require-dev": { "marcocesarato/php-conventional-changelog": "^1.17" diff --git a/composer.lock b/composer.lock index 547f38e4..5ee0713f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,74 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "108f5d181fd03cee818d694c22fa7d8b", + "content-hash": "029fced46fc6d9a65d185053ce0527f9", "packages": [ - { - "name": "clue/stream-filter", - "version": "v1.7.0", - "source": { - "type": "git", - "url": "https://github.com/clue/stream-filter.git", - "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", - "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", - "shasum": "" - }, - "require": { - "php": ">=5.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "Clue\\StreamFilter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering" - } - ], - "description": "A simple and modern approach to stream filtering in PHP", - "homepage": "https://github.com/clue/stream-filter", - "keywords": [ - "bucket brigade", - "callback", - "filter", - "php_user_filter", - "stream", - "stream_filter_append", - "stream_filter_register" - ], - "support": { - "issues": "https://github.com/clue/stream-filter/issues", - "source": "https://github.com/clue/stream-filter/tree/v1.7.0" - }, - "funding": [ - { - "url": "https://clue.engineering/support", - "type": "custom" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2023-12-20T15:40:13+00:00" - }, { "name": "geocoder-php/common-http", "version": "4.7.0", @@ -396,183 +330,6 @@ }, "time": "2024-10-02T11:20:13+00:00" }, - { - "name": "php-http/message", - "version": "1.16.2", - "source": { - "type": "git", - "url": "https://github.com/php-http/message.git", - "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", - "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", - "shasum": "" - }, - "require": { - "clue/stream-filter": "^1.5", - "php": "^7.2 || ^8.0", - "psr/http-message": "^1.1 || ^2.0" - }, - "provide": { - "php-http/message-factory-implementation": "1.0" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.6", - "ext-zlib": "*", - "guzzlehttp/psr7": "^1.0 || ^2.0", - "laminas/laminas-diactoros": "^2.0 || ^3.0", - "php-http/message-factory": "^1.0.2", - "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", - "slim/slim": "^3.0" - }, - "suggest": { - "ext-zlib": "Used with compressor/decompressor streams", - "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", - "laminas/laminas-diactoros": "Used with Diactoros Factories", - "slim/slim": "Used with Slim Framework PSR-7 implementation" - }, - "type": "library", - "autoload": { - "files": [ - "src/filters.php" - ], - "psr-4": { - "Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "HTTP Message related tools", - "homepage": "http://php-http.org", - "keywords": [ - "http", - "message", - "psr-7" - ], - "support": { - "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.16.2" - }, - "time": "2024-10-02T11:34:13+00:00" - }, - { - "name": "php-http/message-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-http/message-factory.git", - "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", - "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", - "shasum": "" - }, - "require": { - "php": ">=5.4", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Factory interfaces for PSR-7 HTTP Message", - "homepage": "http://php-http.org", - "keywords": [ - "factory", - "http", - "message", - "stream", - "uri" - ], - "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/1.1.0" - }, - "abandoned": "psr/http-factory", - "time": "2023-04-14T14:16:17+00:00" - }, - { - "name": "psr/http-message", - "version": "2.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" - }, { "name": "willdurand/geocoder", "version": "5.0.0", From ffc7cf7639d54d6041901db9dab89ecdc1795b86 Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 24 Sep 2026 08:28:41 +0200 Subject: [PATCH 25/35] chore: remove conventional changelog common packages checks between GLPI and the plugin has been hardened --- composer.json | 3 - composer.lock | 818 +------------------------------------------------- 2 files changed, 2 insertions(+), 819 deletions(-) diff --git a/composer.json b/composer.json index 4cb38c7a..659812bf 100644 --- a/composer.json +++ b/composer.json @@ -5,9 +5,6 @@ "geocoder-php/nominatim-provider": "^5.7", "league/iso3166": "^4.3" }, - "require-dev": { - "marcocesarato/php-conventional-changelog": "^1.17" - }, "config": { "optimize-autoloader": true, "platform": { diff --git a/composer.lock b/composer.lock index 5ee0713f..b5d2cf46 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": "029fced46fc6d9a65d185053ce0527f9", + "content-hash": "20397c57834cf59058491e3259bd0375", "packages": [ { "name": "geocoder-php/common-http", @@ -393,821 +393,7 @@ "time": "2025-01-01T15:52:42+00:00" } ], - "packages-dev": [ - { - "name": "marcocesarato/php-conventional-changelog", - "version": "1.17.3", - "source": { - "type": "git", - "url": "https://github.com/marcocesarato/php-conventional-changelog.git", - "reference": "c49b4a69ddf9ecbf055e8029c65e54a956d7ffa2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/marcocesarato/php-conventional-changelog/zipball/c49b4a69ddf9ecbf055e8029c65e54a956d7ffa2", - "reference": "c49b4a69ddf9ecbf055e8029c65e54a956d7ffa2", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-mbstring": "*", - "php": ">=7.1.3", - "symfony/console": "^4 || ^5 || ^6 || ^7 || ^8" - }, - "require-dev": { - "brainmaestro/composer-git-hooks": "^2.8", - "friendsofphp/php-cs-fixer": "^3.8", - "php-mock/php-mock": "^2.3", - "php-mock/php-mock-phpunit": "^2.6", - "phpunit/phpunit": "^9.6" - }, - "bin": [ - "conventional-changelog" - ], - "type": "library", - "extra": { - "hooks": { - "pre-push": "composer check-cs", - "post-merge": "composer install", - "pre-commit": "composer fix-cs" - } - }, - "autoload": { - "psr-4": { - "ConventionalChangelog\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-3.0-or-later" - ], - "authors": [ - { - "name": "Marco Cesarato", - "email": "cesarato.developer@gmail.com" - } - ], - "description": "Generate changelogs and release notes from a project's commit messages and metadata and automate versioning with semver.org and conventionalcommits.org", - "keywords": [ - "changelog", - "commit", - "commits", - "convention", - "conventional", - "conventional-changelog", - "conventional-changelog-preset", - "conventional-commit", - "conventional-commits", - "conventionalcommits", - "generation", - "git", - "history", - "php", - "readme", - "tag" - ], - "support": { - "issues": "https://github.com/marcocesarato/php-conventional-changelog/issues", - "source": "https://github.com/marcocesarato/php-conventional-changelog/tree/v1.17.3" - }, - "time": "2026-01-21T11:40:14+00:00" - }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, - { - "name": "symfony/console", - "version": "v7.4.18", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/23d6f88a29f6d0eac45bd77d70307adf83ba7ab0", - "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2|^8.0" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/lock": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/v7.4.18" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-08-25T14:18:37+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.7.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", - "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-dev" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-06-05T06:23:12+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.37.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "141046a8f9477948ff284fa65be2095baafb94f2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", - "reference": "141046a8f9477948ff284fa65be2095baafb94f2", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-04-10T16:19:22+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.41.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", - "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-07-28T08:25:59+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.42.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", - "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-08-07T06:33:24+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.38.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-05-27T06:59:30+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v3.7.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", - "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-07-27T15:39:01+00:00" - }, - { - "name": "symfony/string", - "version": "v7.4.15", - "source": { - "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "e394af32256bf9e7bf80849d95e589167c10097b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b", - "reference": "e394af32256bf9e7bf80849d95e589167c10097b", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.33", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/translation-contracts": "<2.5" - }, - "require-dev": { - "symfony/emoji": "^7.1|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/intl": "^6.4|^7.0|^8.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0" - }, - "type": "library", - "autoload": { - "files": [ - "Resources/functions.php" - ], - "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", - "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], - "support": { - "source": "https://github.com/symfony/string/tree/v7.4.15" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-07-28T07:33:02+00:00" - } - ], + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": {}, From 18e366a729190b4d84d054daa970177b58c5d8af Mon Sep 17 00:00:00 2001 From: btry Date: Thu, 24 Sep 2026 09:10:50 +0200 Subject: [PATCH 26/35] docs: typo --- src/DataSource/CarbonIntensity/ElectricityMaps/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php b/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php index 0564e95a..b046d6ba 100644 --- a/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php +++ b/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php @@ -334,7 +334,7 @@ public function fetchRange(DateTimeImmutable $start, DateTimeImmutable $stop, So while ($current_date < $request_stop) { $stop = clone $current_date; $stop->add($step); - // For some reason, passing the parameters as a query stringthrough Guzzle + // For some reason, passing the parameters as a query string through Guzzle // Makes the request malformed from the point of view of Electricitymaps // Workarounded by building here the query string // $params = [ From 0c81203b122eb22f0e1467b2314b7a6f407b37ce Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Thu, 25 Jun 2026 09:06:58 +0200 Subject: [PATCH 27/35] test: code cleanup --- .../DataSource/CarbonIntensity/ElectricityMaps/ConfigTest.php | 4 ---- tests/units/DataSource/Lca/Boaviztapi/ConfigTest.php | 4 ---- 2 files changed, 8 deletions(-) diff --git a/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ConfigTest.php b/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ConfigTest.php index da22ef07..034a2347 100644 --- a/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ConfigTest.php +++ b/tests/units/DataSource/CarbonIntensity/ElectricityMaps/ConfigTest.php @@ -99,10 +99,6 @@ public function testConfigUpdate(array $input, array $expected) /** @var array $CFG_GLPI */ global $CFG_GLPI; - $CFG_GLPI['plugi:carbon']['lca_datasources'] = [ - Client::class, - ]; - $instance = new Config(); $result = $instance->configUpdate($input); $this->assertEquals($expected, $result); diff --git a/tests/units/DataSource/Lca/Boaviztapi/ConfigTest.php b/tests/units/DataSource/Lca/Boaviztapi/ConfigTest.php index 7f5249e1..ef3a40b7 100644 --- a/tests/units/DataSource/Lca/Boaviztapi/ConfigTest.php +++ b/tests/units/DataSource/Lca/Boaviztapi/ConfigTest.php @@ -124,10 +124,6 @@ public function testConfigUpdate(array $input, array $expected) /** @var array $CFG_GLPI */ global $CFG_GLPI; - $CFG_GLPI['plugi:carbon']['lca_datasources'] = [ - Client::class, - ]; - $result = (new Config())->configUpdate($input); $this->assertEquals($expected, $result); } From cbfd9e22d9d7818e1bf7792f4fd6a030066f70d3 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Thu, 25 Jun 2026 14:36:59 +0200 Subject: [PATCH 28/35] feat(Impact\Embodied\Boavizta): support for cloud servers depends on CloudInventory plugin --- setup.php | 3 + src/CloudInventoryConnector.php | 57 +++++++ src/ComputerType.php | 2 + src/DataSource/Lca/Boaviztapi/Client.php | 26 ++++ src/Impact/Embodied/Boavizta/Computer.php | 154 +++++++++++++++---- stubs/OptionalClass.php.stub | 25 +++ templates/environmentalimpact-item.html.twig | 6 + tests/src/CommonTestCase.php | 6 +- 8 files changed, 244 insertions(+), 35 deletions(-) create mode 100644 src/CloudInventoryConnector.php create mode 100644 stubs/OptionalClass.php.stub diff --git a/setup.php b/setup.php index 753a6b3c..6e35edff 100644 --- a/setup.php +++ b/setup.php @@ -34,6 +34,7 @@ use CronTask as GlpiCronTask; use Glpi\Config\ProxyExclusion; use Glpi\Plugin\Hooks; +use GlpiPlugin\Carbon\CloudInventoryConnector; use GlpiPlugin\Carbon\Config; use GlpiPlugin\Carbon\CronTask; use GlpiPlugin\Carbon\Dashboard\Grid; @@ -124,6 +125,8 @@ function plugin_carbon_setupHooks() LcaClientFactory::getSecuredConfigs() ); + $PLUGIN_HOOKS[Hooks::POST_INIT]['carbon'] = [CloudInventoryConnector::class, 'checkPluginAvailability']; + // add new cards to the dashboard $PLUGIN_HOOKS[Hooks::DASHBOARD_CARDS]['carbon'] = [Grid::class, 'getDashboardCards']; $PLUGIN_HOOKS[Hooks::DASHBOARD_TYPES]['carbon'] = [Widget::class, 'WidgetTypes']; diff --git a/src/CloudInventoryConnector.php b/src/CloudInventoryConnector.php new file mode 100644 index 00000000..491a8a73 --- /dev/null +++ b/src/CloudInventoryConnector.php @@ -0,0 +1,57 @@ +. + * + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Carbon; + +use Plugin; + +/** + * Establish the functional bridge between Carbon and the plugin CloudInventory + */ +class CloudInventoryConnector +{ + public static function checkPluginAvailability(): void + { + /** @var array $CFG_GLPI */ + global $CFG_GLPI; + + $CFG_GLPI['plugin:carbon']['use_cloudinventory'] = Plugin::isPluginActive('cloudinventory'); + } + + public function pluginAvailable(): bool + { + /** @var array $CFG_GLPI */ + global $CFG_GLPI; + + return $CFG_GLPI['plugin:carbon']['use_cloudinventory'] ?? false; + } +} diff --git a/src/ComputerType.php b/src/ComputerType.php index 2ef95b35..df0eb829 100644 --- a/src/ComputerType.php +++ b/src/ComputerType.php @@ -50,6 +50,7 @@ class ComputerType extends AbstractChildDropdown public const CATEGORY_LAPTOP = 3; public const CATEGORY_TABLET = 4; public const CATEGORY_SMARTPHONE = 5; + public const CATEGORY_CLOUD = 6; public static function getCategories(): array { @@ -60,6 +61,7 @@ public static function getCategories(): array self::CATEGORY_LAPTOP => __('Laptop', 'carbon'), self::CATEGORY_TABLET => __('Tablet', 'carbon'), self::CATEGORY_SMARTPHONE => __('Smartphone', 'carbon'), + self::CATEGORY_CLOUD => __('Cloud server', 'carbon'), ]; } diff --git a/src/DataSource/Lca/Boaviztapi/Client.php b/src/DataSource/Lca/Boaviztapi/Client.php index 9bcf9db4..7685e38c 100644 --- a/src/DataSource/Lca/Boaviztapi/Client.php +++ b/src/DataSource/Lca/Boaviztapi/Client.php @@ -44,6 +44,7 @@ use GlpiPlugin\Carbon\Zone; use Override; use RuntimeException; +use Session; use function Safe\json_encode; @@ -261,6 +262,16 @@ public static function getZones() public function parseResponse(array $response, string $scope): array { $impacts = []; + if (!isset($response['impacts'])) { + if (Session::getLoginUserID(true)) { + $message = __('An error occured while processing the response from Boaviztapi', 'carbon'); + $message .= '
' . ($response['detail'] ?? 'unknown error'); + Session::addMessageAfterRedirect($message, true, ERROR); + } + trigger_error('Error parsing the response ' . var_export($response, true), E_USER_WARNING); + return $impacts; + } + $types = Type::getImpactTypes(); foreach ($response['impacts'] as $type => $impact) { if (!in_array($type, $types)) { @@ -304,4 +315,19 @@ public static function dropdownBoaviztaZone(string $name, array $options = []) { return Dropdown::showFromArray($name, self::getZones(), $options); } + + /** + * Get instances known types of a cloud provider + * + * @param string $provider + * @return array isntances types + */ + public function getCloudInstances(string $provider): array + { + $response = $this->get('cloud/instance/all_instances', [ + 'query' => ['provider' => $provider], + ]); + + return $response; + } } diff --git a/src/Impact/Embodied/Boavizta/Computer.php b/src/Impact/Embodied/Boavizta/Computer.php index 9d16f684..6da6f865 100644 --- a/src/Impact/Embodied/Boavizta/Computer.php +++ b/src/Impact/Embodied/Boavizta/Computer.php @@ -35,10 +35,19 @@ use CommonDBTM; use Computer as GlpiComputer; +use ComputerModel as GlpiComputerModel; use ComputerType as GlpiComputerType; +use GlpiPlugin\Carbon\CloudInventoryConnector; use GlpiPlugin\Carbon\ComputerType; use GlpiPlugin\Carbon\DataSource\Lca\Boaviztapi\ComputerModelizationAdapterTrait; +use GlpiPlugin\Cloudinventory\Amazon; +use GlpiPlugin\Cloudinventory\Azure; +use GlpiPlugin\Cloudinventory\CloudInstance; +use GlpiPlugin\Cloudinventory\Google; +use GlpiPlugin\Cloudinventory\Ovh; +use GlpiPlugin\Cloudinventory\Scaleway; use Override; +use UnhandledMatchError; class Computer extends AbstractAsset { @@ -48,34 +57,113 @@ class Computer extends AbstractAsset protected string $endpoint = 'server'; + /** + * If the plugin CloudInventory is available, this is an oblect from that + * plugin representing the cloud related data of the computer + */ + protected ?CloudInstance $cloud_instance = null; + + /** + * @var array Description of the asset for querying Boaviztapi + */ + protected array $description = []; + #[Override] protected function doEvaluation(): ?array { - // adapt $this->endpoint depending on the type of computer (server, laptop, ...) $type = $this->getType($this->item); - $this->endpoint = $this->getEndpoint($type); + + $response = null; + $this->chooseEvaluationMode($type); + + // select all impact types $this->endpoint .= '?' . $this->getCriteriasQueryString(); - // Ask for embodied impact only - $handle_hardware = in_array($type, [ - ComputerType::CATEGORY_SERVER, - ComputerType::CATEGORY_DESKTOP, - ComputerType::CATEGORY_UNDEFINED, - ]); - $configuration = $this->analyzeHardware(); - if ($handle_hardware && count($configuration) === 0) { - return null; + // Query Boaviztapi + $response = $this->query($this->description); + + $impacts = $this->client->parseResponse($response, 'embedded'); + return $impacts; + } + + private function chooseEvaluationMode(int $type): string + { + if ($type === ComputerType::CATEGORY_CLOUD) { + $cloud_provider = ''; + switch ($this->cloud_instance->fields['itemtype']) { + case Amazon::class: + $cloud_provider = 'aws'; + break; + case Azure::class: + $cloud_provider = 'azure'; + break; + case Google::class: + $cloud_provider = 'gcp'; + break; + case Ovh::class: + $cloud_provider = 'ovhcloud'; + break; + case Scaleway::class: + $cloud_provider = 'scaleway'; + break; + } + $glpi_computer_model = GlpiComputerModel::getById($this->cloud_instance->fields['computermodels_id']); + if ($glpi_computer_model !== false) { + $instance_types = $this->client->getCloudInstances($cloud_provider); + $model = $this->normalizeModel($cloud_provider, $glpi_computer_model->fields['name']); + if (in_array($model, $instance_types)) { + $this->prepareCloudDescription($cloud_provider, $model); + return 'cloud'; + } + } } - $description = [ - 'configuration' => $configuration, + + $this->prepareHardwareDescription($type); + return 'hardware'; + } + + /** + * Prepare description of the asset for the Boaviztapi query + */ + private function prepareHardwareDescription(int $type): void + { + try { + $this->endpoint = match ($type) { + ComputerType::CATEGORY_SERVER => 'server', + ComputerType::CATEGORY_LAPTOP => 'terminal/laptop', + ComputerType::CATEGORY_TABLET => 'terminal/tablet', + ComputerType::CATEGORY_SMARTPHONE => 'terminal/smartphone', + }; + } catch (UnhandledMatchError $e) { + $this->endpoint = 'terminal/desktop'; + } + + $this->description = [ + 'configuration' => $this->analyzeHardware(), 'usage' => [ 'avg_power' => 0, ], ]; - $response = $this->query($description); - $impacts = $this->client->parseResponse($response, 'embedded'); + } - return $impacts; + /** + * Prepare description of the asset for the Boaviztapi query + * + * @param string $provider + * @param string $model + * @return void + */ + private function prepareCloudDescription(string $provider, string $model) + { + $this->endpoint = 'cloud/instance'; + + $this->description = [ + 'usage' => [ + 'avg_power' => 0, + ], + ]; + $this->description['provider'] = $provider; + $this->description['instance_type'] = $model; } /** @@ -85,6 +173,18 @@ protected function doEvaluation(): ?array */ protected function getType(CommonDBTM $item): int { + $cloudInventory_connector = new CloudInventoryConnector(); + if ($cloudInventory_connector->pluginAvailable()) { + $cloud_instance = new CloudInstance(); + $cloud_instance->getFromDBByCrit([ + 'computers_id' => $item->getID(), + ]); + if (!$cloud_instance->isNewItem()) { + $this->cloud_instance = $cloud_instance; + return ComputerType::CATEGORY_CLOUD; + } + } + $computer_table = GlpiComputer::getTable(); $computer_type_table = ComputerType::getTable(); $glpi_computer_type_table = GlpiComputerType::getTable(); @@ -115,24 +215,14 @@ protected function getType(CommonDBTM $item): int return $computer_type->fields['category']; } - /** - * Get the endpoint to use for the given type - */ - protected function getEndpoint(int $type) + protected function normalizeModel(string $provider, string $model): string { - switch ($type) { - case ComputerType::CATEGORY_SERVER: - return 'server'; - case ComputerType::CATEGORY_LAPTOP: - return 'terminal/laptop'; - case ComputerType::CATEGORY_TABLET: - return 'terminal/tablet'; - case ComputerType::CATEGORY_SMARTPHONE: - return 'terminal/smartphone'; + switch ($provider) { + case 'scaleway': + // CloudInventory sets scaleway models with the prefix "SCW-" + return strtolower(substr($model, 4)); } - // ComputerType::CATEGORY_UNDEFINED - // ComputerType::CATEGORY_DESKTOP - return 'terminal/desktop'; + return $provider; } } diff --git a/stubs/OptionalClass.php.stub b/stubs/OptionalClass.php.stub new file mode 100644 index 00000000..6a363cf0 --- /dev/null +++ b/stubs/OptionalClass.php.stub @@ -0,0 +1,25 @@ + */ + public $fields = []; + + /** + * @param array $criteria + * @return bool + * + * @phpstan-impure + */ + public function getFromDBByCrit(array $criteria): bool {} + + public function isNewItem(): bool {} +} + +class Amazon {} +class Azure {} +class Google {} +class Ovh {} +class Scaleway {} diff --git a/templates/environmentalimpact-item.html.twig b/templates/environmentalimpact-item.html.twig index 436e6525..f336a014 100644 --- a/templates/environmentalimpact-item.html.twig +++ b/templates/environmentalimpact-item.html.twig @@ -73,6 +73,12 @@ 'fas fa-chart-pie' ) }} +{% if ((embodied_impact.fields['engine'] is defined) and (embodied_impact.fields['engine_version'] is defined)) %} + {{ fields.smallTitle( + embodied_impact.fields['engine'] ~ ' ' ~ embodied_impact.fields['engine_version'], + ) }} +{% endif %} +
diff --git a/tests/src/CommonTestCase.php b/tests/src/CommonTestCase.php index 35abfb7a..97d2e182 100644 --- a/tests/src/CommonTestCase.php +++ b/tests/src/CommonTestCase.php @@ -64,7 +64,7 @@ class CommonTestCase extends TestCase /** @var int $debugMode save state of GLPI debug mode */ private $debugMode = null; - protected $str = null; + protected ?string $str = null; protected function disableDebug() { @@ -115,7 +115,7 @@ protected function setupGLPIFramework(): void return; } - protected function login($name, $password, $noauto = false) + protected function login(string $name, string $password, $noauto = false) { Session::start(); $auth = new Auth(); @@ -539,7 +539,7 @@ protected function isolateInEntity(): int * * @return mixed */ - protected function callPrivateMethod($instance, string $methodName, ...$args) + protected function callPrivateMethod($instance, string $methodName, mixed ...$args) { $method = new ReflectionMethod($instance, $methodName); if (version_compare(PHP_VERSION, '8.1.0') < 0) { From 60d7d740a362a8770b88d87893def866e5b10372 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Fri, 3 Jul 2026 22:24:37 +0200 Subject: [PATCH 29/35] ff --- src/Impact/History/Computer.php | 2 + templates/history/status-item.html.twig | 8 +++ tests/units/Impact/History/ComputerTest.php | 57 +++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/Impact/History/Computer.php b/src/Impact/History/Computer.php index bb89449b..65e1e27a 100644 --- a/src/Impact/History/Computer.php +++ b/src/Impact/History/Computer.php @@ -165,6 +165,7 @@ public function getEvaluableQuery(array $crit = [], bool $entity_restrict = true 'AND' => [ self::$itemtype::getTableField('is_deleted') => 0, self::$itemtype::getTableField('is_template') => 0, + ['NOT' => [ComputerType::getTableField('category') => ComputerType::CATEGORY_CLOUD]], Location::getTableField('plugin_carbon_sources_zones_id') => ['>', 0], 'OR' => [ [ComputerType::getTableField('is_ignore') => 0], @@ -259,6 +260,7 @@ public static function getHistorizableDiagnosis(CommonDBTM $item): ?array $status['has_type_power_consumption'] = (($data['type_power_consumption'] ?? 0) !== 0); $status['has_usage_profile'] = !ComputerUsageProfile::isNewID($data['plugin_carbon_computerusageprofiles_id']); $status['has_category'] = (($data['category'] ?? 0) !== ComputerType::CATEGORY_UNDEFINED); + $status['is__category_not_cloud'] = (($data['category'] ?? 0) !== ComputerType::CATEGORY_CLOUD); $status['ci_download_enabled'] = $is_carbon_intensity_download_enabled; $status['ci_fallback_available'] = $is_carbon_intensity_fallback_available; $status['not_is_ignore'] = (($data['is_ignore'] ?? 0) === 0); diff --git a/templates/history/status-item.html.twig b/templates/history/status-item.html.twig index 8031a6a2..469bbd2b 100644 --- a/templates/history/status-item.html.twig +++ b/templates/history/status-item.html.twig @@ -193,6 +193,14 @@ {{ fields.field('', field, __('The asset has a category', 'carbon')) }} {% endif %} + {% if status.is_category_not_cloud is defined %} + {% set field = nok_field %} + {% if status.is_category_not_cloud %} + {% set field = ok_field %} + {% endif %} + {{ fields.field('', field, __('The asset is not in a cloud', 'carbon')) }} + {% endif %} + {% if status.has_usage_profile is defined %} {% set field = nok_field %} {% if status.has_usage_profile %} diff --git a/tests/units/Impact/History/ComputerTest.php b/tests/units/Impact/History/ComputerTest.php index 9d4eeda6..1a4cebe0 100644 --- a/tests/units/Impact/History/ComputerTest.php +++ b/tests/units/Impact/History/ComputerTest.php @@ -291,6 +291,7 @@ public function test_getHistorizableDiagnosis_when_computer_is_historizable() 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -329,6 +330,7 @@ public function test_getHistorizableDiagnosis_when_computer_is_deleted() 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -367,6 +369,7 @@ public function test_getHistorizableDiagnosis_when_computer_is_template() 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -405,6 +408,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_usage_profile 'has_usage_profile' => false, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -443,6 +447,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_location() 'has_usage_profile' => true, 'has_location' => false, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => false, // No location cascades this requirement to be not met 'has_model' => true, 'has_model_power_consumption' => true, @@ -481,6 +486,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_category() 'has_usage_profile' => true, 'has_location' => true, 'has_category' => false, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -519,6 +525,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_carbon_intens 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => false, 'has_model' => true, 'has_model_power_consumption' => true, @@ -557,6 +564,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_model() 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => false, 'has_model_power_consumption' => false, @@ -595,6 +603,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_model_power_c 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => false, @@ -633,6 +642,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_type() 'has_usage_profile' => true, 'has_location' => true, 'has_category' => false, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -671,6 +681,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_type_power_co 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -709,6 +720,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_inventory_ent 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -747,6 +759,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_carbon_intens 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -787,6 +800,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_carbon_intens 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -825,6 +839,7 @@ public function test_getHistorizableDiagnosis_when_computer_is_ignored() 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -864,6 +879,7 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_decommission_ 'has_usage_profile' => true, 'has_location' => true, 'has_category' => true, + 'is__category_not_cloud' => true, 'has_carbon_intensity_zone' => true, 'has_model' => true, 'has_model_power_consumption' => true, @@ -879,6 +895,46 @@ public function test_getHistorizableDiagnosis_when_computer_has_no_decommission_ $this->assertEquals($expected, $result); } + public function test_getHistorizableDiagnosis_when_computer_has_cloud_category() + { + $history = new Computer(); + + [ + $glpi_computer, + $glpi_location, + $location, + $source_zone, + $glpi_computer_model, + $glpi_computer_type, + $computer_type, + $infocom, + $usage_profile, + $zone, + ] = $this->getHistorizableComputer(); + $this->updateItem($computer_type, ['category' => ComputerType::CATEGORY_CLOUD]); + + $expected = [ + 'is_deleted' => true, + 'is_template' => true, + 'has_usage_profile' => true, + 'has_location' => true, + 'has_category' => true, + 'is__category_not_cloud' => false, + 'has_carbon_intensity_zone' => true, + 'has_model' => true, + 'has_model_power_consumption' => true, + 'has_type' => true, + 'has_type_power_consumption' => true, + 'has_inventory_entry_date' => true, + 'ci_download_enabled' => true, + 'ci_fallback_available' => true, + 'not_is_ignore' => true, + 'has_decommission_date' => true, + ]; + $result = $history->getHistorizableDiagnosis($glpi_computer); + $this->assertEquals($expected, $result); + } + public function testComputerWithEverythingIsHistorizable() { $history = new Computer(); @@ -933,6 +989,7 @@ public function testComputerWithEverythingIsHistorizable() 'has_type_power_consumption' => true, 'has_usage_profile' => true, 'has_category' => false, + 'is__category_not_cloud' => true, 'has_inventory_entry_date' => true, 'ci_download_enabled' => true, 'ci_fallback_available' => true, From 5a7c2c7a11c9e893938be0c92cd81eee3b3889f2 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Wed, 1 Jul 2026 09:05:41 +0200 Subject: [PATCH 30/35] fix(CarbonIntensity): limit downloads only if the limit is set --- src/CarbonIntensity.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CarbonIntensity.php b/src/CarbonIntensity.php index 7851a359..e33992f2 100644 --- a/src/CarbonIntensity.php +++ b/src/CarbonIntensity.php @@ -251,7 +251,7 @@ public function downloadOneZone(ClientInterface $data_source, Source_Zone $sourc $gap_end = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $gap['end']); $count = $data_source->fullDownload($source_zone, $gap_start, $gap_end, $this, $limit, $progress_bar); $total_count += $count; - if ($total_count >= $limit) { + if ($limit > 0 && $total_count >= $limit) { return $total_count; } } From 808f574effc144d648fe5c23ead6172658b98e5e Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Wed, 1 Jul 2026 09:27:48 +0200 Subject: [PATCH 31/35] fix(DataSource\CarbonIntensity): cache only if the extended range end is in the pastwithout this constraint, a download occurring when the +14 hours is in the future leadds to a truncated cache, as some data is not kown --- src/DataSource/CarbonIntensity/ElectricityMaps/Client.php | 4 +++- src/DataSource/CarbonIntensity/Rte/Client.php | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php b/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php index b046d6ba..9f93af96 100644 --- a/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php +++ b/src/DataSource/CarbonIntensity/ElectricityMaps/Client.php @@ -380,7 +380,9 @@ public function fetchRange(DateTimeImmutable $start, DateTimeImmutable $stop, So } $downloaded_year_month = $start->format('Y-m'); - if (count($full_response) > 0 && $downloaded_year_month < date('Y-m')) { + if (count($full_response) > 0 && $downloaded_year_month < date('Y-m') && date('j') > 1) { + // Cache only if the month being processed is older than the month of now + // and we are at least the 2nd day of the current month (to handle +14 hours overlap) $json = json_encode($full_response); file_put_contents($cache_file, $json); } diff --git a/src/DataSource/CarbonIntensity/Rte/Client.php b/src/DataSource/CarbonIntensity/Rte/Client.php index 2cee7b1a..44960896 100644 --- a/src/DataSource/CarbonIntensity/Rte/Client.php +++ b/src/DataSource/CarbonIntensity/Rte/Client.php @@ -339,8 +339,9 @@ public function fetchRange(DateTimeImmutable $start, DateTimeImmutable $stop, So } } else { $downloaded_year_month = $start->format('Y-m'); - if ($downloaded_year_month < date('Y-m')) { + if (count($response) > 0 && $downloaded_year_month < date('Y-m') && date('j') > 1) { // Cache only if the month being processed is older than the month of now + // and we are at least the 2nd day of the current month (to handle +14 hours overlap) $json = json_encode($response); file_put_contents($cache_file, $json); } From d9269d82486731412cfa926da914717d00865320 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Wed, 1 Jul 2026 14:37:55 +0200 Subject: [PATCH 32/35] refactor(Impact\Embodied\Boavizta): factorize null usage representation --- src/Impact/Embodied/Boavizta/AbstractAsset.php | 4 ++++ src/Impact/Embodied/Boavizta/Computer.php | 8 ++------ src/Impact/Embodied/Boavizta/Monitor.php | 4 +--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Impact/Embodied/Boavizta/AbstractAsset.php b/src/Impact/Embodied/Boavizta/AbstractAsset.php index bf889620..81c15b7f 100644 --- a/src/Impact/Embodied/Boavizta/AbstractAsset.php +++ b/src/Impact/Embodied/Boavizta/AbstractAsset.php @@ -57,6 +57,10 @@ abstract class AbstractAsset extends AbstractEmbodiedImpact implements AssetInte /** @var Client instance of the HTTP client */ protected ?Client $client = null; + protected const USAGE_NULL = [ + 'avg_power' => 0, + ]; + // abstract public static function getEngine(CommonDBTM $item): EngineInterface; /** diff --git a/src/Impact/Embodied/Boavizta/Computer.php b/src/Impact/Embodied/Boavizta/Computer.php index 6da6f865..ca1188ca 100644 --- a/src/Impact/Embodied/Boavizta/Computer.php +++ b/src/Impact/Embodied/Boavizta/Computer.php @@ -140,9 +140,7 @@ private function prepareHardwareDescription(int $type): void $this->description = [ 'configuration' => $this->analyzeHardware(), - 'usage' => [ - 'avg_power' => 0, - ], + 'usage' => self::USAGE_NULL, ]; } @@ -158,9 +156,7 @@ private function prepareCloudDescription(string $provider, string $model) $this->endpoint = 'cloud/instance'; $this->description = [ - 'usage' => [ - 'avg_power' => 0, - ], + 'usage' => self::USAGE_NULL, ]; $this->description['provider'] = $provider; $this->description['instance_type'] = $model; diff --git a/src/Impact/Embodied/Boavizta/Monitor.php b/src/Impact/Embodied/Boavizta/Monitor.php index 1e8acdb0..91298400 100644 --- a/src/Impact/Embodied/Boavizta/Monitor.php +++ b/src/Impact/Embodied/Boavizta/Monitor.php @@ -52,9 +52,7 @@ protected function doEvaluation(): ?array $description = [ 'configuration' => $configuration, - 'usage' => [ - 'avg_power' => 0, - ], + 'usage' => self::USAGE_NULL, ]; $response = $this->query($description); $impacts = $this->client->parseResponse($response, 'embedded'); From 2304d06a2c80c90e456af776fdaef03b4c7e55f4 Mon Sep 17 00:00:00 2001 From: Thierry Bugier Date: Wed, 1 Jul 2026 15:38:30 +0200 Subject: [PATCH 33/35] style(Dashboard\Dashboard): dead code cleanup --- src/Dashboard/Dashboard.php | 18 ------------------ src/Dashboard/Widget.php | 7 ------- 2 files changed, 25 deletions(-) diff --git a/src/Dashboard/Dashboard.php b/src/Dashboard/Dashboard.php index 14d18ccf..4efc4eb7 100644 --- a/src/Dashboard/Dashboard.php +++ b/src/Dashboard/Dashboard.php @@ -67,22 +67,4 @@ public static function getTotalPowerPerModel(): array { return Provider::getSumPowerPerModel([ComputerModel::getTableField('power_consumption') => ['>', '0']]); } - - public static function cardCarbonintensityProvider(array $params = []) - { - $default_params = [ - 'label' => __('Carbon dioxyde intensity', 'carbon'), - 'icon' => "fas fa-computer", - 'color' => '#ea9999', - ]; - $params = array_merge($default_params, $params); - - $data = Provider::getCarbonIntensity($params); - - return [ - 'data' => $data, - 'label' => $params['label'], - 'icon' => $params['icon'], - ]; - } } diff --git a/src/Dashboard/Widget.php b/src/Dashboard/Widget.php index 62629b5a..863d49ba 100644 --- a/src/Dashboard/Widget.php +++ b/src/Dashboard/Widget.php @@ -102,13 +102,6 @@ public static function WidgetTypes(?array $types = null): array 'width' => 16, 'height' => 12, ], - 'usage_abiotic_depletion' => [ - 'label' => __('Usage abiotic depletion potential', 'carbon'), - 'function' => self::class . '::displayUsageAbioticDepletion', - 'image' => '', - 'width' => 6, - 'height' => 3, - ], 'impact_criteria_number' => [ 'label' => __('Impact criteria', 'carbon'), From 75acd2993631eb180013767dd0f7ad36de14e236 Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 25 Sep 2026 09:23:20 +0200 Subject: [PATCH 34/35] fix(Impact\History\Computer): more accurate status --- templates/history/status-item.html.twig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/history/status-item.html.twig b/templates/history/status-item.html.twig index 469bbd2b..aef066f1 100644 --- a/templates/history/status-item.html.twig +++ b/templates/history/status-item.html.twig @@ -134,6 +134,8 @@ {% set field = nok_field %} {% if status.has_model %} {% set field = ok_field %} + {% elseif status.has_type is defined and status.has_type %} + {% set field = warning_field %} {% endif %} {{ fields.field('', field, __('The asset has a model', 'carbon')) }} {% endif %} @@ -153,10 +155,8 @@ {% if status.has_type is defined %} {% set field = nok_field %} - {% if status.has_model is defined %} - {% if status.has_model %} - {% set field = warning_field %} - {% endif %} + {% if status.has_model is defined and status.has_model%} + {% set field = warning_field %} {% endif %} {% if status.has_type %} {% set field = ok_field %} From 4120b0ba6f972fcd9c6599b52c9b599775ec8b69 Mon Sep 17 00:00:00 2001 From: btry Date: Fri, 25 Sep 2026 09:35:28 +0200 Subject: [PATCH 35/35] fix(Zone): remove deprecated include --- ajax/dropdownZone.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/ajax/dropdownZone.php b/ajax/dropdownZone.php index 0e252231..4488fa20 100644 --- a/ajax/dropdownZone.php +++ b/ajax/dropdownZone.php @@ -34,8 +34,6 @@ use GlpiPlugin\Carbon\Source_Zone; use GlpiPlugin\Carbon\Zone; -include(__DIR__ . '/../../../inc/includes.php'); - // Check if plugin is activated... if (!Plugin::isPluginActive('carbon')) { throw new NotFoundHttpException();