diff --git a/CHANGELOG.md b/CHANGELOG.md index e35fb6e4..cb4e6597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 mandatory fields blocking automated item creation ## [1.24.5] - 2026-09-11 diff --git a/inc/container.class.php b/inc/container.class.php index 350541f1..45afb6ed 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1653,6 +1653,15 @@ public static function constructHistory( } } + private static function isMandatoryCheckBypassed(array $data): bool + { + return isCommandLine() + || Session::isCron() + || isAPI() + || !empty($data['_auto_import']) + || !empty($data['is_dynamic']); + } + /** * check data inserted * display a message when not ok @@ -1739,9 +1748,9 @@ public static function validateValues($data, $itemtype, $massiveaction) $field['itemtype'] = PluginFieldsField::getType(); $field['label'] = PluginFieldsLabelTranslation::getLabelFor($field); - // Check mandatory fields if ( - $field['mandatory'] == 1 + !self::isMandatoryCheckBypassed($data) + && $field['mandatory'] == 1 && ( empty($value) || (($field['type'] === 'dropdown' || preg_match('/^dropdown-.+/i', (string) $field['type'])) && $value == 0) @@ -2028,6 +2037,14 @@ private static function checkContainerMandatory(CommonDBTM $item, PluginFieldsCo $status_field_name = PluginFieldsStatusOverride::getStatusFieldName($item::getType()); $data = ['plugin_fields_containers_id' => $c_id]; + if ($item->input['_auto_import'] ?? false) { + $data['_auto_import'] = true; + } + + if (!empty($item->input['is_dynamic'])) { + $data['is_dynamic'] = true; + } + 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] !== '') { @@ -2094,6 +2111,16 @@ private static function populateData($c_id, CommonDBTM $item) $data['items_id'] = $item->getID(); } + // Carry over the "automated import" markers so mandatory fields can be relaxed + // for items created without a human filling a form. + if (!empty($item->input['_auto_import'])) { + $data['_auto_import'] = true; + } + + if (!empty($item->input['is_dynamic'])) { + $data['is_dynamic'] = true; + } + // Add status so it can be used with status overrides $status_field_name = PluginFieldsStatusOverride::getStatusFieldName($item->getType()); $data[$status_field_name] = null; diff --git a/tests/Units/ContainerItemUpdateTest.php b/tests/Units/ContainerItemUpdateTest.php index e381a97e..760ce33e 100644 --- a/tests/Units/ContainerItemUpdateTest.php +++ b/tests/Units/ContainerItemUpdateTest.php @@ -30,6 +30,7 @@ namespace GlpiPlugin\Field\Tests\Units; +use Computer; use Glpi\Tests\DbTestCase; use Glpi\Tests\GLPITestCase; use GlpiPlugin\Field\Tests\FieldTestTrait; @@ -65,10 +66,14 @@ final class ContainerItemUpdateTest extends DbTestCase public function setUp(): void { GLPITestCase::setUp(); + + $GLOBALS['GLPI_IS_COMMAND_LINE'] = false; } public function tearDown(): void { + unset($GLOBALS['GLPI_IS_COMMAND_LINE']); + global $DB; $DB->setMustUnsanitizeData(false); // Be sure to switch back to disabled unsanitization. @@ -300,18 +305,16 @@ public function testCreateTicketInApiLikeContext(): void $this->simulateApiBoot(); - $ticket = new Ticket(); - $ticket_id = $ticket->add([ - 'name' => 'API created ticket', + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Ticket created via API', 'content' => 'Test creation', 'entities_id' => 0, - $field_name => 'created via api', - ]); - $this->assertGreaterThan(0, $ticket_id); + $field_name => 'api create value', + ], [$field_name]); - $plugin_row = $this->getPluginFieldValues(Ticket::class, $ticket_id, $container->getID()); + $plugin_row = $this->getPluginFieldValues(Ticket::class, $ticket->getID(), $container->getID()); $this->assertNotFalse($plugin_row, 'Plugin fields row must exist after API-like creation.'); - $this->assertSame('created via api', $plugin_row[$field_name]); + $this->assertSame('api create value', $plugin_row[$field_name]); } public function testCreateIsBlockedWhenMandatoryDomFieldIsMissing(): void @@ -368,6 +371,69 @@ public function testCreateIsBlockedWhenMandatoryDomFieldIsMissing(): void $this->assertSame('filled value', $plugin_row[$field_name]); } + public function testCreateIsNotBlockedForInventoryCreatedItem(): void + { + $this->login(); + + $container = $this->createFieldContainer([ + 'label' => 'Mandatory Inventory Container', + 'type' => 'dom', + 'itemtypes' => [Computer::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + $this->createField([ + 'label' => 'Mandatory Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + 'mandatory' => 1, + ]); + + $this->createItem(Computer::class, [ + 'name' => 'Computer created by the inventory agent', + 'entities_id' => 0, + 'is_dynamic' => 1, + ], ['is_dynamic']); + } + + public function testCreateIsNotBlockedInApiContext(): void + { + $this->login(); + + $container = $this->createFieldContainer([ + 'label' => 'Mandatory Api Container', + 'type' => 'dom', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + $this->createField([ + 'label' => 'Mandatory Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + 'mandatory' => 1, + ]); + + $this->simulateApiBoot(); + + $_SERVER['REQUEST_URI'] = '/apirest.php/Ticket'; + $this->assertTrue(isAPI()); + + $this->createItem(Ticket::class, [ + 'name' => 'Ticket created via the REST API', + 'content' => 'Test creation', + 'entities_id' => 0, + ]); + } + public function testCreateIsNotBlockedWhenMandatoryTabOrDomtabFieldIsMissing(): void { $this->login(); @@ -473,6 +539,7 @@ public function testUpdateIsBlockedWhenMandatoryTabFieldWasNeverFilled(): void 'id' => $problem->getID(), 'name' => 'Renamed while tab field still empty', ]); + $this->assertFalse($updated, 'Update must be blocked while a mandatory tab field is empty.'); $this->hasSessionMessageThatContains( __('Some mandatory fields are empty', 'fields'), diff --git a/tests/Units/ContainerTest.php b/tests/Units/ContainerTest.php index bd85899d..b00c6dae 100644 --- a/tests/Units/ContainerTest.php +++ b/tests/Units/ContainerTest.php @@ -56,10 +56,14 @@ public function setUp(): void { GLPITestCase::setUp(); $this->login(); + + $GLOBALS['GLPI_IS_COMMAND_LINE'] = false; } public function tearDown(): void { + unset($GLOBALS['GLPI_IS_COMMAND_LINE']); + $this->tearDownFieldTest(); GLPITestCase::tearDown(); } @@ -245,17 +249,9 @@ public function testMailCollectorImportRespectsMandatoryFieldDefaultValue( 'content' => 'This is a test email imported via the mail collector.', ]); - // No default value on the mandatory field $tkt = $collector->buildTicket(1, $message, ['mailgates_id' => $collector->getID(), 'play_rules' => false]); $tkt['entities_id'] = 0; - - $ticket = new Ticket(); - $ticket_id = $ticket->add($tkt); - $this->assertFalse($ticket_id, sprintf('Import must be blocked when the mandatory %s field has no value and no default.', $type)); - $this->hasSessionMessageThatContains( - __('Some mandatory fields are empty', 'fields'), - (string) ERROR, - ); + $this->createItem(Ticket::class, $tkt, ['users_id', 'itemtype']); $this->updateItem( PluginFieldsField::class, @@ -266,16 +262,13 @@ public function testMailCollectorImportRespectsMandatoryFieldDefaultValue( $tkt = $collector->buildTicket(2, $message, ['mailgates_id' => $collector->getID(), 'play_rules' => false]); $tkt['entities_id'] = 0; - - $ticket = new Ticket(); - $ticket_id = $ticket->add($tkt); - $this->assertGreaterThan(0, $ticket_id, sprintf('Import must succeed once the mandatory %s field has a default value.', $type)); + $ticket = $this->createItem(Ticket::class, $tkt, ['users_id', 'itemtype']); $classname = PluginFieldsContainer::getClassname(Ticket::class, $container->fields['name']); $obj = getItemForItemtype($classname); $obj->getFromDBByCrit([ 'plugin_fields_containers_id' => $container->getID(), - 'items_id' => $ticket_id, + 'items_id' => $ticket->getID(), ]); $container_ticket_fields_value = $obj->fields; $stored_value = $multiple ? json_decode((string) $container_ticket_fields_value[$row_key], true)