From 613d2bb11a2beff40553e9d2dcab87c6f23e7036 Mon Sep 17 00:00:00 2001 From: Stanislas Kita <7335054+stonebuzz@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:21:10 +0100 Subject: [PATCH] Fix(Injection): keep models mapped to Fields containers usable --- CHANGELOG.md | 1 + inc/commoninjectionlib.class.php | 29 ++- inc/model.class.php | 9 + tests/fixtures/fields_stubs.php | 110 +++++++++++ tests/unit/FieldsContainerRightTest.php | 238 ++++++++++++++++++++++++ 5 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/fields_stubs.php create mode 100644 tests/unit/FieldsContainerRightTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ea0b461..8af2ae4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fix the uploaded file name being lost after the client injection upload step +- Fix models mapped to additional fields (Fields plugin) becoming inaccessible and their values being rejected on injection ## [2.15.12] - 2026-10-05 diff --git a/inc/commoninjectionlib.class.php b/inc/commoninjectionlib.class.php index 2154699a..f1ec0229 100644 --- a/inc/commoninjectionlib.class.php +++ b/inc/commoninjectionlib.class.php @@ -1784,6 +1784,27 @@ private function addCheckWarning(int $code, string $itemtype): void } + public static function isFieldsContainerItemtype(string $itemtype): bool + { + return is_a($itemtype, 'PluginFieldsAbstractContainerInstance', true); + } + + + public static function canWriteFieldsContainer(string $itemtype): bool + { + /** @var DBmysql $DB */ + global $DB; + + //The generated table stores its container id as the column default + $columns = $DB->listFields(getTableForItemType($itemtype)); + + return class_exists('PluginFieldsProfile') && PluginFieldsProfile::getRightOnContainer( + (int) $_SESSION['glpiactiveprofile']['id'], + (int) ($columns['plugin_fields_containers_id']['Default'] ?? 0), + ) > READ; + } + + /** * Perform data injection into GLPI DB * @@ -1801,7 +1822,13 @@ private function effectiveAddOrUpdate($injectionClass, $item, $values, $add = tr //Skipped without a session, as the lib is also a programmatic entry point for scripts. if (Session::getLoginUserID() !== false) { $input = is_array($values) ? $values : []; - if ($add) { + //Fields values are only written once the parent item passed its own check + if (self::isFieldsContainerItemtype($item::class)) { + if (!self::canWriteFieldsContainer($item::class)) { + $this->addCheckWarning($add ? self::ERROR_CANNOT_IMPORT : self::ERROR_CANNOT_UPDATE, $item::class); + return 0; + } + } elseif ($add) { //Passing the input to can() makes the check cover the target entity if (!$item->can(-1, CREATE, $input)) { $this->addCheckWarning(self::ERROR_CANNOT_IMPORT, $item::class); diff --git a/inc/model.class.php b/inc/model.class.php index 6a72d63a..9a073c8e 100644 --- a/inc/model.class.php +++ b/inc/model.class.php @@ -1290,6 +1290,15 @@ public static function checkRightOnModel(int $models_id): bool continue; } + //Fields denies direct writes by design: its values follow the model itemtype rights, checked in this loop + if (PluginDatainjectionCommonInjectionLib::isFieldsContainerItemtype($itemtype)) { + if (!PluginDatainjectionCommonInjectionLib::canWriteFieldsContainer($itemtype)) { + return false; + } + + continue; + } + $item = new $itemtype(); if ($check_add && !$item->canCreate()) { return false; diff --git a/tests/fixtures/fields_stubs.php b/tests/fixtures/fields_stubs.php new file mode 100644 index 00000000..bac1082b --- /dev/null +++ b/tests/fixtures/fields_stubs.php @@ -0,0 +1,110 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2007-2023 by DataInjection plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/datainjection + * ------------------------------------------------------------------------- + */ + +//Minimal stand-ins for the Fields plugin classes the rights checks rely on, loaded only when Fields is absent + +abstract class PluginFieldsAbstractContainerInstance extends CommonDBChild +{ + public static $itemtype = 'itemtype'; + + public static $items_id = 'items_id'; + + public static function canCreate(): bool + { + return false; + } + + public static function canUpdate(): bool + { + return false; + } +} + +class PluginFieldsProfile +{ + public static int $right = 0; + + public static function getRightOnContainer(int $profile_id, int $container_id): int + { + return $container_id === PluginFieldsComputerstub::CONTAINER_ID ? self::$right : 0; + } +} + +class PluginFieldsComputerstub extends PluginFieldsAbstractContainerInstance +{ + public const CONTAINER_ID = 42; + + public const VALUE_FIELD = 'stubvaluefield'; + + public static $rightname = 'computer'; +} + +class PluginFieldsComputerstubInjection extends PluginFieldsComputerstub implements PluginDatainjectionInjectionInterface +{ + public static $rightname = 'plugin_datainjection_model'; + + public static function getTable($classname = null): string + { + return getTableForItemType(PluginFieldsComputerstub::class); + } + + public function isPrimaryType(): bool + { + return false; + } + + public function connectedTo(): array + { + return [Computer::class]; + } + + public function getOptions($primary_type = ''): array + { + return [ + 76665 => [ + 'table' => self::getTable(), + 'field' => self::VALUE_FIELD, + 'name' => 'Stub - Value', + 'linkfield' => self::VALUE_FIELD, + 'datatype' => 'string', + 'displaytype' => 'text', + 'injectable' => PluginDatainjectionCommonInjectionLib::FIELD_INJECTABLE, + ], + ]; + } + + public function addOrUpdateObject($values = [], $options = []): array + { + $lib = new PluginDatainjectionCommonInjectionLib($this, $values, $options); + $lib->processAddOrUpdate(); + + return $lib->getInjectionResults(); + } +} diff --git a/tests/unit/FieldsContainerRightTest.php b/tests/unit/FieldsContainerRightTest.php new file mode 100644 index 00000000..48ab7ddd --- /dev/null +++ b/tests/unit/FieldsContainerRightTest.php @@ -0,0 +1,238 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2007-2023 by DataInjection plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/datainjection + * ------------------------------------------------------------------------- + */ + +namespace GlpiPlugin\Datainjection\Tests\Unit; + +use Computer; +use DBConnection; +use DBmysql; +use Entity; +use Glpi\Tests\DbTestCase; +use Plugin; +use PluginDatainjectionCommonInjectionLib; +use PluginDatainjectionComputerInjection; +use PluginDatainjectionMapping; +use PluginDatainjectionModel; +use PluginFieldsComputerstub; +use PluginFieldsProfile; +use Session; + +final class FieldsContainerRightTest extends DbTestCase +{ + private int $entities_id = 0; + + //Table DDL must run outside the per-test transaction opened by DbTestCase + public static function setUpBeforeClass(): void + { + if (Plugin::isPluginLoaded('fields')) { + self::markTestSkipped('Fields stubs cannot be declared while the fields plugin is loaded'); + } + + parent::setUpBeforeClass(); + + require_once __DIR__ . '/../fixtures/fields_stubs.php'; + + /** @var DBmysql $DB */ + global $DB; + $DB->doQuery(sprintf( + 'CREATE TABLE IF NOT EXISTS %s ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `items_id` INT UNSIGNED NOT NULL, + `itemtype` VARCHAR(255) DEFAULT %s, + `plugin_fields_containers_id` INT UNSIGNED NOT NULL DEFAULT %d, + `entities_id` INT UNSIGNED NOT NULL DEFAULT 0, + `%s` VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=%s COLLATE=%s ROW_FORMAT=DYNAMIC', + $DB->quoteName(PluginFieldsComputerstub::getTable()), + $DB->quoteValue(Computer::class), + PluginFieldsComputerstub::CONTAINER_ID, + PluginFieldsComputerstub::VALUE_FIELD, + DBConnection::getDefaultCharset(), + DBConnection::getDefaultCollation(), + )); + $DB->clearSchemaCache(); + } + + public static function tearDownAfterClass(): void + { + /** @var DBmysql $DB */ + global $DB; + $DB->dropTable(PluginFieldsComputerstub::getTable(), true); + $DB->clearSchemaCache(); + + parent::tearDownAfterClass(); + } + + public function setUp(): void + { + parent::setUp(); + + $this->login(); + $this->entities_id = getItemByTypeName(Entity::class, '_test_root_entity', true); + PluginFieldsProfile::$right = CREATE; + } + + private function createModelMappedToContainer(): int + { + $model = new PluginDatainjectionModel(); + $models_id = (int) $model->add([ + 'name' => 'Test_Model_Fields_' . random_int(1, PHP_INT_MAX), + 'itemtype' => Computer::class, + 'filetype' => 'csv', + 'behavior_add' => 1, + 'behavior_update' => 1, + 'entities_id' => $this->entities_id, + 'is_private' => 0, + 'users_id' => Session::getLoginUserID(), + ]); + $this->assertGreaterThan(0, $models_id); + + $mapping = new PluginDatainjectionMapping(); + $this->assertGreaterThan(0, $mapping->add([ + 'models_id' => $models_id, + 'itemtype' => PluginFieldsComputerstub::class, + 'rank' => 0, + 'name' => PluginFieldsComputerstub::VALUE_FIELD, + 'value' => PluginFieldsComputerstub::VALUE_FIELD, + 'is_mandatory' => 0, + ])); + + return $models_id; + } + + private function injectComputerWithFieldValue(string $name, string $value): array + { + $lib = new PluginDatainjectionCommonInjectionLib( + new PluginDatainjectionComputerInjection(), + [ + Computer::class => ['name' => $name, 'entities_id' => $this->entities_id], + PluginFieldsComputerstub::class => [PluginFieldsComputerstub::VALUE_FIELD => $value], + ], + [ + 'rights' => [ + 'can_add' => true, + 'can_update' => true, + 'add_dropdown' => true, + 'overwrite_notempty_fields' => true, + ], + 'mandatory_fields' => [Computer::class => ['name' => true]], + 'entities_id' => $this->entities_id, + ], + ); + $lib->processAddOrUpdate(); + + return $lib->getInjectionResults(); + } + + public function testModelMappedToFieldsContainerIsAllowedWithWriteRightOnContainer(): void + { + $models_id = $this->createModelMappedToContainer(); + + $this->assertTrue(PluginDatainjectionModel::checkRightOnModel($models_id)); + } + + public function testModelMappedToFieldsContainerIsDeniedWithReadRightOnContainer(): void + { + $models_id = $this->createModelMappedToContainer(); + + PluginFieldsProfile::$right = READ; + + $this->assertFalse(PluginDatainjectionModel::checkRightOnModel($models_id)); + } + + public function testFieldsValueIsInjectedWithWriteRightOnContainer(): void + { + $name = 'Test_Computer_fields_' . random_int(1, PHP_INT_MAX); + + $results = $this->injectComputerWithFieldValue($name, 'Injected value'); + + $this->assertSame(PluginDatainjectionCommonInjectionLib::SUCCESS, $results['status']); + $container_item = new PluginFieldsComputerstub(); + $this->assertTrue($container_item->getFromDBByCrit([ + 'itemtype' => Computer::class, + 'items_id' => $results[Computer::class], + ])); + $this->assertSame('Injected value', $container_item->fields[PluginFieldsComputerstub::VALUE_FIELD]); + } + + public function testFieldsValueIsRejectedWithReadRightOnContainer(): void + { + $name = 'Test_Computer_fields_' . random_int(1, PHP_INT_MAX); + + PluginFieldsProfile::$right = READ; + $results = $this->injectComputerWithFieldValue($name, 'Injected value'); + + $this->assertSame(PluginDatainjectionCommonInjectionLib::WARNING, $results['status']); + $this->assertSame(0, countElementsInTable( + PluginFieldsComputerstub::getTable(), + ['items_id' => $results[Computer::class]], + )); + } + + public function testFieldsValueIsUpdatedWithWriteRightOnContainer(): void + { + $name = 'Test_Computer_fields_' . random_int(1, PHP_INT_MAX); + $this->injectComputerWithFieldValue($name, 'Injected value'); + + $results = $this->injectComputerWithFieldValue($name, 'Updated value'); + + $this->assertSame(PluginDatainjectionCommonInjectionLib::IMPORT_UPDATE, $results['type']); + $this->assertSame(PluginDatainjectionCommonInjectionLib::SUCCESS, $results['status']); + $container_item = new PluginFieldsComputerstub(); + $this->assertTrue($container_item->getFromDBByCrit([ + 'itemtype' => Computer::class, + 'items_id' => $results[Computer::class], + ])); + $this->assertSame('Updated value', $container_item->fields[PluginFieldsComputerstub::VALUE_FIELD]); + } + + public function testFieldsValueUpdateIsRejectedWithReadRightOnContainer(): void + { + $name = 'Test_Computer_fields_' . random_int(1, PHP_INT_MAX); + $this->injectComputerWithFieldValue($name, 'Injected value'); + + PluginFieldsProfile::$right = READ; + $results = $this->injectComputerWithFieldValue($name, 'Updated value'); + + $this->assertSame(PluginDatainjectionCommonInjectionLib::IMPORT_UPDATE, $results['type']); + $this->assertSame(PluginDatainjectionCommonInjectionLib::WARNING, $results['status']); + $this->assertContains( + [PluginDatainjectionCommonInjectionLib::ERROR_CANNOT_UPDATE, PluginFieldsComputerstub::class], + $results[PluginDatainjectionCommonInjectionLib::ACTION_CHECK], + ); + $container_item = new PluginFieldsComputerstub(); + $this->assertTrue($container_item->getFromDBByCrit([ + 'itemtype' => Computer::class, + 'items_id' => $results[Computer::class], + ])); + $this->assertSame('Injected value', $container_item->fields[PluginFieldsComputerstub::VALUE_FIELD]); + } +}