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.lock b/composer.lock index 24927189..1f356451 100644 --- a/composer.lock +++ b/composer.lock @@ -954,16 +954,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.29", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan-phar-composer-source.git", - "reference": "git" - }, + "version": "2.1.30", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d618573eed4a1b6b75e37b2e0b65ac65c885d88e", - "reference": "d618573eed4a1b6b75e37b2e0b65ac65c885d88e", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a4a7f159927983dd4f7c8020ed227d80b7f39d7d", + "reference": "a4a7f159927983dd4f7c8020ed227d80b7f39d7d", "shasum": "" }, "require": { @@ -1008,7 +1003,7 @@ "type": "github" } ], - "time": "2025-09-25T06:58:18+00:00" + "time": "2025-10-02T16:07:52+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -1738,29 +1733,29 @@ }, { "name": "sebastian/diff", - "version": "6.0.2", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^11.0", + "phpunit/phpunit": "^9.3", "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1792,8 +1787,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -1801,7 +1795,7 @@ "type": "github" } ], - "time": "2024-07-03T04:53:05+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "symfony/console", diff --git a/front/container.form.php b/front/container.form.php index 68968f4d..02827abe 100644 --- a/front/container.form.php +++ b/front/container.form.php @@ -57,6 +57,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(); diff --git a/inc/container.class.php b/inc/container.class.php index af804251..3c48ce0e 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -631,22 +631,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']]); @@ -1100,7 +1084,7 @@ public static function getItemtypes($is_domtab) foreach ($all_itemtypes as $section => $itemtypes) { $all_itemtypes[$section] = array_filter( $itemtypes, - fn($itemtype) => count(self::getSubtypes($itemtype)) > 0, + fn ($itemtype) => count(self::getSubtypes($itemtype)) > 0, ARRAY_FILTER_USE_KEY, ); } @@ -1706,6 +1690,92 @@ public static function findContainer($itemtype, $type = 'tab', $subtype = '') return $id; } + /** + * 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; + + if ($itemtype === '') { + return []; + } + + $entitiesIds = getAncestorsOf("glpi_entities", (int) $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' => $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; + } + + $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, + ]); + + $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; + } + /** * Post item hook for add * Do store data in db @@ -1716,18 +1786,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; @@ -1744,23 +1816,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; } @@ -1774,68 +1845,57 @@ 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']; - } - // tries for 'tab' - if (false === ($c_id = self::findContainer(get_Class($item), $type, $subtype)) && false === $c_id = self::findContainer(get_Class($item))) { - return false; - } - } + $type = $_REQUEST['_plugin_fields_type'] ?? 'dom'; + $subtype = ($type === 'domtab') ? ($_REQUEST['_plugin_fields_subtype'] ?? '') : ''; - $loc_c = new PluginFieldsContainer(); - $loc_c->getFromDB($c_id); + $itemEntityId = $item->getEntityID(); + $entityId = ($itemEntityId === -1) ? ($_SESSION['glpiactive_entity'] ?? 0) : $itemEntityId; - // check rights on $c_id + $containers = self::findContainers($item->getType(), $type, $subtype, $entityId); - 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; - } + $all_data = []; + foreach ($containers as $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 (isset($_SESSION['glpiactiveprofile']['id'])) { + $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $c_id); + if ($right < READ) { + continue; // insufficient rights + } + } - if (count($item->fields) === 0) { - $item->fields = $item->input; - } + $loc_c = new self(); + $loc_c->getFromDB($c_id); - 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; // not the right entity } - $item->input['_plugin_fields_data'] = $data; + if (false !== ($data = self::populateData($c_id, $item))) { + 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 true; + return false; + } + $all_data[] = $data; + } } - return false; + $item->input['_plugin_fields_data_multi'] = $all_data; + + return true; } /** @@ -1846,7 +1906,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(); @@ -1859,105 +1919,105 @@ 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()); - $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 + $prefix = "plugin_fields_{$c_id}_"; + foreach ($fields as $field) { + $base_name = $field['name']; + $isMulti = (bool) $field['multiple']; 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]; + $data[$itemtype_key] = $item->input[$itemtype_key]; + $data[$items_id_key] = $item->input[$items_id_key]; + $has_fields = true; 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'; - } - 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]; - if ($field['type'] === 'richtext') { - $filename_input = sprintf('_%s', $input); - $prefix_input = sprintf('_prefix_%s', $input); - $tag_input = sprintf('_tag_%s', $input); - - $data[$filename_input] = $item->input[$filename_input] ?? []; - $data[$prefix_input] = $item->input[$prefix_input] ?? []; - $data[$tag_input] = $item->input[$tag_input] ?? []; - } - } elseif ($field['multiple']) { - //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 - $data['multiple_dropdown_action'] = $_POST['multiple_dropdown_action'] ?? 'assign'; - //handle multi dropdown field - if ($field['type'] == 'dropdown') { - $multiple_key = sprintf('plugin_fields_%sdropdowns_id', $field['name']); - $multiple_key_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; - } elseif ( - isset($item->input[$multiple_key_defined]) - && $item->input[$multiple_key_defined] - ) { //multi dropdown is empty or has been emptied - $data[$multiple_key] = []; - $has_fields = true; - } elseif (isset($_REQUEST['massiveaction'])) { // called from massiveaction - if (isset($_POST[$multiple_key])) { - $data[$multiple_key] = $_POST[$multiple_key]; - $has_fields = true; - } + // 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" + $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; } } - //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[$field['name']] = $item->input[$field['name']]; - $has_fields = true; - } else { //multi dropdown is empty or has been emptied - $data[$field['name']] = []; - } + continue; + } + + // For fields standard, the input name is "plugin_fields_{$c_id}_{$base_name}" + $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; } } - } - if ($has_fields === true) { - return $data; - } else { - return false; + if (!$valuePresent) { + continue; // not a valid input + } + + if ($field['type'] === 'number') { + $value = str_replace(',', '.', $value); + } + + $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]; + } + } } + + 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 0257413d..33d3ba7b 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -863,8 +863,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); } @@ -899,84 +904,82 @@ 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)) { - return; - } + $itemEntityId = $item->getEntityID(); + $entityId = ($itemEntityId === -1) ? ($_SESSION['glpiactive_entity'] ?? 0) : $itemEntityId; - $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $c_id); - if ($right < READ) { - return; - } + $container_ids = PluginFieldsContainer::findContainers(get_class($item), $type, $subtype, $entityId); - Html::requireJs('tinymce'); + foreach ($container_ids as $container_id) { - //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']); - } + $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $container_id); + if ($right < READ) { + continue; + } - if ($item->isEntityAssign()) { - $current_entity = $item->getEntityID(); - if (!in_array($current_entity, $entities)) { - return; + //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']); } - } - //parse REQUEST_URI - if (!isset($_SERVER['REQUEST_URI'])) { - return; - } - $current_url = $_SERVER['REQUEST_URI']; - if ( - !str_contains($current_url, '.form.php') - && !str_contains($current_url, '.injector.php') - && !str_contains($current_url, '.public.php') - && !str_contains($current_url, 'ajax/planning') - && !str_contains($current_url, 'ajax/timeline.php') // ITILSolution load from timeline - ) { - return; - } + if ($item->isEntityAssign()) { + $current_entity = $item->getEntityID(); + if (!in_array($current_entity, $entities)) { + continue; + } + } - //Retrieve dom container - $itemtypes = PluginFieldsContainer::getUsedItemtypes($type, true); + //parse REQUEST_URI + if (!isset($_SERVER['REQUEST_URI'])) { + continue; + } + $current_url = $_SERVER['REQUEST_URI']; + if ( + !str_contains($current_url, '.form.php') + && !str_contains($current_url, '.injector.php') + && !str_contains($current_url, '.public.php') + && !str_contains($current_url, 'ajax/planning') + && !str_contains($current_url, 'ajax/timeline.php') // ITILSolution load from timeline + ) { + continue; + } - //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; - } + //Retrieve dom container + $itemtypes = PluginFieldsContainer::getUsedItemtypes($type, true); - $class = match (true) { - !($item instanceof CommonITILObject) && $item instanceof CommonDropdown => 'card-body row', - // @phpstan-ignore-next-line -> Instanceof between CommonDBTM and CommonDropdown will always evaluate to false. - !($item instanceof CommonITILObject) && !($item instanceof CommonDropdown) => 'card-body d-flex flex-wrap', // lign 969 - default => '', - }; - $html_id = 'plugin_fields_container_' . mt_rand(); - - echo "
"; - $display_condition = new PluginFieldsContainerDisplayCondition(); - if ($display_condition->computeDisplayContainer($item, $c_id)) { - self::showDomContainer( - $c_id, - $item, - $type, - $subtype, - [], - ); - } - echo '
'; + //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; + } + + $class = match (true) { + !($item instanceof CommonITILObject) && $item instanceof CommonDropdown => 'card-body row', + // @phpstan-ignore-next-line -> Instanceof between CommonDBTM and CommonDropdown will always evaluate to false. + !($item instanceof CommonITILObject) && !($item instanceof CommonDropdown) => 'card-body d-flex flex-wrap', // lign 969 + default => '', + }; + $html_id = 'plugin_fields_container_' . $container_id; + + echo "
"; + $display_condition = new PluginFieldsContainerDisplayCondition(); + if ($display_condition->computeDisplayContainer($item, $container_id)) { + self::showDomContainer( + $container_id, + $item, + $type, + $subtype, + [], + ); + } + echo '
'; - //JS to trigger any change and check if container need to be display or not - $ajax_url = $CFG_GLPI['root_doc'] . '/plugins/fields/ajax/container.php'; - $items_id = !$item->isNewItem() ? $item->getID() : 0; - echo Html::scriptBlock( - <<isNewItem() ? $item->getID() : 0; + echo Html::scriptBlock( + << 0) { @@ -1036,31 +1039,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 + 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); } - - if (refresh_timeout !== null) { - window.clearTimeout(refresh_timeout); - } - refresh_timeout = window.setTimeout(refreshContainer, 1000); - } - ); + ); + } } - } - ); - } + ); + } + ); + JAVASCRIPT ); -JAVASCRIPT - ); + } } public static function prepareHtmlFields( diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..c9c3592b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,12 @@ + + + + tests + + + \ No newline at end of file diff --git a/templates/fields.html.twig b/templates/fields.html.twig index 4f2270b2..72f7e352 100644 --- a/templates/fields.html.twig +++ b/templates/fields.html.twig @@ -42,8 +42,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]|default(field['value']) %} {% set readonly = field['is_readonly'] %} @@ -109,9 +110,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|default({}))) }} diff --git a/tests/PluginFieldsContainerTest.php b/tests/PluginFieldsContainerTest.php new file mode 100644 index 00000000..af551d28 --- /dev/null +++ b/tests/PluginFieldsContainerTest.php @@ -0,0 +1,753 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +use PHPUnit\Framework\TestCase; + +class PluginFieldsContainerTest extends TestCase +{ + private array $createdContainers = []; + private array $createdTickets = []; + + protected function setUp(): void + { + $_SESSION['glpiactive_entity'] = 0; + } + + protected function tearDown(): void + { + $this->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'); + + // 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'); + + // 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..5fdbe1df --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,35 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +require __DIR__ . '/../../../tests/bootstrap.php'; + +if (!Plugin::isPluginActive("fields")) { + throw new RuntimeException("Plugin fields is not active in the test database"); +}