From 7ceb4554e5598321a01b1ae9c3b72b874d964e99 Mon Sep 17 00:00:00 2001 From: Stanislas Kita <7335054+stonebuzz@users.noreply.github.com> Date: Fri, 2 Oct 2026 15:06:09 +0200 Subject: [PATCH 1/2] Fix: apply missing checks on block and field handling --- CHANGELOG.md | 14 + composer.json | 4 +- front/container.form.php | 10 +- front/field.form.php | 6 +- hook.php | 82 ----- inc/container.class.php | 204 ++++++++++- inc/field.class.php | 44 ++- inc/inventory.class.php | 128 ------- setup.php | 16 +- tests/FieldTestCase.php | 2 +- .../Units/ContainerGlpiItemReferenceTest.php | 177 +++++++++ tests/Units/ContainerItemtypesTest.php | 137 +++++++ tests/Units/ContainerReadonlyValuesTest.php | 336 ++++++++++++++++++ tests/Units/ExportBlockAsYamlTest.php | 128 +++++++ tests/bootstrap.php | 4 + 15 files changed, 1029 insertions(+), 263 deletions(-) delete mode 100644 inc/inventory.class.php create mode 100644 tests/Units/ContainerGlpiItemReferenceTest.php create mode 100644 tests/Units/ContainerItemtypesTest.php create mode 100644 tests/Units/ContainerReadonlyValuesTest.php create mode 100644 tests/Units/ExportBlockAsYamlTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 99722e8c..67c6720f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] + +### Fixed + +- Fix blocks export, block deletion and read-only fields not applying expected checks. +- Fix read-only status overrides being resolved with the previous status instead of the submitted one. +- Fix read-only fields of "Insertion in form" blocks being overwritable from the item form. +- Fix block's associated item types and type being changeable after creation. +- Remove obsolete FusionInventory integration. + +### Changed + +- "GLPI item" field now rejects a new reference to an item the current user cannot read, instead of silently clearing it. Automated writes (CLI, cron, inventory) are not affected. + ## [1.21.30] 2026-09-11 - Fix additional fields being saved on an item the user is not allowed to update. diff --git a/composer.json b/composer.json index 483e9407..ad1e9d65 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,12 @@ { "require": { - "php": ">=7.4", - "symfony/yaml": "^5.4" + "php": ">=7.4" }, "require-dev": { "glpi-project/tools": "^0.8" }, "config": { + "autoloader-suffix": "PluginFields", "optimize-autoloader": true, "platform": { "php": "7.4.0" diff --git a/front/container.form.php b/front/container.form.php index 92a8e314..f4c04992 100644 --- a/front/container.form.php +++ b/front/container.form.php @@ -45,12 +45,14 @@ $container->check($_POST['id'], DELETE); $ok = $container->delete($_POST); Html::redirect(PLUGINFIELDS_WEB_DIR . '/front/container.php'); -} elseif (isset($_REQUEST['purge'])) { - $container->check($_REQUEST['id'], PURGE); - $container->delete($_REQUEST, true); +} elseif (isset($_POST['purge'])) { + $container->check($_POST['id'], PURGE); + $container->delete($_POST, true); Html::redirect(PLUGINFIELDS_WEB_DIR . '/front/container.php'); } elseif (isset($_POST['update'])) { $container->check($_POST['id'], UPDATE); + // structural fields drive generated classes and tables; only migrations may change them + unset($_POST['itemtypes'], $_POST['type'], $_POST['subtype']); $container->update($_POST); Html::back(); } elseif (isset($_POST['update_fields_values'])) { @@ -66,7 +68,7 @@ Html::displayRightError(); } - $container->updateFieldsValues($_REQUEST, $_REQUEST['itemtype'], false); + $container->updateFieldsValues(PluginFieldsContainer::removeReadonlyValues($_REQUEST, $item), $_REQUEST['itemtype'], false); } Html::back(); } else { diff --git a/front/field.form.php b/front/field.form.php index da7323ea..54d7b3a5 100644 --- a/front/field.form.php +++ b/front/field.form.php @@ -46,9 +46,9 @@ $field->check($_POST['id'], DELETE); $field->delete($_POST); Html::back(); -} elseif (isset($_REQUEST['purge'])) { - $field->check($_REQUEST['id'], PURGE); - $field->delete($_REQUEST, true); +} elseif (isset($_POST['purge'])) { + $field->check($_POST['id'], PURGE); + $field->delete($_POST, true); $field->redirectToList(); } elseif (isset($_POST['update'])) { $field->check($_POST['id'], UPDATE); diff --git a/hook.php b/hook.php index 7f997ab4..28750fb3 100644 --- a/hook.php +++ b/hook.php @@ -231,88 +231,6 @@ function plugin_fields_MassiveActionsFieldsDisplay($options = []) return false; } - -/**** RULES ENGINE ****/ - -/** - * - * Actions for rules - * @since 0.84 - * @param array $params input data - * @return array an array of actions - */ -function plugin_fields_getRuleActions($params = []) -{ - $actions = []; - - switch ($params['rule_itemtype']) { - case 'PluginFusioninventoryTaskpostactionRule': - $options = PluginFieldsContainer::getAddSearchOptions('Computer'); - foreach ($options as $option) { - $actions[$option['linkfield']]['name'] = $option['name']; - $actions[$option['linkfield']]['type'] = $option['pfields_type']; - if ($option['pfields_type'] == 'dropdown') { - $actions[$option['linkfield']]['table'] = $option['table']; - } - } - - break; - } - - return $actions; -} - - -function plugin_fields_rule_matched($params = []) -{ - /** @var DBmysql $DB */ - global $DB; - - $container = new PluginFieldsContainer(); - - if (class_exists('PluginFusioninventoryAgent') && $params['sub_type'] == 'PluginFusioninventoryTaskpostactionRule') { - $agent = new PluginFusioninventoryAgent(); - - if (isset($params['input']['plugin_fusioninventory_agents_id'])) { - foreach ($params['output'] as $field => $value) { - // check if current field is in a tab container - $iterator = $DB->request([ - 'SELECT' => 'glpi_plugin_fields_containers.id', - 'FROM' => 'glpi_plugin_fields_containers', - 'LEFT JOIN' => [ - 'glpi_plugin_fields_fields' => [ - 'FKEY' => [ - 'glpi_plugin_fields_containers' => 'id', - 'glpi_plugin_fields_fields' => 'plugin_fields_containers_id', - ], - ], - ], - 'WHERE' => [ - 'glpi_plugin_fields_fields.name' => $field, - ], - ]); - if (count($iterator) > 0) { - $data = $iterator->current(); - - //retrieve computer - $agents_id = $params['input']['plugin_fusioninventory_agents_id']; - $agent->getFromDB($agents_id); - - // update current field - $container->updateFieldsValues( - [ - 'plugin_fields_containers_id' => $data['id'], - $field => $value, - 'items_id' => $agent->fields['computers_id'], - ], - Computer::getType(), - ); - } - } - } - } -} - function plugin_fields_giveItem($itemtype, $ID, $data, $num) { $searchopt = &Search::getOptions($itemtype); diff --git a/inc/container.class.php b/inc/container.class.php index b7540a86..8bdff01d 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -174,12 +174,12 @@ public static function installUserData(Migration $migration, $version) foreach ($itemtypes as $itemtype) { $sysname = self::getSystemName($itemtype, $container['name']); - $class_filename = $sysname . '.class.php'; + $class_filename = basename($sysname) . '.class.php'; if (file_exists(PLUGINFIELDS_DIR . "/inc/$class_filename")) { unlink(PLUGINFIELDS_DIR . "/inc/$class_filename"); } - $injclass_filename = $sysname . 'injection.class.php'; + $injclass_filename = basename($sysname) . 'injection.class.php'; if (file_exists(PLUGINFIELDS_DIR . "/inc/$injclass_filename")) { unlink(PLUGINFIELDS_DIR . "/inc/$injclass_filename"); } @@ -538,6 +538,35 @@ public function defineTabs($options = []) return $ong; } + public function prepareInputForUpdate($input) + { + if (isset($input['itemtypes'])) { + $itemtypes = is_array($input['itemtypes']) + ? $input['itemtypes'] + : PluginFieldsToolbox::decodeJSONItemtypes((string) $input['itemtypes']); + if (!is_array($itemtypes) || $itemtypes === [] || !$this->areValidItemtypeNames($itemtypes)) { + Session::AddMessageAfterRedirect(__('At least one selected object is not a valid element type', 'fields'), false, ERROR); + + return false; + } + + $input['itemtypes'] = json_encode(array_values($itemtypes)); + } + + return $input; + } + + private function areValidItemtypeNames(array $itemtypes): bool + { + foreach ($itemtypes as $itemtype) { + if (!is_string($itemtype) || preg_match('/^[A-Za-z_][A-Za-z0-9_\\\\]*$/', $itemtype) !== 1) { + return false; + } + } + + return true; + } + public function prepareInputForAdd($input) { if (!isset($input['itemtypes'])) { @@ -557,6 +586,12 @@ public function prepareInputForAdd($input) $input['itemtypes'] = [$input['itemtypes']]; } + if (!$this->areValidItemtypeNames($input['itemtypes'])) { + Session::AddMessageAfterRedirect(__('At least one selected object is not a valid element type', 'fields'), false, ERROR); + + return false; + } + if ($input['type'] === 'dom') { //check for already exist dom container with this itemtype $found = $this->find(['type' => 'dom']); @@ -741,8 +776,8 @@ public function pre_deleteItem() foreach (json_decode($this->fields['itemtypes']) as $itemtype) { $classname = self::getClassname($itemtype, $this->fields['name']); $sysname = self::getSystemName($itemtype, $this->fields['name']); - $class_filename = $sysname . '.class.php'; - $injection_filename = $sysname . 'injection.class.php'; + $class_filename = basename($sysname) . '.class.php'; + $injection_filename = basename($sysname) . 'injection.class.php'; //delete fields $field_obj = new PluginFieldsField(); @@ -1526,6 +1561,8 @@ public static function validateValues($data, $itemtype, $massiveaction) $valid = true; $empty_errors = []; $number_errors = []; + $url_errors = []; + $reference_errors = []; $container = new self(); $container->getFromDB($data['plugin_fields_containers_id']); @@ -1555,6 +1592,8 @@ public static function validateValues($data, $itemtype, $massiveaction) } } + $stored_values = self::getStoredValues($container, $itemtype, (int) ($data['items_id'] ?? 0)); + foreach ($fields as $field) { if (!$field['is_active']) { continue; @@ -1571,13 +1610,19 @@ public static function validateValues($data, $itemtype, $massiveaction) $itemtype_key = sprintf('itemtype_%s', $name); $items_id_key = sprintf('items_id_%s', $name); - if ( - isset($data[$itemtype_key], $data[$items_id_key]) - && is_a($data[$itemtype_key], CommonDBTM::class, true) - && $data[$items_id_key] > 0 - ) { - $glpi_item = new $data[$itemtype_key](); - $value = $glpi_item->getFromDB($data[$items_id_key]) ? $data[$items_id_key] : null; + $is_unchanged_reference = isset($stored_values[$itemtype_key], $stored_values[$items_id_key], $data[$itemtype_key], $data[$items_id_key]) + && $stored_values[$itemtype_key] === $data[$itemtype_key] + && (int) $stored_values[$items_id_key] === (int) $data[$items_id_key]; + if (!$is_unchanged_reference && isset($data[$items_id_key]) && (int) $data[$items_id_key] > 0) { + $value = self::isValidItemReference($field, $data[$itemtype_key] ?? null, (int) $data[$items_id_key]) + ? (int) $data[$items_id_key] + : null; + if ($value === null) { + $field['itemtype'] = PluginFieldsField::getType(); + $reference_errors[] = PluginFieldsLabelTranslation::getLabelFor($field); + $valid = false; + continue; + } } } elseif (isset($data[$name])) { $value = $data[$name]; @@ -1631,9 +1676,135 @@ public static function validateValues($data, $itemtype, $massiveaction) ' : ' . implode(', ', $url_errors), false, ERROR); } + if ($reference_errors !== []) { + Session::AddMessageAfterRedirect(__('Some item fields reference an invalid item', 'fields') + . ' : ' . implode(', ', $reference_errors), false, ERROR); + } + return $valid; } + /** + * Status of the item for this request: the submitted one when present, otherwise the persisted one. + */ + private static function getStatusValue(CommonDBTM $item): ?int + { + $status_field_name = PluginFieldsStatusOverride::getStatusFieldName($item->getType()); + foreach ([$item->input, $item->fields] as $source) { + if (array_key_exists($status_field_name, $source) && $source[$status_field_name] !== '') { + return (int) $source[$status_field_name]; + } + } + + return null; + } + + public static function removeReadonlyValues(array $data, CommonDBTM $item): array + { + $container_id = (int) $data['plugin_fields_containers_id']; + $fields = (new PluginFieldsField())->find([ + 'plugin_fields_containers_id' => $container_id, + 'is_active' => 1, + ]); + + $status_value = self::getStatusValue($item); + $status_overrides = $status_value !== null + ? PluginFieldsStatusOverride::getOverridesForItemtypeAndStatus($container_id, $item->getType(), $status_value) + : []; + foreach ($status_overrides as $status_override) { + if (isset($fields[$status_override['plugin_fields_fields_id']])) { + $fields[$status_override['plugin_fields_fields_id']]['is_readonly'] = $status_override['is_readonly']; + } + } + + $container = new self(); + $container->getFromDB($container_id); + + $stored_values = self::getStoredValues($container, $item->getType(), (int) $item->getID()); + + foreach ($fields as $field) { + if (!$field['is_readonly']) { + continue; + } + + $dropdown_key = sprintf('plugin_fields_%sdropdowns_id', $field['name']); + unset($data[sprintf('_%s_defined', $field['name'])], $data[sprintf('_%s_defined', $dropdown_key)]); + foreach ([ + $field['name'], + $dropdown_key, + sprintf('itemtype_%s', $field['name']), + sprintf('items_id_%s', $field['name']), + ] as $input_key) { + unset($data[$input_key]); + if (array_key_exists($input_key, $stored_values)) { + $data[$input_key] = $field['multiple'] + ? json_decode((string) $stored_values[$input_key], true) + : $stored_values[$input_key]; + } + } + + if ($item->isNewItem()) { + $data += self::getDefaultInput($field); + } + } + + return $data; + } + + private static function getDefaultInput(array $field): array + { + $default = PluginFieldsField::getDefaultValue($field); + if ($default === null) { + return []; + } + + $input_key = $field['type'] === 'dropdown' + ? sprintf('plugin_fields_%sdropdowns_id', $field['name']) + : $field['name']; + + if (!$field['multiple']) { + return [$input_key => $default]; + } + + $decoded = json_decode((string) $default, true); + + return is_array($decoded) && $decoded !== [] ? [$input_key => $decoded] : []; + } + + private static function getStoredValues(self $container, string $itemtype, int $items_id): array + { + if ($items_id <= 0 || $container->isNewItem()) { + return []; + } + + $values_obj = (new DbUtils())->getItemForItemtype(self::getClassname($itemtype, $container->fields['name'])); + if ($values_obj === false || !$values_obj->getFromDBByCrit(['items_id' => $items_id])) { + return []; + } + + return $values_obj->fields; + } + + private static function isValidItemReference(array $field, $itemtype, int $items_id): bool + { + $allowed_itemtypes = json_decode((string) $field['allowed_values'], true); + if ( + !is_string($itemtype) + || !is_a($itemtype, CommonDBTM::class, true) + || !is_array($allowed_itemtypes) + || !in_array($itemtype, $allowed_itemtypes, true) + ) { + return false; + } + + $item = new $itemtype(); + + // Automated writes (CLI, cron, inventory) run without a user whose rights could be checked + return Session::getLoginUserID() === false || Session::isInventory() + ? $item->getFromDB($items_id) + : $item->can($items_id, READ); + } + public static function findContainer($itemtype, $type = 'tab', $subtype = '') { $condition = [ @@ -1800,6 +1971,9 @@ public static function preItem(CommonDBTM $item) } if (false !== ($data = self::populateData($c_id, $item))) { + // read-only fields keep their stored value, or their default on creation + $data = self::removeReadonlyValues($data, $item); + if (self::validateValues($data, $item::getType(), isset($_REQUEST['massiveaction'])) === false) { $item->input = []; @@ -1842,13 +2016,7 @@ private static function populateData($c_id, CommonDBTM $item) } // Add status so it can be used with status overrides - $status_field_name = PluginFieldsStatusOverride::getStatusFieldName($item->getType()); - $data[$status_field_name] = null; - if (array_key_exists($status_field_name, $item->input) && $item->input[$status_field_name] !== '') { - $data[$status_field_name] = (int) $item->input[$status_field_name]; - } elseif (array_key_exists($status_field_name, $item->fields) && $item->fields[$status_field_name] !== '') { - $data[$status_field_name] = (int) $item->fields[$status_field_name]; - } + $data[PluginFieldsStatusOverride::getStatusFieldName($item->getType())] = self::getStatusValue($item); $has_fields = false; foreach ($fields as $field) { diff --git a/inc/field.class.php b/inc/field.class.php index 659ae463..beca1b7b 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -998,6 +998,9 @@ public static function showForTab($params) //JS to trigger any change and check if container need to be display or not $ajax_url = Plugin::getWebDir('fields') . '/ajax/container.php'; $items_id = !$item->isNewItem() ? $item->getID() : 0; + $js_itemtype = json_encode($item::getType(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); + $js_type = json_encode($type, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); + $js_subtype = json_encode($subtype, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); echo Html::scriptBlock( <<. - * ------------------------------------------------------------------------- - * @copyright Copyright (C) 2013-2023 by Fields plugin team. - * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html - * @link https://github.com/pluginsGLPI/fields - * ------------------------------------------------------------------------- - */ - -class PluginFieldsInventory extends CommonDBTM -{ - public static function updateInventory($params = []) - { - if ( - !empty($params) - && isset($params['inventory_data']) && !empty($params['inventory_data']) - ) { - $availaibleItemType = ['Computer', 'Printer', 'NetworkEquipment']; - foreach (array_keys($params['inventory_data']) as $itemtype) { - if (in_array($itemtype, $availaibleItemType)) { - $items_id = 0; - //retrieve items id switch itemtype - switch ($itemtype) { - case Computer::getType(): - $items_id = $params['computers_id']; - break; - - case NetworkEquipment::getType(): - $items_id = $params['networkequipments_id']; - break; - - case Printer::getType(): - $items_id = $params['printers_id']; - break; - } - - if (class_exists('PluginFusioninventoryInventoryComputerComputer')) { - if ($itemtype == Computer::getType()) { - //load inventory from DB because - //FI not update XML file if computer is not update - $db_info = new PluginFusioninventoryInventoryComputerComputer(); - if ($db_info->getFromDBByCrit(['computers_id' => $items_id])) { - $arrayinventory = unserialize(gzuncompress($db_info->fields['serialized_inventory'])); - if (isset($arrayinventory['custom'])) { - self::updateFields($arrayinventory['custom']['container'], $itemtype, $items_id); - } - } - } else { - //Load XML file because FI always update XML file and don't store inventory into DB - $file = self::loadXMLFile($itemtype, $items_id); - if ( - $file !== false - && class_exists('PluginFusioninventoryFormatconvert') - ) { - $arrayinventory = PluginFusioninventoryFormatconvert::XMLtoArray($file); - if (isset($arrayinventory['CUSTOM'])) { - self::updateFields($arrayinventory['CUSTOM']['CONTAINER'], $itemtype, $items_id); - } - } - } - } - } - } - } - } - - public static function updateFields($containersData, $itemtype, $items_id) - { - if (isset($containersData['ID'])) { - // $containersData contains only one element, encapsulate it into an array - $containersData = [$containersData]; - } - foreach ($containersData as $key => $containerData) { - $container = new PluginFieldsContainer(); - $container->getFromDB($containerData['ID']); - $data = []; - $data['items_id'] = $items_id; - $data['itemtype'] = $itemtype; - $data['plugin_fields_containers_id'] = $containerData['ID']; - foreach ($containerData['FIELDS'] as $key => $value) { - $data[strtolower($key)] = $value; - } - $container->updateFieldsValues($data, $itemtype, false); - } - } - - public static function loadXMLFile($itemtype, $items_id) - { - $pxml = false; - $folder = substr($items_id, 0, -1); - if (empty($folder)) { - $folder = '0'; - } - - //Check if the file exists with the .xml extension (new format) - /** @phpstan-ignore-next-line */ - $file = PLUGIN_FUSIONINVENTORY_XML_DIR . strtolower($itemtype) . '/' . $folder . '/' . $items_id; - if (file_exists($file . '.xml')) { - $file .= '.xml'; - } elseif (!file_exists($file)) { - return false; - } - $pxml = simplexml_load_file($file, 'SimpleXMLElement', LIBXML_NOCDATA); - - return $pxml; - } -} diff --git a/setup.php b/setup.php index 4f5a8ea9..887cca5e 100644 --- a/setup.php +++ b/setup.php @@ -107,15 +107,6 @@ function plugin_init_fields() } } - if (Plugin::isPluginActive('fusioninventory')) { - $PLUGIN_HOOKS['fusioninventory_inventory']['fields'] - = ['PluginFieldsInventory', 'updateInventory']; - } - - // complete rule engine - $PLUGIN_HOOKS['use_rules']['fields'] = ['PluginFusioninventoryTaskpostactionRule']; - $PLUGIN_HOOKS['rule_matched']['fields'] = 'plugin_fields_rule_matched'; - if (isset($_SESSION['glpiactiveentities'])) { // add link in plugin page $PLUGIN_HOOKS['config_page']['fields'] = 'front/container.php'; @@ -312,6 +303,13 @@ function plugin_fields_exportBlockAsYaml($container_id = null) $containers = $container_obj->find($where); foreach ($containers as $container) { + if ( + !Session::haveAccessToEntity((int) $container['entities_id'], (bool) $container['is_recursive']) + || PluginFieldsProfile::getRightOnContainer((int) ($_SESSION['glpiactiveprofile']['id'] ?? 0), (int) $container['id']) < READ + ) { + continue; + } + $itemtypes = (strlen($container['itemtypes']) > 0) ? json_decode($container['itemtypes'], true) : []; diff --git a/tests/FieldTestCase.php b/tests/FieldTestCase.php index af81756c..ac248dee 100644 --- a/tests/FieldTestCase.php +++ b/tests/FieldTestCase.php @@ -87,7 +87,7 @@ public function createField(array $inputs): PluginFieldsField // Re-login to ensure we are logged in $this->login(); - $field = $this->createItem(PluginFieldsField::class, $inputs, ['allowed_values']); + $field = $this->createItem(PluginFieldsField::class, $inputs, ['allowed_values', 'default_value']); self::$createdFields[] = $field; // Re-initialize fields plugin to register new field logic diff --git a/tests/Units/ContainerGlpiItemReferenceTest.php b/tests/Units/ContainerGlpiItemReferenceTest.php new file mode 100644 index 00000000..8b451ba8 --- /dev/null +++ b/tests/Units/ContainerGlpiItemReferenceTest.php @@ -0,0 +1,177 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +declare(strict_types=1); + +namespace GlpiPlugin\Field\Tests\Units; + +use Computer; +use Entity; +use DbTestCase; +use GLPITestCase; +use GlpiPlugin\Field\Tests\FieldTestTrait; +use Location; +use PluginFieldsContainer; + +require_once __DIR__ . '/../FieldTestCase.php'; + +final class ContainerGlpiItemReferenceTest extends DbTestCase +{ + use FieldTestTrait; + + private PluginFieldsContainer $container; + + private string $field_name; + + private Computer $holder; + + private Computer $root_target; + + private Computer $child_target; + + private int $root_entity_id; + + public function setUp(): void + { + GLPITestCase::setUp(); + $this->login(); + + $this->root_entity_id = 0; + $child_entity_id = $this->createItem(Entity::class, [ + 'name' => 'Entity ' . $this->getUniqueString(), + 'entities_id' => 0, + ])->getID(); + $this->setEntity($this->root_entity_id, true); + + $this->container = $this->createFieldContainer([ + 'label' => 'Item reference', + 'type' => 'tab', + 'itemtypes' => [Computer::class], + 'is_active' => 1, + 'entities_id' => $this->root_entity_id, + 'is_recursive' => 1, + ]); + $field = $this->createField([ + 'label' => 'Linked computer', + 'type' => 'glpi_item', + PluginFieldsContainer::getForeignKeyField() => $this->container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + 'allowed_values' => [Computer::class], + ]); + $this->field_name = $field->fields['name']; + + $this->holder = $this->createComputer($this->root_entity_id); + $this->root_target = $this->createComputer($this->root_entity_id); + $this->child_target = $this->createComputer($child_entity_id); + } + + public function tearDown(): void + { + $this->tearDownFieldTest(); + GLPITestCase::tearDown(); + } + + public function testValidReferenceIsAccepted(): void + { + $this->assertTrue($this->validate(Computer::class, $this->root_target->getID())); + } + + public function testUnknownItemIsRejected(): void + { + $this->assertFalse($this->validate(Computer::class, 999999)); + $this->assertReferenceError(); + } + + public function testItemtypeOutsideAllowedValuesIsRejected(): void + { + $location = $this->createItem(Location::class, [ + 'name' => 'Location ' . $this->getUniqueString(), + 'entities_id' => $this->root_entity_id, + ]); + + $this->assertFalse($this->validate(Location::class, $location->getID())); + $this->assertReferenceError(); + } + + public function testReferenceToItemWithoutReadRightIsRejected(): void + { + $this->setEntity($this->root_entity_id, false); + + $this->assertFalse($this->validate(Computer::class, $this->child_target->getID())); + $this->assertReferenceError(); + } + + public function testUnchangedReferenceStaysAcceptedWithoutReadRight(): void + { + $this->assertTrue($this->container->updateFieldsValues( + $this->buildData(Computer::class, $this->child_target->getID()), + Computer::class, + false, + )); + + $this->setEntity($this->root_entity_id, false); + + $this->assertTrue($this->validate(Computer::class, $this->child_target->getID())); + } + + private function validate(string $itemtype, int $items_id): bool + { + return PluginFieldsContainer::validateValues($this->buildData($itemtype, $items_id), Computer::class, false); + } + + private function buildData(string $itemtype, int $items_id): array + { + return [ + 'plugin_fields_containers_id' => $this->container->getID(), + 'itemtype' => Computer::class, + 'items_id' => $this->holder->getID(), + 'itemtype_' . $this->field_name => $itemtype, + 'items_id_' . $this->field_name => $items_id, + ]; + } + + private function assertReferenceError(): void + { + $this->hasSessionMessages(ERROR, ['Some item fields reference an invalid item : Linked computer']); + } + + private function createComputer(int $entities_id): Computer + { + $computer = $this->createItem(Computer::class, [ + 'name' => 'Computer ' . $this->getUniqueString(), + 'entities_id' => $entities_id, + ]); + $this->assertInstanceOf(Computer::class, $computer); + + return $computer; + } +} diff --git a/tests/Units/ContainerItemtypesTest.php b/tests/Units/ContainerItemtypesTest.php new file mode 100644 index 00000000..d9c44a50 --- /dev/null +++ b/tests/Units/ContainerItemtypesTest.php @@ -0,0 +1,137 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + + +declare(strict_types=1); + +namespace GlpiPlugin\Field\Tests\Units; + +use Computer; +use DbTestCase; +use GLPITestCase; +use GlpiPlugin\Field\Tests\FieldTestTrait; +use Monitor; +use PluginFieldsContainer; +use PluginFieldsToolbox; + +require_once __DIR__ . '/../FieldTestCase.php'; + +final class ContainerItemtypesTest extends DbTestCase +{ + use FieldTestTrait; + + public function setUp(): void + { + GLPITestCase::setUp(); + $this->login(); + } + + public function tearDown(): void + { + $this->tearDownFieldTest(); + GLPITestCase::tearDown(); + } + + public static function provideMalformedItemtypes(): iterable + { + yield 'path separator' => ['itemtype' => '../Computer']; + yield 'leading digit' => ['itemtype' => '1Computer']; + yield 'whitespace' => ['itemtype' => 'Computer Model']; + } + + /** + * @dataProvider provideMalformedItemtypes + */ + public function testAddWithMalformedItemtypeIsRejected(string $itemtype): void + { + $container = new PluginFieldsContainer(); + $result = $container->add([ + 'label' => 'Malformed itemtype', + 'type' => 'tab', + 'itemtypes' => [$itemtype], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $this->assertFalse($result); + $this->hasSessionMessages(ERROR, ['At least one selected object is not a valid element type']); + } + + /** + * @dataProvider provideMalformedItemtypes + */ + public function testUpdateWithMalformedItemtypeIsRejected(string $itemtype): void + { + $container = $this->createFieldContainer([ + 'label' => 'Upd malformed', + 'type' => 'tab', + 'itemtypes' => [Computer::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + $original_itemtypes = $container->fields['itemtypes']; + + $result = $container->update([ + 'id' => $container->getID(), + 'itemtypes' => json_encode([$itemtype]), + ]); + + $this->assertFalse($result); + $this->hasSessionMessages(ERROR, ['At least one selected object is not a valid element type']); + $container->getFromDB($container->getID()); + $this->assertSame($original_itemtypes, $container->fields['itemtypes']); + } + + public function testUpdateWithValidItemtypesReencodesThem(): void + { + $container = $this->createFieldContainer([ + 'label' => 'Upd valid', + 'type' => 'tab', + 'itemtypes' => [Computer::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $result = $container->update([ + 'id' => $container->getID(), + 'itemtypes' => json_encode([Computer::class, Monitor::class]), + ]); + + $this->assertTrue($result); + $container->getFromDB($container->getID()); + $this->assertSame( + [Computer::class, Monitor::class], + PluginFieldsToolbox::decodeJSONItemtypes($container->fields['itemtypes']), + ); + } +} diff --git a/tests/Units/ContainerReadonlyValuesTest.php b/tests/Units/ContainerReadonlyValuesTest.php new file mode 100644 index 00000000..94491f1d --- /dev/null +++ b/tests/Units/ContainerReadonlyValuesTest.php @@ -0,0 +1,336 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +declare(strict_types=1); + +namespace GlpiPlugin\Field\Tests\Units; + +use Computer; +use Entity; +use DbTestCase; +use GLPITestCase; +use GlpiPlugin\Field\Tests\FieldTestTrait; +use PluginFieldsContainer; +use PluginFieldsDropdown; +use PluginFieldsField; +use PluginFieldsStatusOverride; +use State; + +require_once __DIR__ . '/../FieldTestCase.php'; + +final class ContainerReadonlyValuesTest extends DbTestCase +{ + use FieldTestTrait; + + private int $entity_id; + + public function setUp(): void + { + GLPITestCase::setUp(); + $this->login(); + $this->entity_id = 0; + $this->setEntity($this->entity_id, true); + } + + public function tearDown(): void + { + $this->tearDownFieldTest(); + GLPITestCase::tearDown(); + } + + public function testReadonlyValueIsReplacedByStoredValue(): void + { + $container = $this->createContainer('tab'); + $readonly_name = $this->createTextField($container, 'Locked', 1); + $editable_name = $this->createTextField($container, 'Editable', 0); + $computer = $this->createComputer(); + + $this->storeValues($container, $computer, [$readonly_name => 'stored']); + + $data = PluginFieldsContainer::removeReadonlyValues( + $this->buildData($container, $computer, [$readonly_name => 'hacked', $editable_name => 'new']), + $computer, + ); + + $this->assertSame('stored', $data[$readonly_name]); + $this->assertSame('new', $data[$editable_name]); + } + + public function testReadonlyValueWithoutStoredRowIsDropped(): void + { + $container = $this->createContainer('tab'); + $readonly_name = $this->createTextField($container, 'Locked', 1); + $editable_name = $this->createTextField($container, 'Editable', 0); + $computer = $this->createComputer(); + + $data = PluginFieldsContainer::removeReadonlyValues( + $this->buildData($container, $computer, [ + $readonly_name => 'hacked', + '_' . $readonly_name . '_defined' => 1, + $editable_name => 'new', + ]), + $computer, + ); + + $this->assertArrayNotHasKey($readonly_name, $data); + $this->assertArrayNotHasKey('_' . $readonly_name . '_defined', $data); + $this->assertSame('new', $data[$editable_name]); + } + + public function testStatusOverrideTogglesReadonly(): void + { + $container = $this->createContainer('dom'); + $field_name = $this->createTextField($container, 'Locked when stocked', 0); + $state = $this->createItem(State::class, ['name' => 'State ' . $this->getUniqueString(), 'entities_id' => $this->entity_id]); + $computer = $this->createComputer(); + + $this->createItem(PluginFieldsStatusOverride::class, [ + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'plugin_fields_fields_id' => $this->fieldIdByName($field_name), + 'itemtype' => Computer::class, + 'states' => [$state->getID()], + 'is_readonly' => 1, + 'mandatory' => 0, + ], ['states', PluginFieldsContainer::getForeignKeyField()]); + $this->storeValues($container, $computer, [$field_name => 'stored']); + + $data = PluginFieldsContainer::removeReadonlyValues( + $this->buildData($container, $computer, [$field_name => 'submitted']), + $computer, + ); + $this->assertSame('submitted', $data[$field_name]); + + $this->updateItem(Computer::class, $computer->getID(), ['states_id' => $state->getID()]); + $this->assertTrue($computer->getFromDB($computer->getID())); + + $data = PluginFieldsContainer::removeReadonlyValues( + $this->buildData($container, $computer, [$field_name => 'submitted']), + $computer, + ); + $this->assertSame('stored', $data[$field_name]); + } + + public function testStatusOverrideUsesSubmittedStatusOnUpdate(): void + { + $container = $this->createContainer('dom'); + $field_name = $this->createTextField($container, 'Locked when stocked', 0); + $state = $this->createItem(State::class, ['name' => 'State ' . $this->getUniqueString(), 'entities_id' => $this->entity_id]); + $computer = $this->createComputer(); + $this->createReadonlyOverride($container, $field_name, $state); + $this->storeValues($container, $computer, [$field_name => 'stored']); + + $computer->input = ['states_id' => $state->getID()]; + $data = PluginFieldsContainer::removeReadonlyValues( + $this->buildData($container, $computer, [$field_name => 'submitted']), + $computer, + ); + $this->assertSame('stored', $data[$field_name]); + + $this->updateItem(Computer::class, $computer->getID(), ['states_id' => $state->getID()]); + $this->assertTrue($computer->getFromDB($computer->getID())); + $computer->input = ['states_id' => 0]; + + $data = PluginFieldsContainer::removeReadonlyValues( + $this->buildData($container, $computer, [$field_name => 'submitted']), + $computer, + ); + $this->assertSame('submitted', $data[$field_name]); + } + + public function testStatusOverrideAppliesOnItemCreation(): void + { + $container = $this->createContainer('dom'); + $field_name = $this->createTextField($container, 'Locked when stocked', 0, 'default'); + $state = $this->createItem(State::class, ['name' => 'State ' . $this->getUniqueString(), 'entities_id' => $this->entity_id]); + $this->createReadonlyOverride($container, $field_name, $state); + + $computer = new Computer(); + $computer->input = ['states_id' => $state->getID()]; + + $data = PluginFieldsContainer::removeReadonlyValues( + [$field_name => 'hacked', 'plugin_fields_containers_id' => $container->getID(), 'itemtype' => Computer::class, 'items_id' => 0], + $computer, + ); + $this->assertSame('default', $data[$field_name]); + } + + public function testReadonlyMultipleDropdownIsRestoredAsArray(): void + { + $container = $this->createContainer('tab'); + $field = $this->createField([ + 'label' => 'Tags', + 'type' => 'dropdown', + 'multiple' => 1, + 'default_value' => [], + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 1, + ], ['default_value']); + $dropdown_key = sprintf('plugin_fields_%sdropdowns_id', $field->fields['name']); + $dropdown_class = PluginFieldsDropdown::getClassname($field->fields['name']); + $stored_ids = [ + $this->createItem($dropdown_class, ['name' => 'A'])->getID(), + $this->createItem($dropdown_class, ['name' => 'B'])->getID(), + ]; + $other_id = $this->createItem($dropdown_class, ['name' => 'C'])->getID(); + $computer = $this->createComputer(); + + $this->storeValues($container, $computer, [$dropdown_key => $stored_ids]); + + $data = PluginFieldsContainer::removeReadonlyValues( + $this->buildData($container, $computer, [$dropdown_key => [$other_id], '_' . $dropdown_key . '_defined' => 1]), + $computer, + ); + + $this->assertSame($stored_ids, $data[$dropdown_key]); + $this->assertArrayNotHasKey('_' . $dropdown_key . '_defined', $data); + } + + public function testDomBlockReadonlyFieldCannotBeOverwrittenFromItemForm(): void + { + $container = $this->createContainer('dom'); + $readonly_name = $this->createTextField($container, 'Locked', 1); + $editable_name = $this->createTextField($container, 'Editable', 0); + $computer = $this->createComputer(); + + $this->storeValues($container, $computer, [$readonly_name => 'stored', $editable_name => 'old']); + + $this->updateItem(Computer::class, $computer->getID(), [ + 'name' => 'Renamed', + $readonly_name => 'hacked', + $editable_name => 'new', + ], [$readonly_name, $editable_name]); + + $values = $this->getStoredValues($container, $computer); + $this->assertSame('stored', $values[$readonly_name]); + $this->assertSame('new', $values[$editable_name]); + } + + public function testDomBlockReadonlyFieldTakesItsDefaultOnItemCreation(): void + { + $container = $this->createContainer('dom'); + $readonly_name = $this->createTextField($container, 'Locked', 1, 'locked default'); + $editable_name = $this->createTextField($container, 'Editable', 0); + + $computer = $this->createItem(Computer::class, [ + 'name' => 'Computer ' . $this->getUniqueString(), + 'entities_id' => $this->entity_id, + $readonly_name => 'hacked', + $editable_name => 'new', + ], [$readonly_name, $editable_name]); + $this->assertInstanceOf(Computer::class, $computer); + + $values = $this->getStoredValues($container, $computer); + $this->assertSame('locked default', $values[$readonly_name]); + $this->assertSame('new', $values[$editable_name]); + } + + private function createContainer(string $type): PluginFieldsContainer + { + return $this->createFieldContainer([ + 'label' => 'Readonly ' . $type, + 'type' => $type, + 'itemtypes' => [Computer::class], + 'is_active' => 1, + 'entities_id' => $this->entity_id, + 'is_recursive' => 1, + ]); + } + + private function createTextField(PluginFieldsContainer $container, string $label, int $is_readonly, string $default_value = ''): string + { + $field = $this->createField([ + 'label' => $label, + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => $is_readonly, + 'default_value' => $default_value, + ]); + + return $field->fields['name']; + } + + private function createReadonlyOverride(PluginFieldsContainer $container, string $field_name, State $state): void + { + $this->createItem(PluginFieldsStatusOverride::class, [ + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'plugin_fields_fields_id' => $this->fieldIdByName($field_name), + 'itemtype' => Computer::class, + 'states' => [$state->getID()], + 'is_readonly' => 1, + 'mandatory' => 0, + ], ['states', PluginFieldsContainer::getForeignKeyField()]); + } + + private function fieldIdByName(string $name): int + { + $field = new PluginFieldsField(); + $this->assertTrue($field->getFromDBByCrit(['name' => $name])); + + return $field->getID(); + } + + private function createComputer(): Computer + { + $computer = $this->createItem(Computer::class, [ + 'name' => 'Computer ' . $this->getUniqueString(), + 'entities_id' => $this->entity_id, + ]); + $this->assertInstanceOf(Computer::class, $computer); + + return $computer; + } + + private function buildData(PluginFieldsContainer $container, Computer $computer, array $values): array + { + return $values + [ + 'plugin_fields_containers_id' => $container->getID(), + 'itemtype' => Computer::class, + 'items_id' => $computer->getID(), + ]; + } + + private function storeValues(PluginFieldsContainer $container, Computer $computer, array $values): void + { + $this->assertTrue($container->updateFieldsValues($this->buildData($container, $computer, $values), Computer::class, false)); + } + + private function getStoredValues(PluginFieldsContainer $container, Computer $computer): array + { + $classname = PluginFieldsContainer::getClassname(Computer::class, $container->fields['name']); + $values_obj = new $classname(); + $this->assertTrue($values_obj->getFromDBByCrit(['items_id' => $computer->getID()])); + + return $values_obj->fields; + } +} diff --git a/tests/Units/ExportBlockAsYamlTest.php b/tests/Units/ExportBlockAsYamlTest.php new file mode 100644 index 00000000..2e1dd070 --- /dev/null +++ b/tests/Units/ExportBlockAsYamlTest.php @@ -0,0 +1,128 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +declare(strict_types=1); + +namespace GlpiPlugin\Field\Tests\Units; + +use Computer; +use Entity; +use DbTestCase; +use GLPITestCase; +use GlpiPlugin\Field\Tests\FieldTestTrait; +use PluginFieldsContainer; +use PluginFieldsProfile; + +require_once __DIR__ . '/../FieldTestCase.php'; + +final class ExportBlockAsYamlTest extends DbTestCase +{ + use FieldTestTrait; + + private int $root_entity_id; + + private int $child_entity_id; + + public function setUp(): void + { + GLPITestCase::setUp(); + $this->login(); + $this->root_entity_id = 0; + $this->child_entity_id = $this->createItem(Entity::class, [ + 'name' => 'Entity ' . $this->getUniqueString(), + 'entities_id' => 0, + ])->getID(); + $this->setEntity($this->root_entity_id, true); + } + + public function tearDown(): void + { + $this->tearDownFieldTest(); + GLPITestCase::tearDown(); + } + + public function testVisibleBlockIsExported(): void + { + $container = $this->createContainer($this->root_entity_id); + + $this->assertTrue(plugin_fields_exportBlockAsYaml($container->getID())); + $this->assertStringContainsString( + $container->getID() . '-' . Computer::class, + (string) file_get_contents(GLPI_TMP_DIR . '/fields_conf.yaml'), + ); + } + + public function testBlockWithoutProfileAccessIsOmitted(): void + { + $container = $this->createContainer($this->root_entity_id); + + $profile_right = new PluginFieldsProfile(); + $this->assertTrue($profile_right->getFromDBByCrit([ + 'profiles_id' => $_SESSION['glpiactiveprofile']['id'], + 'plugin_fields_containers_id' => $container->getID(), + ])); + $this->updateItem(PluginFieldsProfile::class, $profile_right->getID(), ['right' => 0]); + + $this->assertFalse(plugin_fields_exportBlockAsYaml($container->getID())); + } + + public function testBlockOutsideActiveEntitiesIsOmitted(): void + { + $container = $this->createContainer($this->child_entity_id); + + $this->setEntity($this->root_entity_id, false); + $this->assertFalse(plugin_fields_exportBlockAsYaml($container->getID())); + + $this->setEntity($this->child_entity_id, false); + $this->assertTrue(plugin_fields_exportBlockAsYaml($container->getID())); + } + + private function createContainer(int $entities_id): PluginFieldsContainer + { + $container = $this->createFieldContainer([ + 'label' => 'Export', + 'type' => 'tab', + 'itemtypes' => [Computer::class], + 'is_active' => 1, + 'entities_id' => $entities_id, + 'is_recursive' => 0, + ]); + $this->createField([ + 'label' => 'Exported text', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + return $container; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 5ebb18d7..c397b3a0 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -39,6 +39,10 @@ define('TU_PASS', 'glpi'); define('GLPI_LOG_LVL', 'DEBUG'); +if (!is_dir(GLPI_LOG_DIR)) { + mkdir(GLPI_LOG_DIR, 0755, true); +} + require GLPI_ROOT . '/inc/includes.php'; Plugin::load('fields', true); From 5cb639cd3ada09597abc86802dbacab536edf35c Mon Sep 17 00:00:00 2001 From: Stanislas Kita <7335054+stonebuzz@users.noreply.github.com> Date: Fri, 2 Oct 2026 15:57:35 +0200 Subject: [PATCH 2/2] fix --- composer.lock | 456 +++++++++++++++++++++----------------------------- 1 file changed, 190 insertions(+), 266 deletions(-) diff --git a/composer.lock b/composer.lock index eb35804e..7efcf726 100644 --- a/composer.lock +++ b/composer.lock @@ -4,247 +4,21 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8fd001d971e6381c8d6d7f4af8e2913c", - "packages": [ - { - "name": "symfony/deprecation-contracts", - "version": "v2.5.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", - "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "2.5-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/v2.5.4" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:11:13+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/yaml", - "version": "v5.4.45", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "a454d47278cc16a5db371fe73ae66a78a633371e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/a454d47278cc16a5db371fe73ae66a78a633371e", - "reference": "a454d47278cc16a5db371fe73ae66a78a633371e", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/console": "<5.3" - }, - "require-dev": { - "symfony/console": "^5.3|^6.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "bin": [ - "Resources/bin/yaml-lint" - ], - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "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": "Loads and dumps YAML files", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.45" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:11:13+00:00" - } - ], + "content-hash": "ce5f749951bc7e46686362673da6c64f", + "packages": [], "packages-dev": [ { "name": "glpi-project/tools", - "version": "0.8.3", + "version": "0.8.4", "source": { "type": "git", "url": "https://github.com/glpi-project/tools.git", - "reference": "8ea2a7d4702a858f4b0360ba7d4f1841a5e77026" + "reference": "2791387652f570c0f676a372ad74c4dade65c937" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/glpi-project/tools/zipball/8ea2a7d4702a858f4b0360ba7d4f1841a5e77026", - "reference": "8ea2a7d4702a858f4b0360ba7d4f1841a5e77026", + "url": "https://api.github.com/repos/glpi-project/tools/zipball/2791387652f570c0f676a372ad74c4dade65c937", + "reference": "2791387652f570c0f676a372ad74c4dade65c937", "shasum": "" }, "require": { @@ -287,7 +61,7 @@ "issues": "https://github.com/glpi-project/tools/issues", "source": "https://github.com/glpi-project/tools" }, - "time": "2025-10-14T10:26:06+00:00" + "time": "2026-09-09T13:56:33+00:00" }, { "name": "psr/container", @@ -436,18 +210,168 @@ ], "time": "2024-11-06T11:30:55+00:00" }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-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/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+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.33.0", + "version": "v1.43.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", "shasum": "" }, "require": { @@ -496,7 +420,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.0" }, "funding": [ { @@ -516,20 +440,20 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-09-25T15:46:47+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.43.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", "shasum": "" }, "require": { @@ -581,7 +505,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.0" }, "funding": [ { @@ -601,20 +525,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-09-23T22:02:09+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.43.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", "shasum": "" }, "require": { @@ -666,7 +590,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0" }, "funding": [ { @@ -686,11 +610,11 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-09-24T15:19:04+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -746,7 +670,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.37.0" }, "funding": [ { @@ -770,16 +694,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.33.0", + "version": "v1.43.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", "shasum": "" }, "require": { @@ -830,7 +754,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.43.0" }, "funding": [ { @@ -850,20 +774,20 @@ "type": "tidelift" } ], - "time": "2025-01-02T08:10:11+00:00" + "time": "2026-09-25T15:50:05+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", "shasum": "" }, "require": { @@ -910,7 +834,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.38.1" }, "funding": [ { @@ -930,7 +854,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T12:45:58+00:00" }, { "name": "symfony/service-contracts", @@ -1194,5 +1118,5 @@ "platform-overrides": { "php": "7.4.0" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" }