From 8944f2fb5e439662b39148c4b5948e86afedcfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Fri, 4 Apr 2025 13:01:12 +0200 Subject: [PATCH 01/11] [Feature] Allow multiple "Insertion in the form" (dom) blocks per entity Created a new method findContainers() (based on findContainer()) that returns all 'dom' containers for an item based on its entity (with parent entity handling via getAncestorsOf()). Adapted hooks (pre_item_add, pre_item_update, post_item_add, post_item_update) to manage multiple containers using the _plugin_fields_data_multi array. Updated the populateData() function to extract input values by stripping the prefix, ensuring that data is saved into the correct columns of the injection table. Modified the container.form.php file to "clean" the form data (by removing the prefix) before calling updateFieldsValues(), thereby enabling the saving of domtab containers. This PR provides the ability to define multiple "Insertion in the form" blocks for the same item based on its entity by leveraging the new findContainers() method and adapting the save process. --- inc/container.class.php | 330 +++++++++++++++++++++++-------------- inc/field.class.php | 294 +++++++++++++++++---------------- templates/fields.html.twig | 3 +- 3 files changed, 356 insertions(+), 271 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index d8cc983f..da2d91a2 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -557,22 +557,6 @@ public function prepareInputForAdd($input) $input['itemtypes'] = [$input['itemtypes']]; } - if ($input['type'] === 'dom') { - //check for already exist dom container with this itemtype - $found = $this->find(['type' => 'dom']); - if (count($found) > 0) { - foreach (array_column($found, 'itemtypes') as $founditemtypes) { - foreach (json_decode($founditemtypes) as $founditemtype) { - if (in_array($founditemtype, $input['itemtypes'])) { - Session::AddMessageAfterRedirect(__("You cannot add several blocks with type 'Insertion in the form' on same object", 'fields'), false, ERROR); - - return false; - } - } - } - } - } - if ($input['type'] === 'domtab') { //check for already exist domtab container with this itemtype on this tab $found = $this->find(['type' => 'domtab', 'subtype' => $input['subtype']]); @@ -1604,6 +1588,82 @@ public static function findContainer($itemtype, $type = 'tab', $subtype = '') return $id; } + public static function findContainers($itemtype, $type = 'tab', $subtype = '', $itemId = '') + { + global $DB; + $ids = []; + + if (!empty($itemtype) && !empty($itemId) && class_exists($itemtype)) { + + $obj = new $itemtype(); + if ($obj->getFromDB($itemId)) { + + $entityId = $obj->fields['entities_id']; + $entityIds = getAncestorsOf("glpi_entities", $entityId); + $entityIds[] = $entityId; // Add entity obj itself to the list + $glpiActiveEntities = $_SESSION['glpiactiveentities'] ?? 0; + + $entityRestriction = getEntitiesRestrictCriteria('', '', $glpiActiveEntities, true, true); + + if (empty($entityIds)) { + $entityIds = [$entityId]; + } + $entityIdList = implode(",", $entityIds); + + $sql = "SELECT id FROM glpi_plugin_fields_containers + WHERE is_active = 1 + AND type = '$type' + AND JSON_CONTAINS(itemtypes, '\"$itemtype\"') + AND ( + (is_recursive = 1 AND entities_id IN ($entityIdList)) + OR (is_recursive = 0 AND entities_id = '$entityId') + )"; + + if ($subtype !== '') { + if ($subtype === $itemtype . '$main') { + $sql .= " AND type = 'dom'"; + } else { + $sql .= " AND type != 'dom' AND subtype = '$subtype'"; + } + } else { + $sql .= " AND type = '$type'"; + } + + if (is_array($entityRestriction) && !empty($entityRestriction)) { + $allowedEntities = []; + foreach ($entityRestriction as $restriction) { + if (isset($restriction['entities_id']) && is_array($restriction['entities_id'])) { + $allowedEntities = array_merge($allowedEntities, $restriction['entities_id']); + } + } + if (!empty($allowedEntities)) { + $allowedEntitiesStr = implode(",", $allowedEntities); + $sql .= " AND entities_id IN ($allowedEntitiesStr)"; + } + } + + $res = $DB->query($sql); + + while ($row = $DB->fetchAssoc($res)) { + $containerId = (int) $row['id']; + + //profiles restriction + if (isset($_SESSION['glpiactiveprofile']['id']) && $_SESSION['glpiactiveprofile']['id'] !== null) { + $profileId = $_SESSION['glpiactiveprofile']['id']; + $right = PluginFieldsProfile::getRightOnContainer($profileId, $containerId); + if ($right < READ) { + continue; + } + } + + $ids[] = $containerId; + } + } + } + + return $ids; + } + /** * Post item hook for add * Do store data in db @@ -1614,18 +1674,20 @@ public static function findContainer($itemtype, $type = 'tab', $subtype = '') */ public static function postItemAdd(CommonDBTM $item) { - if (array_key_exists('_plugin_fields_data', $item->input)) { - $data = $item->input['_plugin_fields_data']; - $data['items_id'] = $item->getID(); - $data['entities_id'] = $item->isEntityAssign() ? $item->getEntityID() : 0; - //update data - $container = new self(); - if ($container->updateFieldsValues($data, $item->getType(), isset($_REQUEST['massiveaction']))) { - return true; - } + if (array_key_exists('_plugin_fields_data_multi', $item->input)) { + $dataMulti = $item->input['_plugin_fields_data_multi']; + foreach ($dataMulti as $data) { + $data['items_id'] = $item->getID(); + $data['entities_id'] = $item->isEntityAssign() ? $item->getEntityID() : 0; + //update data + $container = new self(); + if ($container->updateFieldsValues($data, $item->getType(), isset($_REQUEST['massiveaction']))) { + continue; + }; - $item->input = []; - return $item; + $item->input = []; + continue; + } } return true; @@ -1642,23 +1704,22 @@ public static function postItemAdd(CommonDBTM $item) public static function preItemUpdate(CommonDBTM $item) { self::preItem($item); - if (array_key_exists('_plugin_fields_data', $item->input)) { - $data = $item->input['_plugin_fields_data']; - $data['entities_id'] = $item->isEntityAssign() ? $item->getEntityID() : 0; - //update data - $container = new self(); - if ( - count($data) == 0 - || $container->updateFieldsValues($data, $item->getType(), isset($_REQUEST['massiveaction'])) - ) { - $item->input['date_mod'] = $_SESSION['glpi_currenttime']; - - return true; + if (array_key_exists('_plugin_fields_data_multi', $item->input)) { + $dataMulti = $item->input['_plugin_fields_data_multi']; + foreach ($dataMulti as $data) { + $data['entities_id'] = $item->isEntityAssign() ? $item->getEntityID() : 0; + //update data + $container = new self(); + if ( + count($data) == 0 + || $container->updateFieldsValues($data, $item->getType(), isset($_REQUEST['massiveaction'])) + ) { + $item->input['date_mod'] = $_SESSION['glpi_currenttime']; + continue; + } + continue; } - - return false; } - return true; } @@ -1672,69 +1733,62 @@ public static function preItemUpdate(CommonDBTM $item) */ public static function preItem(CommonDBTM $item) { - //find container (if not exist, do nothing) - if (isset($item->input['c_id'])) { - $c_id = $item->input['c_id']; - } elseif (isset($_REQUEST['c_id'])) { - $c_id = $_REQUEST['c_id']; - } else { - $type = 'dom'; - if (isset($_REQUEST['_plugin_fields_type'])) { - $type = $_REQUEST['_plugin_fields_type']; - } - $subtype = ''; - if ($type == 'domtab') { - $subtype = $_REQUEST['_plugin_fields_subtype']; - } - if (false === ($c_id = self::findContainer(get_Class($item), $type, $subtype))) { - // tries for 'tab' - if (false === ($c_id = self::findContainer(get_Class($item)))) { - return false; - } - } + $type = 'dom'; + if (isset($_REQUEST['_plugin_fields_type'])) { + $type = $_REQUEST['_plugin_fields_type']; + } + $subtype = ''; + if ($type == 'domtab') { + $subtype = $_REQUEST['_plugin_fields_subtype']; } - $loc_c = new PluginFieldsContainer(); - $loc_c->getFromDB($c_id); + $containers = self::findContainers($item->getType(), $type, $subtype, $item->getID()); - // check rights on $c_id + $all_data = []; - if (isset($_SESSION['glpiactiveprofile']['id']) && $_SESSION['glpiactiveprofile']['id'] != null && $c_id > 0) { - $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $c_id); - if (($right > READ) === false) { - return false; - } - } else { - return false; - } + foreach ($containers as $c_id) { + $loc_c = new PluginFieldsContainer(); + $loc_c->getFromDB($c_id); - // need to check if container is usable on this object entity - $entities = [$loc_c->fields['entities_id']]; - if ($loc_c->fields['is_recursive']) { - $entities = getSonsOf(getTableForItemType('Entity'), $loc_c->fields['entities_id']); - } + // check rights on $c_id - if (count($item->fields) === 0) { - $item->fields = $item->input; - } + if (isset($_SESSION['glpiactiveprofile']['id']) && $_SESSION['glpiactiveprofile']['id'] != null && $c_id > 0) { + $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $c_id); + if ($right > READ === false) { // Si le droit est insuffisant, on passe au container suivant + continue; + } + } else { + continue; + } - if ($item->isEntityAssign() && !in_array($item->getEntityID(), $entities)) { - return false; - } + // need to check if container is usable on this object entity + $entities = [$loc_c->fields['entities_id']]; + if ($loc_c->fields['is_recursive']) { + $entities = getSonsOf(getTableForItemType('Entity'), $loc_c->fields['entities_id']); + } - if (false !== ($data = self::populateData($c_id, $item))) { - if (self::validateValues($data, $item::getType(), isset($_REQUEST['massiveaction'])) === false) { - $item->input = []; + if (count($item->fields) === 0) { + $item->fields = $item->input; + } - return false; + if ($item->isEntityAssign() && !in_array($item->getEntityID(), $entities)) { + continue; } - $item->input['_plugin_fields_data'] = $data; - return true; + if (false !== ($data = self::populateData($c_id, $item))) { + if (self::validateValues($data, $item->getType(), isset($_REQUEST['massiveaction'])) === false) { + $item->input = []; + + return false; + } + $all_data[] = $data; + } } - return false; + $item->input['_plugin_fields_data_multi'] = $all_data; + + return true; } /** @@ -1745,7 +1799,7 @@ public static function preItem(CommonDBTM $item) * * @return array|false */ - private static function populateData($c_id, CommonDBTM $item) + public static function populateData($c_id, CommonDBTM $item) { //find fields associated to found container $field_obj = new PluginFieldsField(); @@ -1758,14 +1812,15 @@ private static function populateData($c_id, CommonDBTM $item) ); //prepare data to update - $data = ['plugin_fields_containers_id' => $c_id]; - if (!$item->isNewItem()) { - //no ID yet while creating - $data['items_id'] = $item->getID(); - } + $data = [ + 'plugin_fields_containers_id' => $c_id, + 'items_id' => $item->getID(), + 'itemtype' => $item->getType(), + 'entities_id' => $item->getEntityID(), + ]; // Add status so it can be used with status overrides - $status_field_name = PluginFieldsStatusOverride::getStatusFieldName($item->getType()); + $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]; @@ -1774,45 +1829,68 @@ private static function populateData($c_id, CommonDBTM $item) } $has_fields = false; + // Prefix for input names + $prefix = "plugin_fields_{$c_id}_"; + foreach ($fields as $field) { + $base_name = $field['name']; if ($field['type'] == 'glpi_item') { - $itemtype_key = sprintf('itemtype_%s', $field['name']); - $items_id_key = sprintf('items_id_%s', $field['name']); + $itemtype_key = "itemtype_{$base_name}"; + $items_id_key = "items_id_{$base_name}"; if (!isset($item->input[$itemtype_key], $item->input[$items_id_key])) { continue; // not a valid input } - $has_fields = true; - $data[$itemtype_key] = $item->input[$itemtype_key]; - $data[$items_id_key] = $item->input[$items_id_key]; + $has_fields = true; + $data[$itemtype_key] = $item->input[$itemtype_key]; + $data[$items_id_key] = $item->input[$items_id_key]; continue; // bypass unique field handling } - if (isset($item->input[$field['name']])) { - //standard field - $input = $field['name']; - } else { - //dropdown field - $input = 'plugin_fields_' . $field['name'] . 'dropdowns_id'; + // For other fields, the input name to be prefixed with "plugin_fields_{$c_id}_" and the field name + // "plugin_fields_{$c_id}_{$base_name}" + if ($field['type'] === 'dropdown') { + // For dropdown fields, the input name is "plugin_fields_{$c_id}_{$base_name}dropdowns_id" + $input = $prefix . $base_name . "dropdowns_id"; + if (isset($item->input[$input])) { + $has_fields = true; + $data[$base_name . "_dropdowns_id"] = $item->input[$input]; + } + // If the field is a dropdown with multiple selection, we need to check if the input name is defined + elseif ($field['multiple']) { + $multiple_key = $input; + $multiple_defined = '_' . $multiple_key . '_defined'; + if (isset($item->input[$multiple_key])) { + $has_fields = true; + $data[$base_name . "_dropdowns_id"] = $item->input[$multiple_key]; + } elseif (isset($item->input[$multiple_defined]) && $item->input[$multiple_defined]) { + $has_fields = true; + $data[$base_name . "_dropdowns_id"] = []; + } + } + continue; } + + // For fields standard, the input name is "plugin_fields_{$c_id}_{$base_name}" + $input = $prefix . $base_name; if (isset($item->input[$input])) { $has_fields = true; // Before is_number check, help user to have a number correct, during a massive action of a number field if ($field['type'] == 'number') { $item->input[$input] = str_replace(',', '.', $item->input[$input]); } - $data[$input] = $item->input[$input]; + $data[$base_name] = $item->input[$input]; if ($field['type'] === 'richtext') { - $filename_input = sprintf('_%s', $input); - $prefix_input = sprintf('_prefix_%s', $input); - $tag_input = sprintf('_tag_%s', $input); + $filename_input = "_" . $input; + $prefix_input = "_prefix_" . $input; + $tag_input = "_tag_" . $input; $data[$filename_input] = $item->input[$filename_input] ?? []; - $data[$prefix_input] = $item->input[$prefix_input] ?? []; - $data[$tag_input] = $item->input[$tag_input] ?? []; + $data[$prefix_input] = $item->input[$prefix_input] ?? []; + $data[$tag_input] = $item->input[$tag_input] ?? []; } } else { //the absence of the field in the input may be due to the fact that the input allows multiple selection @@ -1821,18 +1899,18 @@ private static function populateData($c_id, CommonDBTM $item) if ($field['multiple']) { //handle multi dropdown field if ($field['type'] == 'dropdown') { - $multiple_key = sprintf('plugin_fields_%sdropdowns_id', $field['name']); - $multiple_key_defined = '_' . $multiple_key . '_defined'; + $multiple_key = $prefix . $base_name . "dropdowns_id"; + $multiple_defined = '_' . $multiple_key . '_defined'; //values are defined by user if (isset($item->input[$multiple_key])) { - $data[$multiple_key] = $item->input[$multiple_key]; - $has_fields = true; + $data[$base_name . "_dropdowns_id"] = $item->input[$multiple_key]; + $has_fields = true; } elseif ( - isset($item->input[$multiple_key_defined]) - && $item->input[$multiple_key_defined] + isset($item->input[$multiple_defined]) + && $item->input[$multiple_defined] ) { //multi dropdown is empty or has been emptied - $data[$multiple_key] = []; - $has_fields = true; + $data[$base_name . "_dropdowns_id"] = []; + $has_fields = true; } } @@ -1840,10 +1918,10 @@ private static function populateData($c_id, CommonDBTM $item) if (preg_match('/^dropdown-(?.+)$/', $field['type'], $match) === 1) { //values are defined by user if (isset($item->input[$field['name']])) { - $data[$field['name']] = $item->input[$field['name']]; - $has_fields = true; + $data[$base_name] = $item->input[$field['name']]; + $has_fields = true; } else { //multi dropdown is empty or has been emptied - $data[$field['name']] = []; + $data[$base_name] = []; } } } diff --git a/inc/field.class.php b/inc/field.class.php index 02d43247..c4af01e3 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -871,8 +871,13 @@ public static function showDomContainer($id, $item, $type = 'dom', $subtype = '' $fields = []; } + $fieldTypeName = '_plugin_fields_type_' . $id; + $fieldSubTypeName = '_plugin_fields_subtype_' . $id; + echo Html::hidden('_plugin_fields_type', ['value' => $type]); echo Html::hidden('_plugin_fields_subtype', ['value' => $subtype]); + echo Html::hidden($fieldTypeName, ['value' => $type]); + echo Html::hidden($fieldSubTypeName, ['value' => $subtype]); echo self::prepareHtmlFields($fields, $item, true, true, false, $field_options); } @@ -904,172 +909,173 @@ public static function showForTab($params) $subtype = ''; } - //find container (if not exist, do nothing) - if (isset($_REQUEST['c_id'])) { - $c_id = $_REQUEST['c_id']; - } elseif (!$c_id = PluginFieldsContainer::findContainer(get_Class($item), $type, $subtype)) { + if (!isset($item->fields['id'])) { return; } + $itemId = $item->fields['id']; + + $container_ids = PluginFieldsContainer::findContainers(get_class($item), $type, $subtype, $itemId); - $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $c_id); - if ($right < READ) { - return; - } + foreach ($container_ids as $container_id) { - Html::requireJs('tinymce'); + $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $container_id); + if ($right < READ) { + continue; + } - //need to check if container is usable on this object entity - $loc_c = new PluginFieldsContainer(); - $loc_c->getFromDB($c_id); - $entities = [$loc_c->fields['entities_id']]; - if ($loc_c->fields['is_recursive']) { - $entities = getSonsOf(getTableForItemType('Entity'), $loc_c->fields['entities_id']); - } + //need to check if container is usable on this object entity + $loc_c = new PluginFieldsContainer(); + $loc_c->getFromDB($container_id); + $entities = [$loc_c->fields['entities_id']]; + if ($loc_c->fields['is_recursive']) { + $entities = getSonsOf(getTableForItemType('Entity'), $loc_c->fields['entities_id']); + } - if ($item->isEntityAssign()) { - $current_entity = $item->getEntityID(); - if (!in_array($current_entity, $entities)) { - return; + if ($item->isEntityAssign()) { + $current_entity = $item->getEntityID(); + if (!in_array($current_entity, $entities)) { + continue; + } } - } - //parse REQUEST_URI - if (!isset($_SERVER['REQUEST_URI'])) { - return; - } - $current_url = $_SERVER['REQUEST_URI']; - if ( - strpos($current_url, '.form.php') === false - && strpos($current_url, '.injector.php') === false - && strpos($current_url, '.public.php') === false - && strpos($current_url, 'ajax/timeline.php') === false // ITILSolution load from timeline - ) { - return; - } + //parse REQUEST_URI + if (!isset($_SERVER['REQUEST_URI'])) { + continue; + } + $current_url = $_SERVER['REQUEST_URI']; + if ( + strpos($current_url, '.form.php') === false + && strpos($current_url, '.injector.php') === false + && strpos($current_url, '.public.php') === false + && strpos($current_url, 'ajax/timeline.php') === false // ITILSolution load from timeline + ) { + continue; + } - //Retrieve dom container - $itemtypes = PluginFieldsContainer::getUsedItemtypes($type, true); + //Retrieve dom container + $itemtypes = PluginFieldsContainer::getUsedItemtypes($type, true); - //if no dom containers defined for this itemtype, do nothing (in_array case insensitive) - if (!in_array(strtolower($item::getType()), array_map('strtolower', $itemtypes))) { - return; - } + //if no dom containers defined for this itemtype, do nothing (in_array case insensitive) + if (!in_array(strtolower($item::getType()), array_map('strtolower', $itemtypes))) { + continue; + } - $html_id = 'plugin_fields_container_' . mt_rand(); - if (strpos($current_url, 'helpdesk.public.php') !== false) { - echo "
"; - echo "
"; - $field_options = [ - 'label_class' => 'col-lg-3', - 'input_class' => 'col-lg-9', - ]; - } else { - echo "
"; - } - $display_condition = new PluginFieldsContainerDisplayCondition(); - if ($display_condition->computeDisplayContainer($item, $c_id)) { - self::showDomContainer( - $c_id, - $item, - $type, - $subtype, - $field_options ?? [], - ); - } - if (strpos($current_url, 'helpdesk.public.php') !== false) { + $html_id = 'plugin_fields_container_' . $container_id; + if (strpos($current_url, 'helpdesk.public.php') !== false) { + echo "
"; + echo "
"; + $field_options = [ + 'label_class' => 'col-lg-3', + 'input_class' => 'col-lg-9', + ]; + } else { + echo "
"; + } + $display_condition = new PluginFieldsContainerDisplayCondition(); + if ($display_condition->computeDisplayContainer($item, $container_id)) { + self::showDomContainer( + $container_id, + $item, + $type, + $subtype, + $field_options ?? [], + ); + } + if (strpos($current_url, 'helpdesk.public.php') !== false) { + echo '
'; + } echo '
'; - } - echo '
'; - //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; - echo Html::scriptBlock( - <<isNewItem() ? $item->getID() : 0; + echo Html::scriptBlock( + << 0) { - return; // Do nothing if element is inside fields container - } - refreshContainer(); - } + {} ); - var refresh_timeout = null; - form.find('textarea').each( - function () { - const editor = tinymce.get(this.id); - if (editor !== null) { - editor.on( - 'change', - function(evt) { - if ($(evt.target.targetElm).closest('#{$html_id}').length > 0) { - return; // Do nothing if element is inside fields container - } - - if (refresh_timeout !== null) { - window.clearTimeout(refresh_timeout); - } - refresh_timeout = window.setTimeout(refreshContainer, 1000); - } - ); + $.ajax( + { + url: '{$ajax_url}', + type: 'GET', + data: { + action: 'get_fields_html', + id: {$container_id}, + itemtype: '{$item::getType()}', + items_id: {$items_id}, + type: '{$type}', + subtype: '{$subtype}', + input: data + }, + success: function(data) { + // Close open select2 dropdown that will be replaced + $('#{$html_id}').find('.select2-hidden-accessible').select2('close'); + // Refresh fields HTML + $('#{$html_id}').html(data); } } ); } + $( + function () { + const form = $('#{$html_id}').closest('form'); + form.on( + 'change', + 'input, select, textarea', + function(evt) { + if (evt.target.name == "itilcategories_id") { + // Do not refresh tab container when form is reloaded + // to prevent issues diues to duplicated calls + return; + } + if ($(evt.target).closest('#{$html_id}').length > 0) { + return; // Do nothing if element is inside fields container + } + refreshContainer(); + } + ); + + var refresh_timeout = null; + form.find('textarea').each( + function () { + const editor = tinymce.get(this.id); + if (editor !== null) { + editor.on( + 'change', + function(evt) { + if ($(evt.target.targetElm).closest('#{$html_id}').length > 0) { + return; // Do nothing if element is inside fields container + } + + if (refresh_timeout !== null) { + window.clearTimeout(refresh_timeout); + } + refresh_timeout = window.setTimeout(refreshContainer, 1000); + } + ); + } + } + ); + } + ); + JAVASCRIPT ); -JAVASCRIPT - ); + } } public static function prepareHtmlFields( diff --git a/templates/fields.html.twig b/templates/fields.html.twig index b9480002..9e24a057 100644 --- a/templates/fields.html.twig +++ b/templates/fields.html.twig @@ -40,8 +40,9 @@ {% for field in fields %} + {% set cid = container.fields.id %} {% set type = field['type'] %} - {% set name = field['name'] %} + {% set name = 'plugin_fields_' ~ cid ~ '_' ~ field['name'] %} {% set label = field['label'] %} {% set value = item.input[name] ?: field['value'] %} {% set readonly = field['is_readonly'] %} From 7a9381e8da0c8ef2f398f53c3189a3206f49a58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Fri, 4 Apr 2025 13:26:40 +0200 Subject: [PATCH 02/11] Modified the container.form.php file to "clean" the form data (by removing the prefix) before calling updateFieldsValues(), thereby enabling the saving of domtab containers. --- front/container.form.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/front/container.form.php b/front/container.form.php index b8dd3bca..90d244f7 100644 --- a/front/container.form.php +++ b/front/container.form.php @@ -55,6 +55,17 @@ } elseif (isset($_POST['update_fields_values'])) { $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $_POST['plugin_fields_containers_id']); if ($right > READ) { + $containerID = $_POST['plugin_fields_containers_id']; + $data = []; + foreach ($_REQUEST as $key => $value) { + // if key starts with plugin_fields__ remove the prefix + if (strpos($key, "plugin_fields_{$containerID}_") === 0) { + $new_key = substr($key, strlen("plugin_fields_{$containerID}_")); + $data[$new_key] = $value; + } else { + $data[$key] = $value; + } + } $container->updateFieldsValues($_REQUEST, $_REQUEST['itemtype'], false); } Html::back(); From f573ff10c9e1d6efa776a125d5d5997887c7ae53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Wed, 9 Apr 2025 10:45:53 +0200 Subject: [PATCH 03/11] fix: Undefined array key "entities_id" --- inc/container.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/container.class.php b/inc/container.class.php index da2d91a2..b3ca0c18 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1598,7 +1598,7 @@ public static function findContainers($itemtype, $type = 'tab', $subtype = '', $ $obj = new $itemtype(); if ($obj->getFromDB($itemId)) { - $entityId = $obj->fields['entities_id']; + $entityId = $obj->fields['entities_id'] ?? 0; $entityIds = getAncestorsOf("glpi_entities", $entityId); $entityIds[] = $entityId; // Add entity obj itself to the list $glpiActiveEntities = $_SESSION['glpiactiveentities'] ?? 0; From a19440569cfbcac83dd7a7b59b2664ed0c8e69e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Fri, 11 Apr 2025 19:52:31 +0200 Subject: [PATCH 04/11] feat: support custom Fields values in GLPI ticket notifications - Inject ##fields..## placeholders into ticket notifications --- inc/notificationtargetticket.class.php | 47 ++++++++++++++++++++++++++ setup.php | 4 +++ 2 files changed, 51 insertions(+) create mode 100644 inc/notificationtargetticket.class.php diff --git a/inc/notificationtargetticket.class.php b/inc/notificationtargetticket.class.php new file mode 100644 index 00000000..58064fc1 --- /dev/null +++ b/inc/notificationtargetticket.class.php @@ -0,0 +1,47 @@ +raiseevent; + if (isset($target->obj->fields['id'])) { + $tickets_id = $target->obj->fields['id']; + + $containers = PluginFieldsContainer::findContainers('Ticket', 'dom', '', $tickets_id); + + foreach ($containers as $c_id) { + + $container_obj = new PluginFieldsContainer(); + $container_obj->getFromDB($c_id); + + if (empty($container_obj->fields)) { + continue; + } + + $container_name = $container_obj->fields['name']; + $bloc_classname = PluginFieldsContainer::getClassname('Ticket', $container_name); + + if (class_exists($bloc_classname)) { + + $bloc_obj = new $bloc_classname(); + + $values = $bloc_obj->find([ + 'plugin_fields_containers_id' => $c_id, + 'items_id' => $tickets_id, + ]); + $values = array_shift($values); + + foreach ($values as $field_name => $value) { + if (in_array($field_name, ['id', 'items_id', 'itemtype', 'plugin_fields_containers_id', 'entities_id'])) { + continue; + } + $key = "##fields.{$container_name}.{$field_name}##"; + $target->data[$key] = is_array($value) ? implode(', ', $value) : $value; + } + } + } + } + } +} diff --git a/setup.php b/setup.php index 3ba7b51c..8e1d5440 100644 --- a/setup.php +++ b/setup.php @@ -190,6 +190,10 @@ function plugin_init_fields() 'PluginFieldsField', 'showForTab', ]; + + $PLUGIN_HOOKS['item_get_datas']['fields'] = [ + NotificationTargetTicket::class => [PluginFieldsNotificationTargetTicket::class, 'addNotificationDatas'] + ]; } } From 24de1bc9c058ffa470de6dd7db9adf41f371cd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Mon, 14 Apr 2025 18:30:21 +0200 Subject: [PATCH 05/11] fix: error php-cs-fixer --- inc/container.class.php | 4 ++-- inc/field.class.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index b3ca0c18..b4d70613 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1776,10 +1776,10 @@ public static function preItem(CommonDBTM $item) continue; } - if (false !== ($data = self::populateData($c_id, $item))) { + if (false !== ($data = self::populateData($c_id, $item))) { if (self::validateValues($data, $item->getType(), isset($_REQUEST['massiveaction'])) === false) { $item->input = []; - + return false; } $all_data[] = $data; diff --git a/inc/field.class.php b/inc/field.class.php index c4af01e3..119eb3e3 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -913,7 +913,7 @@ public static function showForTab($params) return; } $itemId = $item->fields['id']; - + $container_ids = PluginFieldsContainer::findContainers(get_class($item), $type, $subtype, $itemId); foreach ($container_ids as $container_id) { From 59767ac67331b90cc02b3bf75a9e9810623e502f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Wed, 16 Apr 2025 18:04:58 +0200 Subject: [PATCH 06/11] fix: PHPStan warnings in findContainers() - Added missing @var annotation for global $DB - Replaced empty() on $entityIds with count() - Removed redundant is_array() check on $entityRestriction - Simplified isset() condition by removing unnecessary !== null check --- inc/container.class.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index b4d70613..9a1baee0 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1590,6 +1590,7 @@ public static function findContainer($itemtype, $type = 'tab', $subtype = '') public static function findContainers($itemtype, $type = 'tab', $subtype = '', $itemId = '') { + /** @var DBmysql $DB */ global $DB; $ids = []; @@ -1605,7 +1606,7 @@ public static function findContainers($itemtype, $type = 'tab', $subtype = '', $ $entityRestriction = getEntitiesRestrictCriteria('', '', $glpiActiveEntities, true, true); - if (empty($entityIds)) { + if (count($entityIds) === 0) { $entityIds = [$entityId]; } $entityIdList = implode(",", $entityIds); @@ -1629,7 +1630,7 @@ public static function findContainers($itemtype, $type = 'tab', $subtype = '', $ $sql .= " AND type = '$type'"; } - if (is_array($entityRestriction) && !empty($entityRestriction)) { + if (!empty($entityRestriction)) { $allowedEntities = []; foreach ($entityRestriction as $restriction) { if (isset($restriction['entities_id']) && is_array($restriction['entities_id'])) { @@ -1648,7 +1649,7 @@ public static function findContainers($itemtype, $type = 'tab', $subtype = '', $ $containerId = (int) $row['id']; //profiles restriction - if (isset($_SESSION['glpiactiveprofile']['id']) && $_SESSION['glpiactiveprofile']['id'] !== null) { + if (isset($_SESSION['glpiactiveprofile']['id'])) { $profileId = $_SESSION['glpiactiveprofile']['id']; $right = PluginFieldsProfile::getRightOnContainer($profileId, $containerId); if ($right < READ) { From 8f60bc9001c69301008a3160b0eb15e7be7ac3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Thu, 17 Apr 2025 12:23:59 +0000 Subject: [PATCH 07/11] update: use GLPI DBIterator Note: PHPStan still reports a type error on DB::request() (expects array|string), but this issue already exists elsewhere in the file and was not introduced by this change. --- inc/container.class.php | 50 ++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index 9a1baee0..90ff408f 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1606,31 +1606,36 @@ public static function findContainers($itemtype, $type = 'tab', $subtype = '', $ $entityRestriction = getEntitiesRestrictCriteria('', '', $glpiActiveEntities, true, true); - if (count($entityIds) === 0) { - $entityIds = [$entityId]; - } - $entityIdList = implode(",", $entityIds); - - $sql = "SELECT id FROM glpi_plugin_fields_containers - WHERE is_active = 1 - AND type = '$type' - AND JSON_CONTAINS(itemtypes, '\"$itemtype\"') - AND ( - (is_recursive = 1 AND entities_id IN ($entityIdList)) - OR (is_recursive = 0 AND entities_id = '$entityId') - )"; + $where = [ + 'is_active' => 1, + 'type' => $type, + new \QueryExpression("JSON_CONTAINS(itemtypes, " . $DB->quote('"' . $itemtype . '"') . ")"), + 'AND' => [ + 'OR' => [ + [ + 'is_recursive' => 1, + 'entities_id' => $entityIds, + ], + [ + 'is_recursive' => 0, + 'entities_id' => $entityId, + ] + ] + ] + ]; if ($subtype !== '') { if ($subtype === $itemtype . '$main') { - $sql .= " AND type = 'dom'"; + $where['type'] = 'dom'; } else { - $sql .= " AND type != 'dom' AND subtype = '$subtype'"; + $where['type'] = ['!=', 'dom']; + $where['subtype'] = $subtype; } } else { - $sql .= " AND type = '$type'"; + $where['type'] = $type; } - if (!empty($entityRestriction)) { + if (is_array($entityRestriction) && !empty($entityRestriction)) { $allowedEntities = []; foreach ($entityRestriction as $restriction) { if (isset($restriction['entities_id']) && is_array($restriction['entities_id'])) { @@ -1638,14 +1643,17 @@ public static function findContainers($itemtype, $type = 'tab', $subtype = '', $ } } if (!empty($allowedEntities)) { - $allowedEntitiesStr = implode(",", $allowedEntities); - $sql .= " AND entities_id IN ($allowedEntitiesStr)"; + $where['entities_id'] = $allowedEntities; } } - $res = $DB->query($sql); + $iterator = $DB->request([ + 'SELECT' => 'id', + 'FROM' => self::getTable(), + 'WHERE' => $where, + ]); - while ($row = $DB->fetchAssoc($res)) { + foreach ($iterator as $row) { $containerId = (int) $row['id']; //profiles restriction From 810a542280b86a4ddf63564e415a96e0dd419421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Thu, 17 Apr 2025 12:42:48 +0000 Subject: [PATCH 08/11] php-cs-fixer OK and phpstan OK --- inc/notificationtargetticket.class.php | 94 +++++++++++++------------- setup.php | 2 +- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/inc/notificationtargetticket.class.php b/inc/notificationtargetticket.class.php index 58064fc1..ef7f1d0e 100644 --- a/inc/notificationtargetticket.class.php +++ b/inc/notificationtargetticket.class.php @@ -1,47 +1,47 @@ -raiseevent; - if (isset($target->obj->fields['id'])) { - $tickets_id = $target->obj->fields['id']; - - $containers = PluginFieldsContainer::findContainers('Ticket', 'dom', '', $tickets_id); - - foreach ($containers as $c_id) { - - $container_obj = new PluginFieldsContainer(); - $container_obj->getFromDB($c_id); - - if (empty($container_obj->fields)) { - continue; - } - - $container_name = $container_obj->fields['name']; - $bloc_classname = PluginFieldsContainer::getClassname('Ticket', $container_name); - - if (class_exists($bloc_classname)) { - - $bloc_obj = new $bloc_classname(); - - $values = $bloc_obj->find([ - 'plugin_fields_containers_id' => $c_id, - 'items_id' => $tickets_id, - ]); - $values = array_shift($values); - - foreach ($values as $field_name => $value) { - if (in_array($field_name, ['id', 'items_id', 'itemtype', 'plugin_fields_containers_id', 'entities_id'])) { - continue; - } - $key = "##fields.{$container_name}.{$field_name}##"; - $target->data[$key] = is_array($value) ? implode(', ', $value) : $value; - } - } - } - } - } -} +raiseevent; + if (isset($target->obj->fields['id'])) { + $tickets_id = $target->obj->fields['id']; + + $containers = PluginFieldsContainer::findContainers('Ticket', 'dom', '', $tickets_id); + + foreach ($containers as $c_id) { + + $container_obj = new PluginFieldsContainer(); + $container_obj->getFromDB($c_id); + + if (empty($container_obj->fields)) { + continue; + } + + $container_name = $container_obj->fields['name']; + $bloc_classname = PluginFieldsContainer::getClassname('Ticket', $container_name); + + if (class_exists($bloc_classname)) { + + $bloc_obj = new $bloc_classname(); + + $values = $bloc_obj->find([ + 'plugin_fields_containers_id' => $c_id, + 'items_id' => $tickets_id, + ]); + $values = array_shift($values); + + foreach ($values as $field_name => $value) { + if (in_array($field_name, ['id', 'items_id', 'itemtype', 'plugin_fields_containers_id', 'entities_id'])) { + continue; + } + $key = "##fields.{$container_name}.{$field_name}##"; + $target->data[$key] = is_array($value) ? implode(', ', $value) : $value; + } + } + } + } + } +} diff --git a/setup.php b/setup.php index 8e1d5440..2f21917b 100644 --- a/setup.php +++ b/setup.php @@ -192,7 +192,7 @@ function plugin_init_fields() ]; $PLUGIN_HOOKS['item_get_datas']['fields'] = [ - NotificationTargetTicket::class => [PluginFieldsNotificationTargetTicket::class, 'addNotificationDatas'] + NotificationTargetTicket::class => [PluginFieldsNotificationTargetTicket::class, 'addNotificationDatas'], ]; } } From af2b0403c9aa09631a89089bcd43e0803d473216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Thu, 17 Apr 2025 15:14:47 +0200 Subject: [PATCH 09/11] update: container.class.php --- inc/container.class.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index 90ff408f..9097aa95 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1618,8 +1618,9 @@ public static function findContainers($itemtype, $type = 'tab', $subtype = '', $ ], [ 'is_recursive' => 0, - 'entities_id' => $entityId, - ] + ], + ], + ], ] ] ]; From 6dd8f769cb6421b4d8e60a69ed89c5f3289206c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Thu, 17 Apr 2025 15:18:31 +0200 Subject: [PATCH 10/11] fix: last change --- inc/container.class.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index 9097aa95..4569c031 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1621,8 +1621,6 @@ public static function findContainers($itemtype, $type = 'tab', $subtype = '', $ ], ], ], - ] - ] ]; if ($subtype !== '') { From 68edf397f11681d0c93d0154237fe29ccf899010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Thu, 17 Apr 2025 15:26:01 +0200 Subject: [PATCH 11/11] update for phpstan --- inc/container.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/container.class.php b/inc/container.class.php index 4569c031..5c77249f 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1634,7 +1634,7 @@ public static function findContainers($itemtype, $type = 'tab', $subtype = '', $ $where['type'] = $type; } - if (is_array($entityRestriction) && !empty($entityRestriction)) { + if (!empty($entityRestriction)) { $allowedEntities = []; foreach ($entityRestriction as $restriction) { if (isset($restriction['entities_id']) && is_array($restriction['entities_id'])) {