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/Makefile b/Makefile
new file mode 100644
index 00000000..d4cfadbf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,2 @@
+include ../../PluginsMakefile.mk
+
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: |
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
deleted file mode 100644
index a3f8fe0a..00000000
--- a/ajax/toggleZoneDownload.php
+++ /dev/null
@@ -1,69 +0,0 @@
-.
- *
- * -------------------------------------------------------------------------
- */
-
-use Config as GlpiConfig;
-use GlpiPlugin\Carbon\Source;
-use GlpiPlugin\Carbon\Source_Zone;
-
-include(__DIR__ . '/../../../inc/includes.php');
-
-// 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();
-}
diff --git a/composer.json b/composer.json
index e9a9a0b1..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.1",
"marcocesarato/php-conventional-changelog": "^1.17"
},
"config": {
@@ -35,5 +34,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/composer.lock b/composer.lock
index 3a534dbe..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": "a821ef4c2ae32d2c6dce28a1a3b4ceb9",
+ "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.44",
+ "version": "v7.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "60e3944c4859c487aa6ea2f0f7754917f70f7524"
+ "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/60e3944c4859c487aa6ea2f0f7754917f70f7524",
- "reference": "60e3944c4859c487aa6ea2f0f7754917f70f7524",
+ "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.44"
+ "source": "https://github.com/symfony/console/tree/v7.4.18"
},
"funding": [
{
@@ -920,7 +864,7 @@
"type": "tidelift"
}
],
- "time": "2026-08-21T07:42:12+00:00"
+ "time": "2026-08-25T14:18:37+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -1330,16 +1274,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 +1337,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 +1357,7 @@
"type": "tidelift"
}
],
- "time": "2026-06-16T09:55:08+00:00"
+ "time": "2026-07-27T15:39:01+00:00"
},
{
"name": "symfony/string",
@@ -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/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 a195c0fa..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;
@@ -125,7 +126,9 @@
$progress_bar->setProgress($rows_count);
}
$file = null; // close the file
-
+ if (isset($output)) {
+ $output->writeln("");
+ }
Config::setPluginConfigurationValues(['ember_dataset_date' => EMBER_DATASET_DATE]);
}
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/package-lock.json b/package-lock.json
index ce930f9d..a5955b40 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": {
@@ -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"
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/phpstan.neon b/phpstan.neon
index c02769b3..8580ab79 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
@@ -5,6 +10,7 @@ parameters:
bootstrapFiles:
- ../../stubs/glpi_constants.php
- ../../vendor/autoload.php
+ - setup.php
paths:
- src
- front
@@ -17,5 +23,3 @@ parameters:
- ../../src
stubFiles:
- ../../stubs/glpi_constants.php
-rules:
- - GlpiProject\Tools\PHPStan\Rules\GlobalVarTypeRule
diff --git a/setup.php b/setup.php
index 5017da4b..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;
@@ -46,10 +47,13 @@
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.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 +67,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');
@@ -102,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()
@@ -123,7 +132,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';
}
@@ -223,8 +232,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;
}
@@ -239,22 +249,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;
}
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..95a9fa71 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
@@ -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/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..5c717a49 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)
@@ -229,6 +229,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/CarbonIntensity.php b/src/CarbonIntensity.php
index 4c9fc7ec..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;
/**
@@ -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..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');
@@ -117,7 +119,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..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;
@@ -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..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;
@@ -141,7 +142,7 @@ protected function configure()
}
#[Override]
- protected function execute(InputInterface $input, OutputInterface $output)
+ protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->output = $output;
@@ -161,6 +162,9 @@ protected function execute(InputInterface $input, OutputInterface $output)
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 9c36d94b..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;
@@ -60,7 +65,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;
@@ -93,10 +98,16 @@ protected function execute(InputInterface $input, OutputInterface $output)
$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("