From 4fa60d0c95ce66ef3645858b13d59fccba5b1da8 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/12] [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 | 224 +++++++++++++------------ templates/fields.html.twig | 3 +- 3 files changed, 321 insertions(+), 236 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 4f3a4d92..0e675b08 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,105 +909,105 @@ 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) { @@ -1045,31 +1050,32 @@ function(evt) { } ); - 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); + 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); } - 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 b1a770f7..178214f3 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 9b7510769c82e0a052f5ef065efdc21cd0150b50 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/12] 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 8a80410a..ce5441d8 100644 --- a/front/container.form.php +++ b/front/container.form.php @@ -56,6 +56,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 58436b89dc5c52d74de5cd5b58d3db17a56d7ae5 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/12] 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 f15e0421cc63984254d91ca46eec0658178ab548 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 04/12] 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 0e675b08..c358e6a6 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 8b091b227971d090f776ff2ece28e0763e9225ae 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 05/12] 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 8a608a5325ec865f978e8ac4928faae07d477a27 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 06/12] 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 f3b5477f9205f90bcb94c228c66fac14268c0bd1 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 07/12] 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 ac84ac208bad5d75463ec15ce43683cd0238f20b 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 08/12] 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 c69f103861f69fddd2749972154969b6a4df16a5 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 09/12] 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'])) { From 3555e97eb3c2a0fcadbaf9038dd79c8a1dd8d35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Sun, 25 May 2025 14:40:16 +0200 Subject: [PATCH 10/12] update: findContainers method to use entityId instead of itemId update: showForTab method fix: dropdown field --- inc/container.class.php | 293 +++++++++++++++++-------------------- inc/field.class.php | 34 +---- templates/fields.html.twig | 4 +- 3 files changed, 143 insertions(+), 188 deletions(-) diff --git a/inc/container.class.php b/inc/container.class.php index 5c77249f..28d92335 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -1588,85 +1588,87 @@ public static function findContainer($itemtype, $type = 'tab', $subtype = '') return $id; } - public static function findContainers($itemtype, $type = 'tab', $subtype = '', $itemId = '') + /** + * Find containers for a specific itemtype, type, subtype and entity id + * + * @param string $itemtype Itemtype GLPI + * @param string $type Type of container (tab, dom, domtab) + * @param string $subtype + * @param integer $entityId Entity ID default is 0 (root entity) + * + * @return array List of container IDs + */ + public static function findContainers($itemtype, $type = 'tab', $subtype = '', $entityId = 0): array { /** @var DBmysql $DB */ global $DB; - $ids = []; - if (!empty($itemtype) && !empty($itemId) && class_exists($itemtype)) { - - $obj = new $itemtype(); - if ($obj->getFromDB($itemId)) { - - $entityId = $obj->fields['entities_id'] ?? 0; - $entityIds = getAncestorsOf("glpi_entities", $entityId); - $entityIds[] = $entityId; // Add entity obj itself to the list - $glpiActiveEntities = $_SESSION['glpiactiveentities'] ?? 0; + if ($itemtype === '') { + return []; + } - $entityRestriction = getEntitiesRestrictCriteria('', '', $glpiActiveEntities, true, true); + $entitiesIds = getAncestorsOf("glpi_entities", (string) $entityId); + $entitiesIds[] = $entityId; // Add entity active itself to the list - $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, - ], - ], + $where = [ + 'is_active' => 1, + 'type' => $type, + new \QueryExpression("JSON_CONTAINS(itemtypes, " . $DB->quote('"' . $itemtype . '"') . ")"), + 'AND' => [ + 'OR' => [ + [ + 'is_recursive' => 1, + 'entities_id' => $entitiesIds, ], - ]; + [ + 'is_recursive' => 0, + 'entities_id' => $entityId, + ], + ], + ], + ]; - if ($subtype !== '') { - if ($subtype === $itemtype . '$main') { - $where['type'] = 'dom'; - } else { - $where['type'] = ['!=', 'dom']; - $where['subtype'] = $subtype; - } - } else { - $where['type'] = $type; - } + if ($subtype !== '') { + if ($subtype === $itemtype . '$main') { + $where['type'] = 'dom'; + } else { + $where['type'] = ['!=', 'dom']; + $where['subtype'] = $subtype; + } + } else { + $where['type'] = $type; + } - if (!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)) { - $where['entities_id'] = $allowedEntities; - } + $entityRestriction = getEntitiesRestrictCriteria('', '', $entityId, true, true); + if (!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)) { + $where['entities_id'] = $allowedEntities; + } + } - $iterator = $DB->request([ - 'SELECT' => 'id', - 'FROM' => self::getTable(), - 'WHERE' => $where, - ]); - - foreach ($iterator as $row) { - $containerId = (int) $row['id']; - - //profiles restriction - if (isset($_SESSION['glpiactiveprofile']['id'])) { - $profileId = $_SESSION['glpiactiveprofile']['id']; - $right = PluginFieldsProfile::getRightOnContainer($profileId, $containerId); - if ($right < READ) { - continue; - } - } + $iterator = $DB->request([ + 'SELECT' => 'id', + 'FROM' => self::getTable(), + 'WHERE' => $where, + ]); - $ids[] = $containerId; + $ids = []; + foreach ($iterator as $row) { + $containerId = (int) $row['id']; + + if (isset($_SESSION['glpiactiveprofile']['id'])) { + $profileId = $_SESSION['glpiactiveprofile']['id']; + if (PluginFieldsProfile::getRightOnContainer($profileId, $containerId) < READ) { + continue; } } + $ids[] = $containerId; } return $ids; @@ -1741,35 +1743,29 @@ public static function preItemUpdate(CommonDBTM $item) */ public static function preItem(CommonDBTM $item) { - $type = 'dom'; - if (isset($_REQUEST['_plugin_fields_type'])) { - $type = $_REQUEST['_plugin_fields_type']; - } - $subtype = ''; - if ($type == 'domtab') { - $subtype = $_REQUEST['_plugin_fields_subtype']; - } + $type = $_REQUEST['_plugin_fields_type'] ?? 'dom'; + $subtype = ($type === 'domtab') ? ($_REQUEST['_plugin_fields_subtype'] ?? '') : ''; + + $itemEntityId = $item->getEntityID(); + $entityId = ($itemEntityId === -1) ? ($_SESSION['glpiactive_entity'] ?? 0) : $itemEntityId; - $containers = self::findContainers($item->getType(), $type, $subtype, $item->getID()); + $containers = self::findContainers($item->getType(), $type, $subtype, $entityId); $all_data = []; foreach ($containers as $c_id) { - $loc_c = new PluginFieldsContainer(); - $loc_c->getFromDB($c_id); - // check rights on $c_id - - if (isset($_SESSION['glpiactiveprofile']['id']) && $_SESSION['glpiactiveprofile']['id'] != null && $c_id > 0) { + if (isset($_SESSION['glpiactiveprofile']['id'])) { $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $c_id); - if ($right > READ === false) { // Si le droit est insuffisant, on passe au container suivant - continue; + if ($right < READ) { + continue; // insufficient rights } - } else { - continue; } + $loc_c = new self(); + $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']) { @@ -1781,11 +1777,12 @@ public static function preItem(CommonDBTM $item) } if ($item->isEntityAssign() && !in_array($item->getEntityID(), $entities)) { - continue; + continue; // not the right entity } if (false !== ($data = self::populateData($c_id, $item))) { - if (self::validateValues($data, $item->getType(), isset($_REQUEST['massiveaction'])) === false) { + if (!self::validateValues($data, $item->getType(), isset($_REQUEST['massiveaction']))) { + // if validation fails, we need to remove the data from the item input $item->input = []; return false; @@ -1828,13 +1825,8 @@ public 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]; - } + $statusField = PluginFieldsStatusOverride::getStatusFieldName($item->getType()); + $data[$statusField] = $item->input[$statusField] ?? $item->fields[$statusField] ?? null; $has_fields = false; // Prefix for input names @@ -1842,6 +1834,7 @@ public static function populateData($c_id, CommonDBTM $item) foreach ($fields as $field) { $base_name = $field['name']; + $isMulti = (bool) $field['multiple']; if ($field['type'] == 'glpi_item') { $itemtype_key = "itemtype_{$base_name}"; $items_id_key = "items_id_{$base_name}"; @@ -1850,9 +1843,9 @@ public static function populateData($c_id, CommonDBTM $item) 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; continue; // bypass unique field handling } @@ -1861,86 +1854,68 @@ public static function populateData($c_id, CommonDBTM $item) // "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"] = []; + $htmlKeyWithId = $prefix . $base_name . "dropdowns_id"; // html key in POST data with id + $htmlKeyNoId = "plugin_fields_{$base_name}dropdowns_id"; // html key in POST data without id + $colKey = 'plugin_fields_' . $base_name . 'dropdowns_id'; // column key in DB + + if (array_key_exists($htmlKeyWithId, $item->input)) { + $data[$colKey] = $item->input[$htmlKeyWithId]; + $has_fields = true; + } elseif (array_key_exists($htmlKeyNoId, $item->input)) { + $data[$colKey] = $item->input[$htmlKeyNoId]; + $has_fields = true; + } elseif ($isMulti) { + $definedKeyWithId = '_' . $htmlKeyWithId . '_defined'; + $definedKeyNoId = '_' . $htmlKeyNoId . '_defined'; + if (!empty($item->input[$definedKeyWithId]) || !empty($item->input[$definedKeyNoId])) { + $data[$colKey] = []; + $has_fields = true; } } 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]); + $htmlKeyWithId = $prefix . $base_name; + $htmlKeyNoId = "plugin_fields_{$base_name}"; + + $valuePresent = false; + $value = null; + if (array_key_exists($htmlKeyWithId, $item->input)) { + $value = $item->input[$htmlKeyWithId]; + $valuePresent = true; + } elseif (array_key_exists($htmlKeyNoId, $item->input)) { + $value = $item->input[$htmlKeyNoId]; + $valuePresent = true; + } elseif ($isMulti) { + $definedKeyWithId = '_' . $htmlKeyWithId . '_defined'; + $definedKeyNoId = '_' . $htmlKeyNoId . '_defined'; + if (!empty($item->input[$definedKeyWithId]) || !empty($item->input[$definedKeyNoId])) { + $value = []; + $valuePresent = true; } - $data[$base_name] = $item->input[$input]; + } - if ($field['type'] === 'richtext') { - $filename_input = "_" . $input; - $prefix_input = "_prefix_" . $input; - $tag_input = "_tag_" . $input; + if (!$valuePresent) { + continue; // not a valid input + } - $data[$filename_input] = $item->input[$filename_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 - // ex my_dom[] - //in these conditions, the input is never sent by the browser - if ($field['multiple']) { - //handle multi dropdown field - if ($field['type'] == 'dropdown') { - $multiple_key = $prefix . $base_name . "dropdowns_id"; - $multiple_defined = '_' . $multiple_key . '_defined'; - //values are defined by user - if (isset($item->input[$multiple_key])) { - $data[$base_name . "_dropdowns_id"] = $item->input[$multiple_key]; - $has_fields = true; - } elseif ( - isset($item->input[$multiple_defined]) - && $item->input[$multiple_defined] - ) { //multi dropdown is empty or has been emptied - $data[$base_name . "_dropdowns_id"] = []; - $has_fields = true; - } - } + if ($field['type'] === 'number') { + $value = str_replace(',', '.', $value); + } - //managed multi GLPI item dropdown field - if (preg_match('/^dropdown-(?.+)$/', $field['type'], $match) === 1) { - //values are defined by user - if (isset($item->input[$field['name']])) { - $data[$base_name] = $item->input[$field['name']]; - $has_fields = true; - } else { //multi dropdown is empty or has been emptied - $data[$base_name] = []; - } - } + $data[$base_name] = $value; + $has_fields = true; + + // If the field is a richtext + if ($field['type'] === 'richtext') { + foreach (['_' . $htmlKeyWithId, '_prefix_' . $htmlKeyWithId, '_tag_' . $htmlKeyWithId] as $extra) { + $data[$extra] = $item->input[$extra]; } } } - if ($has_fields === true) { - return $data; - } else { - return false; - } + return $has_fields ? $data : false; } public static function getAddSearchOptions($itemtype, $containers_id = false) diff --git a/inc/field.class.php b/inc/field.class.php index c358e6a6..539e26fd 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -909,12 +909,10 @@ public static function showForTab($params) $subtype = ''; } - if (!isset($item->fields['id'])) { - return; - } - $itemId = $item->fields['id']; + $itemEntityId = $item->getEntityID(); + $entityId = ($itemEntityId === -1) ? ($_SESSION['glpiactive_entity'] ?? 0) : $itemEntityId; - $container_ids = PluginFieldsContainer::findContainers(get_class($item), $type, $subtype, $itemId); + $container_ids = PluginFieldsContainer::findContainers(get_class($item), $type, $subtype, $entityId); foreach ($container_ids as $container_id) { @@ -923,21 +921,6 @@ public static function showForTab($params) continue; } - //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)) { - continue; - } - } - //parse REQUEST_URI if (!isset($_SERVER['REQUEST_URI'])) { continue; @@ -955,13 +938,9 @@ public static function showForTab($params) //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))) { - continue; - } - $html_id = 'plugin_fields_container_' . $container_id; - if (strpos($current_url, 'helpdesk.public.php') !== false) { + $in_helpdesk = (strpos($current_url, 'helpdesk.public.php') !== false); + if ($in_helpdesk) { echo "
"; echo "
"; $field_options = [ @@ -981,7 +960,8 @@ public static function showForTab($params) $field_options ?? [], ); } - if (strpos($current_url, 'helpdesk.public.php') !== false) { + + if ($in_helpdesk) { echo '
'; } echo '
'; diff --git a/templates/fields.html.twig b/templates/fields.html.twig index 178214f3..b02a3732 100644 --- a/templates/fields.html.twig +++ b/templates/fields.html.twig @@ -108,9 +108,9 @@ {% set dropdown_options = dropdown_options|merge({'entity_sons': true}) %} {% endif %} {% if "dropdowns_id" in name %} - {% set dropdown_itemtype = call("getItemtypeForForeignKeyField", [name]) %} + {% set dropdown_itemtype = call("getItemtypeForForeignKeyField", ['plugin_fields_' ~ field['name'] ~ 'dropdowns_id']) %} {% else %} - {% set dropdown_itemtype = call("PluginFieldsDropdown::getClassname", [name]) %} + {% set dropdown_itemtype = call("PluginFieldsDropdown::getClassname", [field['name']]) %} {% endif %} {% set name_fk = call("getForeignKeyFieldForItemType", [dropdown_itemtype]) %} {{ macros.dropdownField(dropdown_itemtype, name_fk, value, label, field_options|merge(dropdown_options)) }} From b093525920c64c323f7ae8dc563f6b57ac33e89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Sun, 25 May 2025 23:05:44 +0200 Subject: [PATCH 11/12] test(fields): add PHPUnit test suite for plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • New phpunit.xml and bootstrap for the Fields plugin • PluginFieldsContainerTest covers: - Creating / reading containers - Filtering containers by entity, subtype, recursion - Hooks: preItem, postItemAdd, preItemUpdate - populateData (single + multi-dropdown) - showForTab rendering and rights checks --- .gitignore | 1 + composer.json | 3 +- composer.lock | 1674 +++++++++++++++++++++++---- phpunit.xml | 8 + tests/PluginFieldsContainerTest.php | 731 ++++++++++++ tests/bootstrap.php | 30 + 6 files changed, 2236 insertions(+), 211 deletions(-) create mode 100644 phpunit.xml create mode 100644 tests/PluginFieldsContainerTest.php create mode 100644 tests/bootstrap.php diff --git a/.gitignore b/.gitignore index 283db0af..2e847868 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist/ vendor/ .gh_token *.min.* +*.cache diff --git a/composer.json b/composer.json index 4a460f31..e7e8aee1 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "php-parallel-lint/php-parallel-lint": "^1.4", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.1", - "phpstan/phpstan-deprecation-rules": "^2.0" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, "config": { "optimize-autoloader": true, diff --git a/composer.lock b/composer.lock index e63266fb..65760869 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "edee4b40b4a23f0b5146a771fc5e5322", + "content-hash": "aa2b1621d126bd003fdb3efcc09d4d24", "packages": [ { "name": "symfony/deprecation-contracts", @@ -75,7 +75,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -134,7 +134,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -519,6 +519,76 @@ ], "time": "2024-05-06T16:37:16+00:00" }, + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, { "name": "evenement/evenement", "version": "v3.0.2", @@ -842,6 +912,242 @@ }, "time": "2024-09-18T06:58:02+00:00" }, + { + "name": "myclabs/deep-copy", + "version": "1.13.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-04-29T12:36:36+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, { "name": "php-parallel-lint/php-parallel-lint", "version": "v1.4.0", @@ -953,16 +1259,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.13", + "version": "2.1.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e55e03e6d4ac49cd1240907e5b08e5cd378572a9" + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e55e03e6d4ac49cd1240907e5b08e5cd378572a9", - "reference": "e55e03e6d4ac49cd1240907e5b08e5cd378572a9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8ca5f79a8f63c49b2359065832a654e1ec70ac30", + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30", "shasum": "" }, "require": { @@ -1007,7 +1313,7 @@ "type": "github" } ], - "time": "2025-04-27T12:28:25+00:00" + "time": "2025-03-24T13:45:00+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -1052,9 +1358,9 @@ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.2" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.1" }, - "time": "2025-04-26T19:59:57+00:00" + "time": "2024-11-28T21:56:36+00:00" }, { "name": "psr/container", @@ -1482,279 +1788,1185 @@ "homepage": "https://cboden.dev/" } ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", - "keywords": [ - "asynchronous", - "event-loop" - ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2023-11-13T13:48:05+00:00" + "time": "2020-10-26T13:14:26+00:00" }, { - "name": "react/promise", - "version": "v3.2.0", + "name": "sebastian/recursion-context", + "version": "4.0.5", "source": { "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { - "php": ">=7.1.0" + "php": ">=7.3" }, "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" + "phpunit/phpunit": "^9.3" }, "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\": "src/" + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" }, { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2024-05-24T10:39:05+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { - "name": "react/socket", - "version": "v1.16.0", + "name": "sebastian/resource-operations", + "version": "3.0.4", "source": { "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/dns": "^1.13", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.6 || ^1.2.1", - "react/stream": "^1.4" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3.3 || ^2", - "react/promise-stream": "^1.4", - "react/promise-timer": "^1.11" + "phpunit/phpunit": "^9.0" }, "type": "library", - "autoload": { - "psr-4": { - "React\\Socket\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", - "keywords": [ - "Connection", - "Socket", - "async", - "reactphp", - "stream" - ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.16.0" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2024-07-26T10:38:09+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { - "name": "react/stream", - "version": "v1.4.0", + "name": "sebastian/type", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.8", - "react/event-loop": "^1.2" + "php": ">=7.3" }, "require-dev": { - "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + "phpunit/phpunit": "^9.5" }, "type": "library", - "autoload": { - "psr-4": { - "React\\Stream\\": "src/" + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", - "keywords": [ - "event-driven", - "io", - "non-blocking", - "pipe", - "reactphp", - "readable", - "stream", - "writable" - ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.4.0" + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "time": "2024-06-11T12:45:25+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { - "name": "sebastian/diff", - "version": "4.0.6", + "name": "sebastian/version", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { "php": ">=7.3" }, - "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" - }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1769,24 +2981,15 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" }, "funding": [ { @@ -1794,7 +2997,7 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2020-09-28T06:39:44+00:00" }, { "name": "symfony/console", @@ -2260,7 +3463,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -2318,7 +3521,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -2338,7 +3541,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -2399,7 +3602,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -2419,19 +3622,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -2479,7 +3683,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -2495,11 +3699,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -2555,7 +3759,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0" }, "funding": [ { @@ -2575,16 +3779,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -2635,7 +3839,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -2651,11 +3855,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -2711,7 +3915,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" }, "funding": [ { @@ -3022,6 +4226,56 @@ ], "time": "2024-11-10T20:33:58+00:00" }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, { "name": "twig/twig", "version": "v3.11.3", @@ -3105,15 +4359,15 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=7.4" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.4.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..9e9e7e21 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,8 @@ + + + + ./tests/ + + \ No newline at end of file diff --git a/tests/PluginFieldsContainerTest.php b/tests/PluginFieldsContainerTest.php new file mode 100644 index 00000000..087e0eaa --- /dev/null +++ b/tests/PluginFieldsContainerTest.php @@ -0,0 +1,731 @@ +deleteAllContainers(); + + // Delete all created tickets + foreach ($this->createdTickets as $ticketId) { + $ticket = new Ticket(); + $ticket->delete(['id' => $ticketId]); + } + } + + /** + * * Delete all created containers and their associated fields and profiles + */ + private function deleteAllContainers(): void + { + // Delete all created containers + foreach ($this->createdContainers as $containerId) { + $fieldsProfile = new PluginFieldsProfile(); + $fieldsObj = new PluginFieldsField(); + $fieldscontainer = new PluginFieldsContainer(); + + $fieldsProfile->deleteByCriteria(['plugin_fields_containers_id' => $containerId]); + $fieldsObj->deleteByCriteria(['plugin_fields_containers_id' => $containerId]); + $fieldscontainer->delete(['id' => $containerId]); + } + + $this->createdContainers = []; + $this->createdTickets = []; + } + + /** + * * Add container + * @param array $input + * @return false|int + */ + private function addContainer(array $input): false|int + { + $container = new PluginFieldsContainer(); + $input['is_active'] = 1; + $containerId = $container->add($input, [], false); + if (is_int($containerId) && $containerId > 0) { + $this->createdContainers[] = $containerId; + } + return $containerId; + } + + /** + * * Get container by ID + * @param int $id + * @return PluginFieldsContainer + */ + private function getContainer(int $id): PluginFieldsContainer + { + $container = new PluginFieldsContainer(); + $container->getFromDB($id); + return $container; + } + + /** + * * Add field to container + * @param int $containerId + * @param string $fieldName + * @param string $type + * @return false|int + */ + private function addFieldToContainer(int $containerId, string $fieldName, string $type = 'text', bool $multiple = false): false|int + { + $field = new PluginFieldsField(); + $id = $field->add([ + 'name' => $fieldName, + 'label' => ucfirst($fieldName), + 'type' => $type, + 'plugin_fields_containers_id' => $containerId, + 'ranking' => 1, + 'default_value' => '', + 'is_active' => 1, + 'is_readonly' => 0, + 'mandatory' => 1, + 'multiple' => $multiple ? 1 : 0, + 'allowed_values' => null, + ]); + + $container = new PluginFieldsContainer(); + $container->getFromDB($containerId); + $className = PluginFieldsContainer::getClassname('Ticket', $container->fields['name']); + $className::addField($fieldName, $type, ['multiple' => $multiple]); + + return $id; + } + + /** + * * Add ticket + * @param array $input + * @return Ticket|false + */ + private function addTicket(array $input): Ticket|false + { + $ticket = new Ticket(); + $ticketId = $ticket->add($input); + if (is_int($ticketId)) { + $this->createdTickets[] = $ticketId; + } else { + return false; + } + return $this->getTicket($ticketId); + } + + /** + * * Get ticket by ID + * @param int $id + * @return Ticket + */ + private function getTicket(int $id): Ticket + { + $ticket = new Ticket(); + $ticket->getFromDB($id); + return $ticket; + } + + /** + * * Test adding and reading a container + */ + public function testAddAndReadContainer(): void + { + $containerOne = $this->addContainer([ + 'name' => 'testcontainerone', + 'label' => 'Test container 1', + 'itemtypes' => ['Computer', 'Ticket'], + 'type' => 'tab', + 'subtype' => null, + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->assertNotFalse($containerOne); + $this->assertIsInt($containerOne); + + $containerOne = $this->getContainer($containerOne); + + $this->assertSame('Test container 1', $containerOne->fields['label']); + $this->assertStringContainsString('Computer', $containerOne->fields['itemtypes']); + $this->assertStringContainsString('Ticket', $containerOne->fields['itemtypes']); + + $this->deleteAllContainers(); + } + + /** + * * Test find containers returns correct containers for given itemtype and entity + */ + public function testFindContainersReturnsCorrectContainersForGivenItemtypeAndEntity() + { + // Container A should be found + $idExactMatchContainer = $this->addContainer([ + 'name' => 'containerca', + 'label' => 'Container CA', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + // Container B should be found (recursive and same entity) + $idRecursiveParentContainer = $this->addContainer([ + 'name' => 'containercb', + 'label' => 'Container CB', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + // Container C should not be found (wrong entity) + $idWrongEntityContainer = $this->addContainer([ + 'name' => 'containercc', + 'label' => 'Container CC', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 1, + 'is_recursive' => 0, + ]); + + // Container D should not be found (recursive but wrong entity) + $idRecursiveWrongEntityContainer = $this->addContainer([ + 'name' => 'containercd', + 'label' => 'Container CD', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 1, + 'is_recursive' => 1, + ]); + + // Container E should not be found (wrong itemtype) + $idWrongItemtypeContainer = $this->addContainer([ + 'name' => 'containerce', + 'label' => 'Container CE', + 'itemtypes' => ['Computer'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + // Container F should be found (multiple itemtypes) + $idMultipleItemtypesContainer = $this->addContainer([ + 'name' => 'containercf', + 'label' => 'Container CF', + 'itemtypes' => ['Computer', 'Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + $containersIds = PluginFieldsContainer::findContainers('Ticket', 'dom', '', 0); + + $this->assertContains($idExactMatchContainer, $containersIds, 'Container A should be found'); + $this->assertContains($idRecursiveParentContainer, $containersIds, 'Container B should be found (recursive and same entity)'); + $this->assertContains($idMultipleItemtypesContainer, $containersIds, 'Container F should be found (multiple itemtypes)'); + + $this->assertNotContains($idWrongEntityContainer, $containersIds, 'Container C should not be found (wrong entity)'); + $this->assertNotContains($idRecursiveWrongEntityContainer, $containersIds, 'Container D should not be found (recursive but wrong entity)'); + $this->assertNotContains($idWrongItemtypeContainer, $containersIds, 'Container E should not be found (wrong itemtype)'); + + $this->deleteAllContainers(); + } + + public function testFindContainersWithSubtypeInExpectedFormat(): void + { + // Container with a subtype (itemtype "Entity", type "domtab" and subtype "Entity$1") should be found + $idValidSubtypeContainer = $this->addContainer([ + 'name' => 'containerentitytabone', + 'label' => 'Container Entity Tab 1', + 'itemtypes' => ['Entity'], + 'type' => 'domtab', + 'subtype' => 'Entity$1', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + // Container with a another subtype (itemtype "Entity", type "domtab" and subtype "Entity$2") should not be found + $idDifferentSubtypeContainer = $this->addContainer([ + 'name' => 'containerentitytabtwo', + 'label' => 'Container Entity Tab 2', + 'itemtypes' => ['Entity'], + 'type' => 'domtab', + 'subtype' => 'Entity$2', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + // Same subtype but itemtype is "Ticket" => should not be found + $idWrongItemtypeWithSubtype = $this->addContainer([ + 'name' => 'containerwrongitemtype', + 'label' => 'Container Wrong itemType', + 'itemtypes' => ['Ticket'], + 'type' => 'domtab', + 'subtype' => 'Entity$1', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + // Same subtype and itemtype, but wrong type "dom" => should not be found + $idWrongTypeWithValidSubtype = $this->addContainer([ + 'name' => 'containerwrongType', + 'label' => 'Container Wrong Type', + 'itemtypes' => ['Entity'], + 'type' => 'dom', + 'subtype' => 'Entity$1', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + $containersIds = PluginFieldsContainer::findContainers('Entity', 'domtab', 'Entity$1', 0); + + $this->assertIsArray($containersIds); + $this->assertNotEmpty($containersIds); + + $this->assertContains($idValidSubtypeContainer, $containersIds, 'Container with a subtype (itemtype "Entity", type "domtab" and subtype "Entity$1") should be found'); + + $this->assertNotContains($idDifferentSubtypeContainer, $containersIds, 'Container with a another subtype (itemtype "Entity", type "domtab" and subtype "Entity$2") should not be found'); + $this->assertNotContains($idWrongItemtypeWithSubtype, $containersIds, 'Same subtype but itemtype is "Ticket" => should not be found'); + $this->assertNotContains($idWrongTypeWithValidSubtype, $containersIds, 'Same subtype and itemtype, but wrong type "dom" => should not be found'); + + $this->deleteAllContainers(); + } + + public function testFindContainersConsidersRecursiveEntitiesFromChild(): void + { + // Container defined in parent entity (0) with recursion => should be visible from child + $idContainerParentRecursive = $this->addContainer([ + 'name' => 'containerparentrecursive', + 'label' => 'Container parent recursive', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'subtype' => null, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + // Container defined in parent entity (0) without recursion => should not be visible from child + $idContainerParentNotRecursive = $this->addContainer([ + 'name' => 'containerparentnotrecursive', + 'label' => 'Container parent not recursive', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'subtype' => null, + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + $containersIds = PluginFieldsContainer::findContainers('Ticket', 'dom', '', 1); + + $this->assertIsArray($containersIds); + $this->assertNotEmpty($containersIds); + + $this->assertContains($idContainerParentRecursive, $containersIds, 'Container defined in parent entity (0) with recursion => should be visible from child'); + $this->assertNotContains($idContainerParentNotRecursive, $containersIds, 'Container defined in parent entity (0) without recursion => should not be visible from child'); + $this->deleteAllContainers(); + } + + public function testParentCannotSeeChildRecursiveContainers(): void + { + // Container defined in child (1), recursive + $idContainerChildRecursive = $this->addContainer([ + 'name' => 'containerchildrecursive', + 'label' => 'Container child recursive', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'subtype' => null, + 'entities_id' => 1, + 'is_recursive' => 1, + ]); + + $containersIds = PluginFieldsContainer::findContainers('Ticket', 'dom', '', 0); + + $this->assertNotContains($idContainerChildRecursive, $containersIds, 'Container defined in child (1), recursive, should not be visible from parent (0)'); + $this->deleteAllContainers(); + } + + public function testPreItemHandlesMultipleValidContainers(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; + + $id1 = $this->addContainer([ + 'name' => 'container1', + 'label' => 'Container 1', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($id1, 'testfield1'); + + $id2 = $this->addContainer([ + 'name' => 'container2', + 'label' => 'Container 2', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + $this->addFieldToContainer($id2, 'testfield2'); + + $ticket = new Ticket(); + $ticket->input = [ + 'name' => 'Test ticket', + 'content' => 'Test content', + 'entities_id' => 0, + 'plugin_fields_' . $id1 . '_testfield1' => 'foo', + 'plugin_fields_' . $id2 . '_testfield2' => 'bar', + 'status' => 1, + ]; + + $preItemReturn = PluginFieldsContainer::preItem($ticket); + + $this->assertTrue($preItemReturn); + $this->assertArrayHasKey('_plugin_fields_data_multi', $ticket->input); + $this->assertCount(2, $ticket->input['_plugin_fields_data_multi']); + + $data = $ticket->input['_plugin_fields_data_multi']; + + $this->assertSame('foo', $data[0]['testfield1']); + $this->assertSame('bar', $data[1]['testfield2']); + $this->deleteAllContainers(); + } + + public function testPostItemAddHandlesMultipleContainers(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; + $_REQUEST['massiveaction'] = false; + + // add containers + fields + $containerId1 = $this->addContainer([ + 'name' => 'containerpostitemaddone', + 'label' => 'Container postItemAdd 1', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId1, 'field1'); + + $containerId2 = $this->addContainer([ + 'name' => 'containerpostitemaddtwo', + 'label' => 'Container postItemAdd 2', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId2, 'field2'); + + require_once GLPI_ROOT . "/files/_plugins/fields/inc/ticketcontainerpostitemaddone.class.php"; + require_once GLPI_ROOT . "/files/_plugins/fields/inc/ticketcontainerpostitemaddtwo.class.php"; + + // add a ticket + $ticket = $this->addTicket([ + 'name' => 'Test ticket postItemAdd', + 'content' => 'test content', + 'entities_id' => 0, + ]); + + // 3. Injecter les données comme si elles venaient du formulaire + $ticket->input += [ + '_plugin_fields_data_multi' => [ + [ + 'plugin_fields_containers_id' => $containerId1, + 'field1' => 'value1', + ], + [ + 'plugin_fields_containers_id' => $containerId2, + 'field2' => 'value2', + ], + ], + ]; + + // check if postItemAdd return true + $this->assertTrue(PluginFieldsContainer::postItemAdd($ticket)); + + // check if the data is correctly saved in database + $className1 = PluginFieldsContainer::getClassname('Ticket', 'containerpostitemaddone'); + $objClass1 = new $className1(); + $objClass1->getFromDBByCrit([ + 'items_id' => $ticket->getID(), + 'plugin_fields_containers_id' => $containerId1, + ]); + $this->assertEquals('value1', $objClass1->fields['field1']); + + $className2 = PluginFieldsContainer::getClassname('Ticket', 'containerpostitemaddtwo'); + $objClass2 = new $className2(); + $objClass2->getFromDBByCrit([ + 'items_id' => $ticket->getID(), + 'plugin_fields_containers_id' => $containerId2, + ]); + $this->assertEquals('value2', $objClass2->fields['field2']); + $this->deleteAllContainers(); + } + + public function testPreItemUpdateHandlesMultipleContainers(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; + $_REQUEST['massiveaction'] = false; + + // add containers + fields + $containerId1 = $this->addContainer([ + 'name' => 'containerupdateone', + 'label' => 'Container update 1', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId1, 'field1'); + + $containerId2 = $this->addContainer([ + 'name' => 'containerupdatetwo', + 'label' => 'Container update 2', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId2, 'field2'); + + require_once GLPI_ROOT . "/files/_plugins/fields/inc/ticketcontainerupdateone.class.php"; + require_once GLPI_ROOT . "/files/_plugins/fields/inc/ticketcontainerupdatetwo.class.php"; + + // add a ticket + $ticket = $this->addTicket([ + 'name' => 'Ticket to update', + 'content' => 'test content', + 'entities_id' => 0, + ]); + + $ticket->input['_plugin_fields_data_multi'] = [ + [ + 'plugin_fields_containers_id' => $containerId1, + 'field1' => 'value1', + ], + [ + 'plugin_fields_containers_id' => $containerId2, + 'field2' => 'value2', + ], + ]; + PluginFieldsContainer::postItemAdd($ticket); + + // update the fields + $ticket->input += [ + 'plugin_fields_' . $containerId1 . '_field1' => 'value1 updated', + 'plugin_fields_' . $containerId2 . '_field2' => 'value2 updated', + ]; + PluginFieldsContainer::preItemUpdate($ticket); + + // check if the data is correctly updated in database + $className1 = PluginFieldsContainer::getClassname('Ticket', 'containerupdateone'); + $valueField1 = (new $className1())->find(['items_id' => $ticket->getID(), 'plugin_fields_containers_id' => $containerId1]); + $this->assertSame('value1 updated', current($valueField1)['field1']); + + $className2 = PluginFieldsContainer::getClassname('Ticket', 'containerupdatetwo'); + $valueField2 = (new $className2())->find(['items_id' => $ticket->getID(), 'plugin_fields_containers_id' => $containerId2]); + $this->assertSame('value2 updated', current($valueField2)['field2']); + $this->deleteAllContainers(); + } + + public function testPopulateDataWithPrefix(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; + + // add a container with a field + $containerId = $this->addContainer([ + 'name' => 'containerpopulate', + 'label' => 'Container populate', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId, 'field1'); + + // add a ticket with input data + $ticket = new Ticket(); + $ticket->fields['id'] = 123; + $ticket->fields['entities_id'] = 0; + $ticket->input = [ + 'plugin_fields_' . $containerId . '_field1' => 'hello world', + ]; + + // call populateData + $data = PluginFieldsContainer::populateData($containerId, $ticket); + + // check fields are populated without prefix + $this->assertIsArray($data); + $this->assertSame(123, $data['items_id']); + $this->assertSame('Ticket', $data['itemtype']); + $this->assertSame(0, $data['entities_id']); + $this->assertSame('hello world', $data['field1']); + $this->deleteAllContainers(); + } + + public function testPopulateDataWithMultipleSelectionFields(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; + + // add a container with a dropdown field that allows multiple selections + $containerId = $this->addContainer([ + 'name' => 'containermulti', + 'label' => 'Container Multi', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + + $fieldName = 'comboonemulti'; + $this->addFieldToContainer($containerId, $fieldName, 'dropdown', true); + + // add options to the dropdown field + $dropdownClass = PluginFieldsDropdown::getClassname($fieldName); + $dropdown = new $dropdownClass(); + $idOptA = $dropdown->add(['name' => 'Option A']); + $idOptB = $dropdown->add(['name' => 'Option B']); + + // add a ticket with input data + $ticket = new Ticket(); + $ticket->getEmpty(); + $ticket->fields['id'] = 1001; + $ticket->fields['entities_id'] = 0; + + $ticket->input = [ + "plugin_fields_{$containerId}_{$fieldName}dropdowns_id" => [$idOptA, $idOptB], + "_plugin_fields_{$containerId}_{$fieldName}dropdowns_id_defined" => true, + ]; + + // call populateData + $data = PluginFieldsContainer::populateData($containerId, $ticket); + $this->assertIsArray($data); + + $col = "plugin_fields_{$fieldName}dropdowns_id"; + $this->assertArrayHasKey($col, $data); + $this->assertSame([$idOptA, $idOptB], $data[$col]); + $this->deleteAllContainers(); + } + + public function testShowForTabDisplaysMultipleContainers(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SESSION['glpiactiveprofile']['id'] = 4; // profil with right to read fields + $_SERVER['REQUEST_URI'] = '/front/ticket.form.php'; + $_SESSION['glpi_tabs']['ticket'] = 'ticket$main'; + + // add two containers with fields + $idContainer1 = $this->addContainer([ + 'name' => 'containeroneone', + 'label' => 'Container 11', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($idContainer1, 'field1'); + + $idContainer2 = $this->addContainer([ + 'name' => 'containertwotwo', + 'label' => 'Container 22', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($idContainer2, 'field2'); + + // add a ticket to test + $ticket = $this->addTicket([ + 'name' => 'ShowForTab test', + 'content' => 'dummy', + 'entities_id' => 0, + ]); + + // capture the output of showForTab + ob_start(); + PluginFieldsField::showForTab(['item' => $ticket, 'options' => []]); + $html = ob_get_clean(); + + // check if the HTML contains both containers + $this->assertStringContainsString( + 'id=\'plugin_fields_container_'.$idContainer1.'\'', + $html, + "Container 1 (id=$idContainer1) not displayed" + ); + $this->assertStringContainsString( + 'id=\'plugin_fields_container_'.$idContainer2.'\'', + $html, + "Container 2 (id=$idContainer2) not displayed" + ); + $this->deleteAllContainers(); + } + + public function testShowForTabRightsAreEnforced(): void + { + $_SESSION['glpiactive_entity'] = 0; + $_SERVER['REQUEST_URI'] = '/front/ticket.form.php'; + $_SESSION['glpi_tabs']['ticket'] = -1; + + // add a container with a field + $containerId = $this->addContainer([ + 'name' => 'rightTest', + 'label' => 'Container Right Test', + 'itemtypes' => ['Ticket'], + 'type' => 'dom', + 'entities_id' => 0, + 'is_recursive' => 0, + ]); + $this->addFieldToContainer($containerId, 'visiblefield'); + + // set profiles + $profileWithRight = 4; + $profileNoRight = -1; + + // add a ticket + $ticket = new Ticket(); + $ticket->getEmpty(); + $ticket->fields['id'] = 42; + $ticket->fields['entities_id'] = 0; + + // case 1 : profile with right + $_SESSION['glpiactiveprofile']['id'] = $profileWithRight; + + ob_start(); + PluginFieldsField::showForTab(['item' => $ticket]); + $htmlWithRight = trim(ob_get_clean()); + + $this->assertNotSame( + '', + $htmlWithRight, + 'Container should be visible for a profile with right.' + ); + // end case 1 + + // case 2 : profile without right + $_SESSION['glpiactiveprofile']['id'] = $profileNoRight; + + ob_start(); + PluginFieldsField::showForTab(['item' => $ticket]); + $htmlNoRight = trim(ob_get_clean()); + + $this->assertSame( + '', + $htmlNoRight, + 'Container should not be visible for a profile without right.' + ); + // end case 2 + $this->deleteAllContainers(); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..6b9d7c13 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,30 @@ +checkPluginState('fields'); +$plugin->getFromDBbyDir('fields'); + +if (!plugin_fields_check_prerequisites()) { + echo "\nPrerequisites are not met!"; + die(1); +} + +if (!$plugin->isInstalled('fields')) { + $plugin->install($plugin->getID()); +} +if (!$plugin->isActivated('fields')) { + $plugin->activate($plugin->getID()); +} From cf0f71f0a372dedaacfa6c5aaba4c0e65a229066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Mercier?= Date: Sun, 25 May 2025 23:38:43 +0200 Subject: [PATCH 12/12] composer lock generate --- composer.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/composer.lock b/composer.lock index 65760869..91685089 100644 --- a/composer.lock +++ b/composer.lock @@ -858,16 +858,16 @@ }, { "name": "glpi-project/tools", - "version": "0.7.4", + "version": "0.7.5", "source": { "type": "git", "url": "https://github.com/glpi-project/tools.git", - "reference": "65a09a93350da6fa67d423dd94e4cb4023a17e20" + "reference": "c6ff4a7640384232ead150b46d4a647a14d12ab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/glpi-project/tools/zipball/65a09a93350da6fa67d423dd94e4cb4023a17e20", - "reference": "65a09a93350da6fa67d423dd94e4cb4023a17e20", + "url": "https://api.github.com/repos/glpi-project/tools/zipball/c6ff4a7640384232ead150b46d4a647a14d12ab3", + "reference": "c6ff4a7640384232ead150b46d4a647a14d12ab3", "shasum": "" }, "require": { @@ -910,7 +910,7 @@ "issues": "https://github.com/glpi-project/tools/issues", "source": "https://github.com/glpi-project/tools" }, - "time": "2024-09-18T06:58:02+00:00" + "time": "2025-05-22T07:31:28+00:00" }, { "name": "myclabs/deep-copy", @@ -1317,21 +1317,21 @@ }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "9d8e7d4e32711715ad78a1fb6ec368df9af01fdf" + "reference": "468e02c9176891cc901143da118f09dc9505fc2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/9d8e7d4e32711715ad78a1fb6ec368df9af01fdf", - "reference": "9d8e7d4e32711715ad78a1fb6ec368df9af01fdf", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.13" + "phpstan/phpstan": "^2.1.15" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2",