From a76d6cb12507c4734f06ec00e27b3aabe6994e91 Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 29 Sep 2026 12:20:17 +0200 Subject: [PATCH 1/6] Fix ticket observer can edit --- inc/container.class.php | 11 +++++++++++ inc/field.class.php | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index 8221cd1e..2cbfd1a1 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1909,6 +1909,17 @@ public static function preItemUpdate(CommonDBTM $item) { self::preItem($item); if (array_key_exists('_plugin_fields_data', $item->input)) { + // Only save plugin fields if the user can update this specific item. + // Automated contexts (cron jobs, API without active profile) bypass this check. + if ( + isset($_SESSION['glpiactiveprofile']['id']) + && $_SESSION['glpiactiveprofile']['id'] !== null + && !$item->canUpdateItem() + ) { + unset($item->input['_plugin_fields_data']); + return true; + } + $data = $item->input['_plugin_fields_data']; $data['itemtype'] = $item::class; $data['entities_id'] = $item->isEntityAssign() ? $item->getEntityID() : 0; diff --git a/inc/field.class.php b/inc/field.class.php index 9a89e0d3..7eb741a8 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -891,7 +891,7 @@ public static function showForTabContainer($c_id, $item) return null; } - $canedit = $right > READ; + $canedit = $right > READ && ($item->isNewItem() || $item->canUpdateItem()); //get fields for this container $field_obj = new self(); @@ -1210,7 +1210,7 @@ public static function prepareHtmlFields( return null; } - $canedit = $right > READ; + $canedit = $right > READ && ($item->isNewItem() || $item->canUpdateItem()); // Fill status overrides if needed if (in_array($item->getType(), PluginFieldsStatusOverride::getStatusItemtypes())) { From df5feb400f4c6a7c1ec5acd54ba52b8e8b82c656 Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 30 Sep 2026 09:35:29 +0200 Subject: [PATCH 2/6] fix CI --- inc/container.class.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index 2cbfd1a1..a90e02d2 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1913,8 +1913,10 @@ public static function preItemUpdate(CommonDBTM $item) // Automated contexts (cron jobs, API without active profile) bypass this check. if ( isset($_SESSION['glpiactiveprofile']['id']) - && $_SESSION['glpiactiveprofile']['id'] !== null - && !$item->canUpdateItem() + && $_SESSION['glpiactiveprofile']['id'] != null + && $item instanceof CommonITILObject + && Session::getCurrentInterface() === 'helpdesk' + && !$item->canRequesterUpdateItem() ) { unset($item->input['_plugin_fields_data']); return true; From 4b571e57c51250048e2bd638c063a0a21a838a3a Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 30 Sep 2026 09:50:52 +0200 Subject: [PATCH 3/6] changelog --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ecf7d67..0e7924f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,15 +13,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed -- Fix display width for rich text fields - Fix massive action update on CustomAssets - Fix mandatory fields on a Tab block not being enforced when updating an item. - Fix administrators losing access to a block's configuration after setting a profile to "no access" on that block. - Fix dependency conflict with GLPI core by no longer vendoring symfony/deprecation-contracts and symfony/polyfill-ctype. - Fix default field values not being applied when fields are empty on creation -- Fix a field's default value not being applied to existing items and not being shown in search results for items with no dedicated row in the container table - Fix mandatory fields blocking automated item creation -- Fix unclear mandatory field error when a GLPI form creating a ticket does not provide the field. +- Fix ticket observers being able to edit and save additional fields they are not allowed to modify ## [1.24.5] - 2026-09-11 From 83e4dc20ff1348f0efa47301c4afc9d61840d017 Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 30 Sep 2026 17:24:05 +0200 Subject: [PATCH 4/6] tests --- inc/container.class.php | 11 +- tests/Units/ContainerItemRightTest.php | 133 +++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index a90e02d2..3ace8805 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1914,9 +1914,14 @@ public static function preItemUpdate(CommonDBTM $item) if ( isset($_SESSION['glpiactiveprofile']['id']) && $_SESSION['glpiactiveprofile']['id'] != null - && $item instanceof CommonITILObject - && Session::getCurrentInterface() === 'helpdesk' - && !$item->canRequesterUpdateItem() + && ( + // Central interface: no UPDATE right on this item type + !$item::canUpdate() + // Helpdesk interface: UPDATE right exists but user is not the requester (observer) + || ($item instanceof CommonITILObject + && Session::getCurrentInterface() === 'helpdesk' + && !$item->canRequesterUpdateItem()) + ) ) { unset($item->input['_plugin_fields_data']); return true; diff --git a/tests/Units/ContainerItemRightTest.php b/tests/Units/ContainerItemRightTest.php index ae31b33c..d6ee8eef 100644 --- a/tests/Units/ContainerItemRightTest.php +++ b/tests/Units/ContainerItemRightTest.php @@ -175,4 +175,137 @@ private function setRightOnContainer(int $containers_id, int $right): void ])); $this->updateItem(PluginFieldsProfile::class, $profile_right->getID(), ['right' => $right]); } + + /** + * Test rendering: helpdesk observer cannot edit fields + * An observer on a ticket should see fields rendered as readonly. + */ + public function testDomContainerRenderReadOnlyForHelpdeskObserver(): void + { + $this->login(); + $entity_id = getItemByTypeName(\Entity::class, '_test_root_entity', true); + $this->setEntity($entity_id, true); + + $container = $this->createFieldContainer([ + 'label' => 'Observer Readonly Container', + 'type' => 'dom', + 'itemtypes' => [\Ticket::class], + 'is_active' => 1, + 'entities_id' => $entity_id, + 'is_recursive' => 1, + ]); + $field = $this->createField([ + 'label' => 'Observer Test Field', + 'type' => 'text', + \PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + $ticket = $this->createItem(\Ticket::class, [ + 'name' => 'Ticket for observer test', + 'content' => 'Test', + 'entities_id' => $entity_id, + ]); + + // Create a helpdesk observer role + $observer_profile = $this->createItem(\Profile::class, [ + 'name' => 'Helpdesk_Observer_' . $this->getUniqueString(), + 'interface' => 'helpdesk', + ]); + // Grant write access on container + $this->setRightOnContainerForProfile($observer_profile->getID(), $container->getID(), READ); + + // Create observer user (not requester) + $observer_username = 'observer_' . $this->getUniqueString(); + $this->createItem(\User::class, [ + 'name' => $observer_username, + 'password' => 'Test1234!', + 'password2' => 'Test1234!', + 'profiles_id' => $observer_profile->getID(), + '_profiles_id' => $observer_profile->getID(), + '_entities_id' => $entity_id, + '_is_recursive' => true, + ], ['password', 'password2']); + + // Add observer to ticket + $this->createItem(\Ticket_User::class, [ + 'tickets_id' => $ticket->getID(), + 'users_id' => getItemByTypeName(\User::class, $observer_username, true), + 'type' => \CommonITILActor::OBSERVER, + ]); + + // Login as observer and render + $this->login($observer_username, 'Test1234!'); + $this->setEntity($entity_id, true); + + $html = $this->renderDomContainerForAny($container->getID(), $ticket); + + // Assert: field must be rendered with readonly attribute + $this->assertStringContainsString( + 'readonly', + $html, + 'Fields must be rendered as readonly for helpdesk observers.' + ); + } + + /** + * Test rendering: new item creation allows editing even for limited profiles + * When creating a new ticket, fields should remain editable regardless of observer role. + */ + public function testDomContainerRenderEditableOnNewTicketCreation(): void + { + $this->login(); + $entity_id = getItemByTypeName(\Entity::class, '_test_root_entity', true); + $this->setEntity($entity_id, true); + + $container = $this->createFieldContainer([ + 'label' => 'New Ticket Container', + 'type' => 'dom', + 'itemtypes' => [\Ticket::class], + 'is_active' => 1, + 'entities_id' => $entity_id, + 'is_recursive' => 1, + ]); + $field = $this->createField([ + 'label' => 'New Ticket Field', + 'type' => 'text', + \PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + // Create new (non-existent) ticket for rendering + $new_ticket = new \Ticket(); + $new_ticket->fields['entities_id'] = $entity_id; + + $html = $this->renderDomContainerForAny($container->getID(), $new_ticket); + + // Assert: field must NOT be readonly when creating a new ticket + $this->assertStringNotContainsString( + 'readonly', + $html, + 'Fields must remain editable when creating a new ticket.' + ); + } + + private function renderDomContainerForAny(int $containers_id, \CommonDBTM $item): string + { + ob_start(); + PluginFieldsField::showDomContainer($containers_id, $item); + return (string) ob_get_clean(); + } + + private function setRightOnContainerForProfile(int $profile_id, int $containers_id, int $right): void + { + $profile_right = new PluginFieldsProfile(); + if ($profile_right->getFromDBByCrit([ + 'profiles_id' => $profile_id, + 'plugin_fields_containers_id' => $containers_id, + ])) { + $this->updateItem(PluginFieldsProfile::class, $profile_right->getID(), ['right' => $right]); + } + } } From cb543e8e6bde18b82a450ddecf3eeb5abc151518 Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 30 Sep 2026 17:36:41 +0200 Subject: [PATCH 5/6] lint --- tests/Units/ContainerItemRightTest.php | 42 +++++++++++++++----------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/tests/Units/ContainerItemRightTest.php b/tests/Units/ContainerItemRightTest.php index d6ee8eef..b3264a0c 100644 --- a/tests/Units/ContainerItemRightTest.php +++ b/tests/Units/ContainerItemRightTest.php @@ -32,6 +32,12 @@ namespace GlpiPlugin\Field\Tests\Units; +use Ticket; +use Profile; +use User; +use Ticket_User; +use CommonITILActor; +use CommonDBTM; use Computer; use Entity; use Glpi\Tests\DbTestCase; @@ -183,34 +189,34 @@ private function setRightOnContainer(int $containers_id, int $right): void public function testDomContainerRenderReadOnlyForHelpdeskObserver(): void { $this->login(); - $entity_id = getItemByTypeName(\Entity::class, '_test_root_entity', true); + $entity_id = getItemByTypeName(Entity::class, '_test_root_entity', true); $this->setEntity($entity_id, true); $container = $this->createFieldContainer([ 'label' => 'Observer Readonly Container', 'type' => 'dom', - 'itemtypes' => [\Ticket::class], + 'itemtypes' => [Ticket::class], 'is_active' => 1, 'entities_id' => $entity_id, 'is_recursive' => 1, ]); - $field = $this->createField([ + $this->createField([ 'label' => 'Observer Test Field', 'type' => 'text', - \PluginFieldsContainer::getForeignKeyField() => $container->getID(), + PluginFieldsContainer::getForeignKeyField() => $container->getID(), 'ranking' => 1, 'is_active' => 1, 'is_readonly' => 0, ]); - $ticket = $this->createItem(\Ticket::class, [ + $ticket = $this->createItem(Ticket::class, [ 'name' => 'Ticket for observer test', 'content' => 'Test', 'entities_id' => $entity_id, ]); // Create a helpdesk observer role - $observer_profile = $this->createItem(\Profile::class, [ + $observer_profile = $this->createItem(Profile::class, [ 'name' => 'Helpdesk_Observer_' . $this->getUniqueString(), 'interface' => 'helpdesk', ]); @@ -219,7 +225,7 @@ public function testDomContainerRenderReadOnlyForHelpdeskObserver(): void // Create observer user (not requester) $observer_username = 'observer_' . $this->getUniqueString(); - $this->createItem(\User::class, [ + $this->createItem(User::class, [ 'name' => $observer_username, 'password' => 'Test1234!', 'password2' => 'Test1234!', @@ -230,10 +236,10 @@ public function testDomContainerRenderReadOnlyForHelpdeskObserver(): void ], ['password', 'password2']); // Add observer to ticket - $this->createItem(\Ticket_User::class, [ + $this->createItem(Ticket_User::class, [ 'tickets_id' => $ticket->getID(), - 'users_id' => getItemByTypeName(\User::class, $observer_username, true), - 'type' => \CommonITILActor::OBSERVER, + 'users_id' => getItemByTypeName(User::class, $observer_username, true), + 'type' => CommonITILActor::OBSERVER, ]); // Login as observer and render @@ -246,7 +252,7 @@ public function testDomContainerRenderReadOnlyForHelpdeskObserver(): void $this->assertStringContainsString( 'readonly', $html, - 'Fields must be rendered as readonly for helpdesk observers.' + 'Fields must be rendered as readonly for helpdesk observers.', ); } @@ -257,28 +263,28 @@ public function testDomContainerRenderReadOnlyForHelpdeskObserver(): void public function testDomContainerRenderEditableOnNewTicketCreation(): void { $this->login(); - $entity_id = getItemByTypeName(\Entity::class, '_test_root_entity', true); + $entity_id = getItemByTypeName(Entity::class, '_test_root_entity', true); $this->setEntity($entity_id, true); $container = $this->createFieldContainer([ 'label' => 'New Ticket Container', 'type' => 'dom', - 'itemtypes' => [\Ticket::class], + 'itemtypes' => [Ticket::class], 'is_active' => 1, 'entities_id' => $entity_id, 'is_recursive' => 1, ]); - $field = $this->createField([ + $this->createField([ 'label' => 'New Ticket Field', 'type' => 'text', - \PluginFieldsContainer::getForeignKeyField() => $container->getID(), + PluginFieldsContainer::getForeignKeyField() => $container->getID(), 'ranking' => 1, 'is_active' => 1, 'is_readonly' => 0, ]); // Create new (non-existent) ticket for rendering - $new_ticket = new \Ticket(); + $new_ticket = new Ticket(); $new_ticket->fields['entities_id'] = $entity_id; $html = $this->renderDomContainerForAny($container->getID(), $new_ticket); @@ -287,11 +293,11 @@ public function testDomContainerRenderEditableOnNewTicketCreation(): void $this->assertStringNotContainsString( 'readonly', $html, - 'Fields must remain editable when creating a new ticket.' + 'Fields must remain editable when creating a new ticket.', ); } - private function renderDomContainerForAny(int $containers_id, \CommonDBTM $item): string + private function renderDomContainerForAny(int $containers_id, CommonDBTM $item): string { ob_start(); PluginFieldsField::showDomContainer($containers_id, $item); From ad80d8b526e4e92ece0f3b88c031f5dbf574f72c Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 30 Sep 2026 17:50:19 +0200 Subject: [PATCH 6/6] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7924f2..1348685f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fix display width for rich text fields - Fix massive action update on CustomAssets - Fix mandatory fields on a Tab block not being enforced when updating an item. - Fix administrators losing access to a block's configuration after setting a profile to "no access" on that block. - Fix dependency conflict with GLPI core by no longer vendoring symfony/deprecation-contracts and symfony/polyfill-ctype. - Fix default field values not being applied when fields are empty on creation +- Fix a field's default value not being applied to existing items and not being shown in search results for items with no dedicated row in the container table - Fix mandatory fields blocking automated item creation +- Fix unclear mandatory field error when a GLPI form creating a ticket does not provide the field. - Fix ticket observers being able to edit and save additional fields they are not allowed to modify ## [1.24.5] - 2026-09-11