. * --------------------------------------------------------------------- */ if (!defined('GLPI_ROOT')) { die("Sorry. You can't access this file directly"); } /** * CommonTreeDropdown Class * * Hierarchical and cross entities **/ abstract class CommonTreeDropdown extends CommonDropdown { public $can_be_translated = false; function getAdditionalFields() { return [['name' => $this->getForeignKeyField(), 'label' => __('As child of'), 'type' => 'parent', 'list' => false]]; } function defineTabs($options = []) { $ong = []; $this->addDefaultFormTab($ong); $this->addImpactTab($ong, $options); $this->addStandardTab($this->getType(), $ong, $options); if ($this->dohistory) { $this->addStandardTab('Log', $ong, $options); } if (DropdownTranslation::canBeTranslated($this)) { $this->addStandardTab('DropdownTranslation', $ong, $options); } return $ong; } function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { if (!$withtemplate && ($item->getType() == $this->getType())) { $nb = 0; if ($_SESSION['glpishow_count_on_tabs']) { $nb = countElementsInTable($this->getTable(), [$this->getForeignKeyField() => $item->getID()]); } return self::createTabEntry($this->getTypeName(Session::getPluralNumber()), $nb); } return ''; } static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) { if ($item instanceof CommonTreeDropdown) { $item->showChildren(); } return true; } /** * Compute completename based on parent one * * @param $parentCompleteName string parent complete name (need to be stripslashes / comes from DB) * @param $thisName string item name (need to be addslashes : comes from input) **/ static function getCompleteNameFromParents($parentCompleteName, $thisName) { return addslashes($parentCompleteName). " > ".$thisName; } /** * @param $input **/ function adaptTreeFieldsFromUpdateOrAdd($input) { $parent = clone $this; // Update case input['name'] not set : if (!isset($input['name']) && isset($this->fields['name'])) { $input['name'] = addslashes($this->fields['name']); } // leading/ending space will break findID/import $input['name'] = trim($input['name']); if (isset($input[$this->getForeignKeyField()]) && !$this->isNewID($input[$this->getForeignKeyField()]) && $parent->getFromDB($input[$this->getForeignKeyField()])) { $input['level'] = $parent->fields['level']+1; // Sometimes (internet address), the complete name may be different ... /* if ($input[$this->getForeignKeyField()]==0) { // Root entity case $input['completename'] = $input['name']; } else {*/ $input['completename'] = self::getCompleteNameFromParents($parent->fields['completename'], $input['name']); // } } else { $input[$this->getForeignKeyField()] = 0; $input['level'] = 1; $input['completename'] = $input['name']; } return $input; } function prepareInputForAdd($input) { return $this->adaptTreeFieldsFromUpdateOrAdd($input); } function pre_deleteItem() { global $DB; // Not set in case of massive delete : use parent if (isset($this->input['_replace_by']) && $this->input['_replace_by']) { $parent = $this->input['_replace_by']; } else { $parent = $this->fields[$this->getForeignKeyField()]; } $this->cleanParentsSons(); $tmp = clone $this; $result = $DB->request( [ 'SELECT' => 'id', 'FROM' => $this->getTable(), 'WHERE' => [$this->getForeignKeyField() => $this->fields['id']] ] ); foreach ($result as $data) { $data[$this->getForeignKeyField()] = $parent; $tmp->update($data); } return true; } function prepareInputForUpdate($input) { global $GLPI_CACHE; if (isset($input[$this->getForeignKeyField()])) { // Can't move a parent under a child if (in_array($input[$this->getForeignKeyField()], getSonsOf($this->getTable(), $input['id']))) { return false; } // Parent changes => clear ancestors and update its level and completename if ($input[$this->getForeignKeyField()] != $this->fields[$this->getForeignKeyField()]) { $input["ancestors_cache"] = ''; if (Toolbox::useCache()) { $ckey = 'ancestors_cache_' . $this->getTable() . '_' . $this->getID(); $GLPI_CACHE->delete($ckey); } return $this->adaptTreeFieldsFromUpdateOrAdd($input); } } // Name changes => update its completename (and its level : side effect ...) if ((isset($input['name'])) && ($input['name'] != $this->fields['name'])) { return $this->adaptTreeFieldsFromUpdateOrAdd($input); } return $input; } /** * @param $ID * @param $updateName * @param $changeParent **/ function regenerateTreeUnderID($ID, $updateName, $changeParent) { global $DB, $GLPI_CACHE; //drop from sons cache when needed if ($changeParent && Toolbox::useCache()) { $ckey = 'ancestors_cache_' . $this->getTable() . '_' . $ID; $GLPI_CACHE->delete($ckey); } if (($updateName) || ($changeParent)) { $currentNode = clone $this; if ($currentNode->getFromDB($ID)) { $currentNodeCompleteName = $currentNode->getField("completename"); $nextNodeLevel = ($currentNode->getField("level") + 1); } else { $nextNodeLevel = 1; } $query = [ 'SELECT' => ['id', 'name'], 'FROM' => $this->getTable(), 'WHERE' => [$this->getForeignKeyField() => $ID] ]; if (Session::haveTranslations($this->getType(), 'completename')) { DropdownTranslation::regenerateAllCompletenameTranslationsFor($this->getType(), $ID); } foreach ($DB->request($query) as $data) { $update = []; if ($updateName || $changeParent) { if (isset($currentNodeCompleteName)) { $update['completename'] = self::getCompleteNameFromParents( $currentNodeCompleteName, addslashes($data["name"]) ); } else { $update['completename'] = addslashes($data["name"]); } } if ($changeParent) { // We have to reset the ancestors as only these changes (ie : not the children). $update['ancestors_cache'] = 'NULL'; // And we must update the level of the current node ... $update['level'] = $nextNodeLevel; } $DB->update( $this->getTable(), $update, ['id' => $data['id']] ); // Translations : if (Session::haveTranslations($this->getType(), 'completename')) { DropdownTranslation::regenerateAllCompletenameTranslationsFor($this->getType(), $data['id']); } $this->regenerateTreeUnderID($data["id"], $updateName, $changeParent); } } } /** * Clean sons of all parents from caches * * @param null|integer $id Parent id to clean. Default to current id * @param boolean $cache Whether to clean cache (defaults to true) * * @return void */ protected function cleanParentsSons($id = null, $cache = true) { global $DB, $GLPI_CACHE; if ($id === null) { $id = $this->getID(); } $ancestors = getAncestorsOf($this->getTable(), $id); if ($id != $this->getID()) { $ancestors[$id] = "$id"; } if (!count($ancestors)) { return; } $DB->update( $this->getTable(), [ 'sons_cache' => 'NULL' ], [ 'id' => $ancestors ] ); //drop from sons cache when needed if ($cache && Toolbox::useCache()) { foreach ($ancestors as $ancestor) { $ckey = 'sons_cache_' . $this->getTable() . '_' . $ancestor; if ($GLPI_CACHE->has($ckey)) { $sons = $GLPI_CACHE->get($ckey); if (isset($sons[$this->getID()])) { unset($sons[$this->getID()]); $GLPI_CACHE->set($ckey, $sons); } } else { // If cache key does not exists in current context (UI using APCu), it may exists // in another context (CLI using filesystem). So we force deletion of cache in all contexts // to be sure to not use a stale value. $GLPI_CACHE->delete($ckey); } } } } /** * Add new son in its parent in cache * * @return void */ protected function addSonInParents() { global $GLPI_CACHE; //add sons cache when needed if (Toolbox::useCache()) { $ancestors = getAncestorsOf($this->getTable(), $this->getID()); foreach ($ancestors as $ancestor) { $ckey = 'sons_cache_' . $this->getTable() . '_' . $ancestor; if ($GLPI_CACHE->has($ckey)) { $sons = $GLPI_CACHE->get($ckey); if (!isset($sons[$this->getID()])) { $sons[$this->getID()] = $this->getID(); $GLPI_CACHE->set($ckey, $sons); } } else { // If cache key does not exists in current context (UI using APCu), it may exists // in another context (CLI using filesystem). So we force deletion of cache in all contexts // to be sure to not use a stale value. $GLPI_CACHE->delete($ckey); } } } } function post_addItem() { $parent = $this->fields[$this->getForeignKeyField()]; //do not clean APCu, it will be updated $this->cleanParentsSons(null, false); $this->addSonInParents(); if ($parent && $this->dohistory) { $changes = [ 0, '', addslashes($this->getNameID()), ]; Log::history($parent, $this->getType(), $changes, $this->getType(), Log::HISTORY_ADD_SUBITEM); } } function post_updateItem($history = 1) { $ID = $this->getID(); $changeParent = in_array($this->getForeignKeyField(), $this->updates); $this->regenerateTreeUnderID($ID, in_array('name', $this->updates), $changeParent); if ($changeParent) { $oldParentID = $this->oldvalues[$this->getForeignKeyField()]; $newParentID = $this->fields[$this->getForeignKeyField()]; $oldParentNameID = ''; $newParentNameID = ''; $parent = clone $this; if ($oldParentID > 0) { $this->cleanParentsSons($oldParentID); if ($history) { if ($parent->getFromDB($oldParentID)) { $oldParentNameID = $parent->getNameID(); } $changes = [ '0', addslashes($this->getNameID()), '', ]; Log::history($oldParentID, $this->getType(), $changes, $this->getType(), Log::HISTORY_DELETE_SUBITEM); } } if ($newParentID > 0) { $this->cleanParentsSons(null, false); $this->addSonInParents(); if ($history) { if ($parent->getFromDB($newParentID)) { $newParentNameID = $parent->getNameID(); } $changes = [ '0', '', addslashes($this->getNameID()), ]; Log::history($newParentID, $this->getType(), $changes, $this->getType(), Log::HISTORY_ADD_SUBITEM); } } if ($history) { $changes = [ '0', $oldParentNameID, $newParentNameID, ]; Log::history($ID, $this->getType(), $changes, $this->getType(), Log::HISTORY_UPDATE_SUBITEM); } getAncestorsOf(getTableForItemType($this->getType()), $ID); } } function post_deleteFromDB() { $parent = $this->fields[$this->getForeignKeyField()]; if ($parent && $this->dohistory) { $changes = [ '0', addslashes($this->getNameID()), '', ]; Log::history($parent, $this->getType(), $changes, $this->getType(), Log::HISTORY_DELETE_SUBITEM); } } /** * Get the this for all the current item and all its parent * * @return string **/ function getTreeLink() { $link = ''; if ($this->fields[$this->getForeignKeyField()]) { $papa = clone $this; if ($papa->getFromDB($this->fields[$this->getForeignKeyField()])) { $link = $papa->getTreeLink() . " > "; } } return $link . $this->getLink(); } /** * Print the HTML array children of a TreeDropdown * * @return void */ function showChildren() { global $DB; $ID = $this->getID(); $this->check($ID, READ); $fields = array_filter( $this->getAdditionalFields(), function ($field) { return isset($field['list']) && $field['list']; } ); $nb = count($fields); $entity_assign = $this->isEntityAssign(); // Minimal form for quick input. if (static::canCreate()) { $link = $this->getFormURL(); echo "
"; echo "
"; echo ""; echo ""; echo ""; echo "\n"; echo "
".__('New child heading')."
".__('Name').""; Html::autocompletionTextField($this, "name", ['value' => '']); if ($entity_assign && ($this->getForeignKeyField() != 'entities_id')) { echo ""; } if ($entity_assign && $this->isRecursive()) { echo ""; } echo ""; echo "
"; Html::closeForm(); echo "
\n"; } echo "
"; echo ""; echo ""; $header = ""; if ($entity_assign) { $header .= ""; } foreach ($fields as $field) { $header .= ""; } $header .= ""; $header .= "\n"; echo $header; $fk = $this->getForeignKeyField(); $result = $DB->request( [ 'FROM' => $this->getTable(), 'WHERE' => [$fk => $ID], 'ORDER' => 'name', ] ); $nb = 0; foreach ($result as $data) { $nb++; echo ""; if ($entity_assign) { echo ""; } foreach ($fields as $field) { echo ""; } echo ""; echo "\n"; } if ($nb) { echo $header; } echo "
".sprintf(__('Sons of %s'), $this->getTreeLink()); echo "
".__('Name')."".Entity::getTypeName(1)."".$field['label']."".__('Comments')."
"; if ((($fk == 'entities_id') && in_array($data['id'], $_SESSION['glpiactiveentities'])) || !$entity_assign || (($fk != 'entities_id') && in_array($data['entities_id'], $_SESSION['glpiactiveentities']))) { echo "".$data['name'].""; } else { echo $data['name']; } echo "".Dropdown::getDropdownName("glpi_entities", $data["entities_id"]).""; switch ($field['type']) { case 'UserDropdown' : echo getUserName($data[$field['name']]); break; case 'bool' : echo Dropdown::getYesNo($data[$field['name']]); break; case 'dropdownValue' : echo Dropdown::getDropdownName(getTableNameForForeignKeyField($field['name']), $data[$field['name']]); break; default: echo $data[$field['name']]; } echo "".$data['comment']."
\n"; } function getSpecificMassiveActions($checkitem = null) { $isadmin = static::canUpdate(); $actions = parent::getSpecificMassiveActions($checkitem); if ($isadmin) { $actions[__CLASS__.MassiveAction::CLASS_ACTION_SEPARATOR.'move_under'] = "". _x('button', 'Move under'); } return $actions; } static function showMassiveActionsSubForm(MassiveAction $ma) { switch ($ma->getAction()) { case 'move_under' : $itemtype = $ma->getItemType(true); echo __('As child of'); Dropdown::show($itemtype, ['name' => 'parent', 'comments' => 0, 'entity' => $_SESSION['glpiactive_entity'], 'entity_sons' => $_SESSION['glpiactive_entity_recursive']]); echo "

\n"; return true; } return parent::showMassiveActionsSubForm($ma); } static function processMassiveActionsForOneItemtype(MassiveAction $ma, CommonDBTM $item, array $ids) { $input = $ma->getInput(); switch ($ma->getAction()) { case 'move_under' : if (isset($input['parent'])) { $fk = $item->getForeignKeyField(); $parent = clone $item; if (!$parent->getFromDB($input['parent'])) { $ma->itemDone($item->getType(), $ids, MassiveAction::ACTION_KO); $ma->addMessage($parent->getErrorMessage(ERROR_NOT_FOUND)); return; } foreach ($ids as $id) { if ($item->can($id, UPDATE)) { // Check if parent is not a child of the original one if (!in_array($parent->getID(), getSonsOf($item->getTable(), $item->getID()))) { if ($item->update(['id' => $id, $fk => $parent->getID()])) { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK); } else { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO); $ma->addMessage($item->getErrorMessage(ERROR_ON_ACTION)); } } else { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO); $ma->addMessage($item->getErrorMessage(ERROR_COMPAT)); } } else { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_NORIGHT); $ma->addMessage($item->getErrorMessage(ERROR_RIGHT)); } } } else { $ma->itemDone($item->getType(), $ids, MassiveAction::ACTION_KO); $ma->addMessage($parent->getErrorMessage(ERROR_COMPAT)); } return; } parent::processMassiveActionsForOneItemtype($ma, $item, $ids); } function rawSearchOptions() { $tab = []; $tab[] = [ 'id' => 'common', 'name' => __('Characteristics') ]; $tab[] = [ 'id' => '1', 'table' => $this->getTable(), 'field' => 'completename', 'name' => __('Complete name'), 'datatype' => 'itemlink', 'massiveaction' => false ]; $tab[] = [ 'id' => '2', 'table' => $this->getTable(), 'field' => 'id', 'name' => __('ID'), 'massiveaction' => false, 'datatype' => 'number' ]; $tab[] = [ 'id' => '14', 'table' => $this->getTable(), 'field' => 'name', 'name' => __('Name'), 'datatype' => 'itemlink', 'autocomplete' => true, ]; $tab[] = [ 'id' => '13', 'table' => $this->getTable(), 'field' => 'completename', 'name' => __('Father'), 'datatype' => 'dropdown', 'massiveaction' => false, // Add virtual condition to relink table 'joinparams' => ['condition' => "AND 1=1"] ]; $tab[] = [ 'id' => '16', 'table' => $this->getTable(), 'field' => 'comment', 'name' => __('Comments'), 'datatype' => 'text' ]; if ($this->isEntityAssign()) { $tab[] = [ 'id' => '80', 'table' => 'glpi_entities', 'field' => 'completename', 'name' => Entity::getTypeName(1), 'massiveaction' => false, 'datatype' => 'dropdown' ]; } if ($this->maybeRecursive()) { $tab[] = [ 'id' => '86', 'table' => $this->getTable(), 'field' => 'is_recursive', 'name' => __('Child entities'), 'datatype' => 'bool' ]; } if ($this->isField('date_mod')) { $tab[] = [ 'id' => '19', 'table' => $this->getTable(), 'field' => 'date_mod', 'name' => __('Last update'), 'datatype' => 'datetime', 'massiveaction' => false ]; } if ($this->isField('date_creation')) { $tab[] = [ 'id' => '121', 'table' => $this->getTable(), 'field' => 'date_creation', 'name' => __('Creation date'), 'datatype' => 'datetime', 'massiveaction' => false ]; } // add objectlock search options $tab = array_merge($tab, ObjectLock::rawSearchOptionsToAdd(get_class($this))); return $tab; } function haveChildren() { $fk = $this->getForeignKeyField(); $id = $this->fields['id']; return (countElementsInTable($this->getTable(), [$fk => $id]) > 0); } /** * reformat text field describing a tree (such as completename) * * @param $value string * * @return string **/ static function cleanTreeText($value) { $tmp = explode('>', $value); foreach ($tmp as $k => $v) { $v = trim($v); if (empty($v)) { unset($tmp[$k]); } else { $tmp[$k] = $v; } } return implode(' > ', $tmp); } function findID(array &$input) { global $DB; if (isset($input['completename'])) { // Clean data $input['completename'] = self::cleanTreeText($input['completename']); } if (isset($input['completename']) && !empty($input['completename'])) { $criteria = [ 'SELECT' => 'id', 'FROM' => $this->getTable(), 'WHERE' => [ 'completename' => $input['completename'] ] ]; if ($this->isEntityAssign()) { $criteria['WHERE'] = $criteria['WHERE'] + getEntitiesRestrictCriteria( $this->getTable(), '', $input['entities_id'], $this->maybeRecursive() ); } // Check twin : $iterator = $DB->request($criteria); if (count($iterator)) { $result = $iterator->next(); return $result['id']; } } else if (isset($input['name']) && !empty($input['name'])) { $fk = $this->getForeignKeyField(); $criteria = [ 'SELECT' => 'id', 'FROM' => $this->getTable(), 'WHERE' => [ 'name' => $input['name'], $fk => (isset($input[$fk]) ? $input[$fk] : 0) ] ]; if ($this->isEntityAssign()) { $criteria['WHERE'] = $criteria['WHERE'] + getEntitiesRestrictCriteria( $this->getTable(), '', $input['entities_id'], $this->maybeRecursive() ); } // Check twin : $iterator = $DB->request($criteria); if (count($iterator)) { $result = $iterator->next(); return $result['id']; } } return -1; } function import(array $input) { if (isset($input['name'])) { return parent::import($input); } if (!isset($input['completename']) || empty($input['completename'])) { return -1; } // Import a full tree from completename $names = explode('>', $input['completename']); $fk = $this->getForeignKeyField(); $i = count($names); $parent = 0; foreach ($names as $name) { $i--; $name = trim($name); if (empty($name)) { // Skip empty name (completename starting/endind with >, double >, ...) continue; } $tmp = [ 'name' => $name, $fk => $parent, ]; if (isset($input['is_recursive'])) { $tmp['is_recursive'] = $input['is_recursive']; } if (isset($input['entities_id'])) { $tmp['entities_id'] = $input['entities_id']; } if (!$i) { // Other fields (comment, ...) only for last node of the tree foreach ($input as $key => $val) { if ($key != 'completename') { $tmp[$key] = $val; } } } $parent = parent::import($tmp); } return $parent; } static function getIcon() { return "fas fa-sitemap"; } }