From f407141e8b130ca03f5936a1fc60958865a4844b Mon Sep 17 00:00:00 2001 From: tremo Date: Tue, 21 Jul 2026 15:05:19 +0200 Subject: [PATCH 1/3] fix(container.class): Prevent item creation with null value for mandatory fields --- inc/container.class.php | 9 ++ tests/Units/ContainerItemUpdateTest.php | 118 ++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/inc/container.class.php b/inc/container.class.php index 6f38c4dd..b35efa4d 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1970,6 +1970,15 @@ public static function preItem(CommonDBTM $item) return true; } + if ($item->isNewItem() && $loc_c->fields['type'] === 'dom') { + $data = ['plugin_fields_containers_id' => $c_id]; + if (self::validateValues($data, $item::getType(), isset($_REQUEST['massiveaction'])) === false) { + $item->input = []; + + return false; + } + } + return false; } diff --git a/tests/Units/ContainerItemUpdateTest.php b/tests/Units/ContainerItemUpdateTest.php index f8474a7c..b7a456de 100644 --- a/tests/Units/ContainerItemUpdateTest.php +++ b/tests/Units/ContainerItemUpdateTest.php @@ -311,6 +311,124 @@ public function testCreateTicketInApiLikeContext(): void $this->assertSame('created via api', $plugin_row[$field_name]); } + public function testCreateIsBlockedWhenMandatoryDomFieldIsMissing(): void + { + $this->login(); + + $container = $this->createFieldContainer([ + 'label' => 'Mandatory Create Container', + 'type' => 'dom', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $field = $this->createField([ + 'label' => 'Mandatory Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + 'mandatory' => 1, + ]); + $field_name = $field->fields['name']; + + $this->simulateApiBoot(); + + // Creation with the mandatory field omitted must be rejected. + $ticket = new Ticket(); + $ticket_id = $ticket->add([ + 'name' => 'Ticket without mandatory field', + 'content' => 'Test creation', + 'entities_id' => 0, + ]); + $this->assertFalse($ticket_id, 'Creation must be blocked when a mandatory dom field is missing.'); + $this->hasSessionMessageThatContains( + __('Some mandatory fields are empty', 'fields'), + ERROR, + ); + + // Creation with the mandatory field filled must succeed. + $ticket = new Ticket(); + $ticket_id = $ticket->add([ + 'name' => 'Ticket with mandatory field', + 'content' => 'Test creation', + 'entities_id' => 0, + $field_name => 'filled value', + ]); + $this->assertGreaterThan(0, $ticket_id); + + $plugin_row = $this->getPluginFieldValues(Ticket::class, $ticket_id, $container->getID()); + $this->assertNotFalse($plugin_row); + $this->assertSame('filled value', $plugin_row[$field_name]); + } + + public function testCreateIsNotBlockedWhenMandatoryTabOrDomtabFieldIsMissing(): void + { + $this->login(); + + // TAB container with a mandatory field. + $tab_container = $this->createFieldContainer([ + 'label' => 'Mandatory Tab Container', + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + $this->createField([ + 'label' => 'Mandatory Tab Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $tab_container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + 'mandatory' => 1, + ]); + + // DOMTAB container with a mandatory field, attached to a specific tab. + $domtab_container = $this->createFieldContainer([ + 'label' => 'Mandatory Domtab Container', + 'type' => 'domtab', + 'subtype' => Ticket::class . '$1', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + $this->createField([ + 'label' => 'Mandatory Domtab Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $domtab_container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + 'mandatory' => 1, + ]); + + // Creation with no plugin field must not block the creation. + $ticket = new Ticket(); + $ticket_id = $ticket->add([ + 'name' => 'Ticket with mandatory tab field missing', + 'content' => 'Test creation', + 'entities_id' => 0, + ]); + $this->assertGreaterThan(0, $ticket_id, 'A mandatory tab field must not block creation.'); + + // Creation forcing the "domtab" container resolution via explicit c_id: + // it must not block the creation either. + $ticket = new Ticket(); + $ticket_id = $ticket->add([ + 'name' => 'Ticket with mandatory domtab field missing', + 'content' => 'Test creation', + 'entities_id' => 0, + 'c_id' => $domtab_container->getID(), + ]); + $this->assertGreaterThan(0, $ticket_id, 'A mandatory domtab field must not block creation.'); + } + /** * Update a ticket with explicit c_id through an API-like context. */ From 9af89bba079519e6bd3bb1be17d7fdfbfda57760 Mon Sep 17 00:00:00 2001 From: tremo Date: Tue, 21 Jul 2026 15:06:38 +0200 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ef6902..be0ec1ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fix item creation with null value for mandatory fields - Fix search crash when two containers share a dropdown field with the same name. ## [1.24.2] - 2026-06-30 From 30f4d72a69fb6e11edee4b4cd5dc542f5a4fe281 Mon Sep 17 00:00:00 2001 From: tremo Date: Wed, 22 Jul 2026 08:57:47 +0200 Subject: [PATCH 3/3] add status override --- inc/container.class.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index b35efa4d..979a5a09 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1970,12 +1970,19 @@ public static function preItem(CommonDBTM $item) return true; } + //call validateValues() with a minimal data array to check for missing mandatory fields + //in case populateData() fails if ($item->isNewItem() && $loc_c->fields['type'] === 'dom') { + $status_field_name = PluginFieldsStatusOverride::getStatusFieldName($item::getType()); $data = ['plugin_fields_containers_id' => $c_id]; + 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]; + } + if (self::validateValues($data, $item::getType(), isset($_REQUEST['massiveaction'])) === false) { $item->input = []; - - return false; } }