. * --------------------------------------------------------------------- */ use Glpi\Event; if (!defined('GLPI_ROOT')) { die("Sorry. You can't access this file directly"); } /** * Common DataBase Table Manager Class - Persistent Object **/ class CommonDBTM extends CommonGLPI { /** * Data fields of the Item. * * @var mixed[] */ public $fields = []; /** * Flag to determine whether or not changes must be logged into history. * * @var boolean */ public $dohistory = false; /** * List of fields that must not be taken into account when logging history or computating last * modification date. * * @var string[] */ public $history_blacklist = []; /** * Flag to determine whether or not automatic messages must be generated on actions. * * @var boolean */ public $auto_message_on_action = true; /** * Flag to determine whether or not a link to item form can be automatically generated via * self::getLink() method. * * @var boolean */ public $no_form_page = false; /** * Flag to determine whether or not table name of item can be automatically generated via * self::getTable() method. * * @var boolean */ static protected $notable = false; /** * List of fields that must not be taken into account for dictionnary processing. * * @var string[] */ public $additional_fields_for_dictionnary = []; /** * List of linked item types on which entities informations should be forwarded on update. * * @var string[] */ static protected $forward_entity_to = []; /** * Foreign key field cache : set dynamically calling getForeignKeyField * * @TODO Remove this variable as it is not used ? */ protected $fkfield = ""; /** * Search option of item. Initialized on first call to self::getOptions() and used as cache. * * @var array * * @TODO Should be removed and replaced by real cache usage. */ protected $searchopt = false; /** * {@inheritDoc} */ public $taborientation = 'vertical'; /** * {@inheritDoc} */ public $get_item_to_display_tab = true; /** * List of linked item types from plugins on which entities informations should be forwarded on update. * * @var array */ static protected $plugins_forward_entity = []; /** * Flag to determine whether or not table name of item has a notepad. * * @var boolean */ protected $usenotepad = false; /** * Flag to determine whether or not notification queu should be flushed immediately when an * action is performed on item. * * @var boolean */ public $notificationqueueonaction = false; /** * Computed/forced values of classes tables. * @var string[] */ protected static $tables_of = []; /** * Computed values of classes foreign keys. * @var string[] */ protected static $foreign_key_fields_of = []; /** * Fields to remove when querying data with api * @var array */ static $undisclosedFields = []; /** * Constructor **/ function __construct () { } /** * Return the table used to store this object * * @param string $classname Force class (to avoid late_binding on inheritance) * * @return string **/ static function getTable($classname = null) { if ($classname === null) { $classname = get_called_class(); } if (!class_exists($classname) || $classname::$notable) { return ''; } if (!isset(self::$tables_of[$classname]) || empty(self::$tables_of[$classname])) { self::$tables_of[$classname] = getTableForItemType($classname); } return self::$tables_of[$classname]; } /** * force table value (used for config management for old versions) * * @param string $table name of the table to be forced * * @return void **/ static function forceTable($table) { self::$tables_of[get_called_class()] = $table; } static function getForeignKeyField() { $classname = get_called_class(); if (!isset(self::$foreign_key_fields_of[$classname]) || empty(self::$foreign_key_fields_of[$classname])) { self::$foreign_key_fields_of[$classname] = getForeignKeyFieldForTable(static::getTable()); } return self::$foreign_key_fields_of[$classname]; } /** * Return SQL path to access a field. * * @param string $field Name of the field (or SQL keyword like '*') * @param string|null $classname Forced classname (to avoid late_binding on inheritance) * * @return string * * @throws InvalidArgumentException * @throws LogicException **/ static function getTableField($field, $classname = null) { if (empty($field)) { throw new InvalidArgumentException('Argument $field cannot be empty.'); } $tablename = self::getTable($classname); if (empty($tablename)) { throw new LogicException('Invalid table name.'); } return sprintf('%s.%s', $tablename, $field); } /** * Retrieve an item from the database * * @param integer $ID ID of the item to get * * @return boolean true if succeed else false **/ function getFromDB($ID) { global $DB; // Make new database object and fill variables // != 0 because 0 is consider as empty if (strlen($ID) == 0) { return false; } $iterator = $DB->request([ 'FROM' => $this->getTable(), 'WHERE' => [ $this->getTable() . '.' . $this->getIndexName() => Toolbox::cleanInteger($ID) ], 'LIMIT' => 1 ]); if (count($iterator) == 1) { $this->fields = $iterator->next(); $this->post_getFromDB(); return true; } else if (count($iterator) > 1) { Toolbox::logWarning( sprintf( 'getFromDB expects to get one result, %1$s found!', count($iterator) ) ); } return false; } /** * Hydrate an object from a resultset row * * @param array $rs The row * * @return void */ function getFromResultSet($rs) { //just set fields! $this->fields = $rs; } /** * Generator to browse object from an iterator * @see http://php.net/manual/en/language.generators.syntax.php * * @since 9.2 * * @param DBmysqlIterator $iter Iterator instance * * @return CommonDBTM */ public static function getFromIter(DBmysqlIterator $iter) { $item = new static; foreach ($iter as $row) { if (!isset($row["id"])) { continue; } if ($item->getFromDB($row["id"])) { yield $item; } } } /** * Get an object using some criteria * * @since 9.2 * * @param Array $crit search criteria * * @return boolean|array */ public function getFromDBByCrit(array $crit) { global $DB; $crit = ['SELECT' => 'id', 'FROM' => $this->getTable(), 'WHERE' => $crit]; $iter = $DB->request($crit); if (count($iter) == 1) { $row = $iter->next(); return $this->getFromDB($row['id']); } else if (count($iter) > 1) { Toolbox::logWarning( sprintf( 'getFromDBByCrit expects to get one result, %1$s found!', count($iter) ) ); } return false; } /** * Retrieve an item from the database by request. The request is an array * similar to the one expected in DB::request(). * * @since 9.3 * * @see DB::request() * * @param array $request expression * * @return boolean true if succeed else false **/ public function getFromDBByRequest(array $request) { global $DB; // Limit the request to the useful expressions $request = array_diff_key($request, [ 'FROM' => '', 'SELECT' => '', 'COUNT' => '', 'GROUPBY' => '', ]); $request['FROM'] = $this->getTable(); $request['SELECT'] = $this->getTable() . '.*'; $iterator = $DB->request($request); if (count($iterator) == 1) { $this->fields = $iterator->next(); $this->post_getFromDB(); return true; } else if (count($iterator) > 1) { Toolbox::logWarning( sprintf( 'getFromDBByRequest expects to get one result, %1$s found!', count($iterator) ) ); } return false; } /** * Get the identifier of the current item * * @return integer ID **/ function getID() { if (isset($this->fields[static::getIndexName()])) { return $this->fields[static::getIndexName()]; } return -1; } /** * Actions done at the end of the getFromDB function * * @return void **/ function post_getFromDB() { } /** * Actions done to not show some fields when geting a single item from API calls * * @param array $fields Fields to unset undiscloseds * * @return void */ static public function unsetUndisclosedFields(&$fields) { foreach (static::$undisclosedFields as $key) { unset($fields[$key]); } } /** * Retrieve all items from the database * * @param array $condition condition used to search if needed (empty get all) (default '') * @param array|string $order order field if needed (default '') * @param integer $limit limit retrieved data if needed (default '') * * @return array all retrieved data in a associative array by id **/ function find($condition = [], $order = [], $limit = null) { global $DB; $criteria = [ 'FROM' => $this->getTable() ]; if (count($condition)) { $criteria['WHERE'] = $condition; } if (!is_array($order)) { $order = [$order]; } if (count($order)) { $criteria['ORDERBY'] = $order; } if ((int)$limit > 0) { $criteria['LIMIT'] = (int)$limit; } $data = []; $iterator = $DB->request($criteria); while ($line = $iterator->next()) { $data[$line['id']] = $line; } return $data; } /** * Get the name of the index field * * @return string name of the index field **/ static function getIndexName() { return "id"; } /** * Get an empty item * *@return boolean true if succeed else false **/ function getEmpty() { global $DB; //make an empty database object $table = $this->getTable(); if (!empty($table) && ($fields = $DB->listFields($table))) { foreach (array_keys($fields) as $key) { $this->fields[$key] = ""; } } else { return false; } if (array_key_exists('entities_id', $this->fields) && isset($_SESSION["glpiactive_entity"])) { $this->fields['entities_id'] = $_SESSION["glpiactive_entity"]; } $this->post_getEmpty(); // Call the plugin hook - $this->fields can be altered Plugin::doHook("item_empty", $this); return true; } /** * Actions done at the end of the getEmpty function * * @return void **/ function post_getEmpty() { } /** * Get type to register log on * * @since 0.83 * * @return array array of type + ID **/ function getLogTypeID() { return [$this->getType(), $this->fields['id']]; } /** * Update the item in the database * * @param string[] $updates fields to update * @param string[] $oldvalues array of old values of the updated fields * * @return void **/ function updateInDB($updates, $oldvalues = []) { global $DB; foreach ($updates as $field) { if (isset($this->fields[$field])) { $DB->update( $this->getTable(), [$field => $this->fields[$field]], ['id' => $this->fields['id']] ); if ($DB->affectedRows() == 0) { if (isset($oldvalues[$field])) { unset($oldvalues[$field]); } } } else { // Clean oldvalues if (isset($oldvalues[$field])) { unset($oldvalues[$field]); } } } if (count($oldvalues)) { Log::constructHistory($this, $oldvalues, $this->fields); } return true; } /** * Add an item to the database * * @return integer|boolean new ID of the item is insert successfull else false **/ function addToDB() { global $DB; $nb_fields = count($this->fields); if ($nb_fields > 0) { $params = []; foreach ($this->fields as $key => $value) { //FIXME: why is that handled here? if (($this->getType() == 'ProfileRight') && ($value == '')) { $value = 0; } $params[$key] = $value; } $result = $DB->insert($this->getTable(), $params); if ($result) { if (!isset($this->fields['id']) || is_null($this->fields['id']) || ($this->fields['id'] == 0)) { $this->fields['id'] = $DB->insertId(); } return $this->fields['id']; } } return false; } /** * Restore item = set deleted flag to 0 * * @return boolean true if succeed else false **/ function restoreInDB() { global $DB; if ($this->maybeDeleted()) { $params = ['is_deleted' => 0]; // Auto set date_mod if exsist if (isset($this->fields['date_mod'])) { $params['date_mod'] = $_SESSION["glpi_currenttime"]; } if ($DB->update($this->getTable(), $params, ['id' => $this->fields['id']])) { return true; } } return false; } /** * Mark deleted or purge an item in the database * * @param boolean $force force the purge of the item (not used if the table do not have a deleted field) * (default 0) * * @return boolean true if succeed else false **/ function deleteFromDB($force = 0) { global $DB; if (($force == 1) || !$this->maybeDeleted() || ($this->useDeletedToLockIfDynamic() && !$this->isDynamic())) { $this->cleanDBonPurge(); if ($this instanceof CommonDropdown) { $this->cleanTranslations(); } $this->cleanHistory(); $this->cleanRelationData(); $this->cleanRelationTable(); $result = $DB->delete( $this->getTable(), [ 'id' => $this->fields['id'] ] ); if ($result) { $this->post_deleteFromDB(); return true; } } else { // Auto set date_mod if exsist $toadd = []; if (isset($this->fields['date_mod'])) { $toadd['date_mod'] = $_SESSION["glpi_currenttime"]; } $result = $DB->update( $this->getTable(), [ 'is_deleted' => 1 ] + $toadd, [ 'id' => $this->fields['id'] ] ); $this->cleanDBonMarkDeleted(); if ($result) { return true; } } return false; } /** * Clean data in the tables which have linked the deleted item * * @return void **/ function cleanHistory() { global $DB; if ($this->dohistory) { $DB->delete( 'glpi_logs', [ 'itemtype' => $this->getType(), 'items_id' => $this->fields['id'] ] ); } } /** * Clean data in the tables which have linked the deleted item * Clear 1/N Relation * * @return void **/ function cleanRelationData() { global $DB, $CFG_GLPI; $RELATION = getDbRelations(); if (isset($RELATION[$this->getTable()])) { $newval = (isset($this->input['_replace_by']) ? $this->input['_replace_by'] : 0); foreach ($RELATION[$this->getTable()] as $tablename => $field) { if ($tablename[0] != '_') { $itemtype = getItemTypeForTable($tablename); // Code factorization : we transform the singleton to an array if (!is_array($field)) { $field = [$field]; } foreach ($field as $f) { $result = $DB->request( [ 'FROM' => $tablename, 'WHERE' => [$f => $this->getID()], ] ); foreach ($result as $data) { // Be carefull : we must use getIndexName because self::update rely on that ! if ($object = getItemForItemtype($itemtype)) { $idName = $object->getIndexName(); // And we must ensure that the index name is not the same as the field // we try to modify. Otherwise we will loose this element because all // will be set to $newval ... if ($idName != $f) { $object->update([$idName => $data[$idName], $f => $newval, '_disablenotif' => true]); // Disable notifs } } } } } } } // Clean ticket open against the item if (in_array($this->getType(), $CFG_GLPI["ticket_types"])) { $job = new Ticket(); $itemsticket = new Item_Ticket(); $iterator = $DB->request([ 'FROM' => 'glpi_items_tickets', 'WHERE' => [ 'items_id' => $this->getID(), 'itemtype' => $this->getType() ] ]); while ($data = $iterator->next()) { $cnt = countElementsInTable('glpi_items_tickets', ['tickets_id' => $data['tickets_id']]); $itemsticket->delete(["id" => $data["id"]]); if ($cnt == 1 && !$CFG_GLPI["keep_tickets_on_delete"]) { $job->delete(["id" => $data["tickets_id"]]); } } } } /** * Actions done after the DELETE of the item in the database * * @return void **/ function post_deleteFromDB() { } /** * Actions done when item is deleted from the database * * @return void **/ function cleanDBonPurge() { } /** * Delete children items and relation with other items from database. * * @param array $relations_classes List of classname on which deletion will be done * Classes needs to extends CommonDBConnexity. * * @return void **/ protected function deleteChildrenAndRelationsFromDb(array $relations_classes) { foreach ($relations_classes as $classname) { if (!is_a($classname, CommonDBConnexity::class, true)) { Toolbox::logWarning( sprintf( 'Unable to clean elements of class %s as it does not extends "CommonDBConnexity"', $classname ) ); continue; } /** @var CommonDBConnexity $relation_item */ $relation_item = new $classname(); $relation_item->cleanDBonItemDelete($this->getType(), $this->fields['id']); } } /** * Clean translations associated to a dropdown * * @since 0.85 * * @return void **/ function cleanTranslations() { //Do not try to clean is dropdown translation is globally off if (DropdownTranslation::isDropdownTranslationActive()) { $translation = new DropdownTranslation(); $translation->deleteByCriteria(['itemtype' => get_class($this), 'items_id' => $this->getID()]); } } /** * Clean the data in the relation tables for the deleted item * Clear N/N Relation * * @return void **/ function cleanRelationTable() { global $CFG_GLPI, $DB; // If this type have INFOCOM, clean one associated to purged item if (Infocom::canApplyOn($this)) { $infocom = new Infocom(); if ($infocom->getFromDBforDevice($this->getType(), $this->fields['id'])) { $infocom->delete(['id' => $infocom->fields['id']]); } } // If this type have NETPORT, clean one associated to purged item if (in_array($this->getType(), $CFG_GLPI['networkport_types'])) { // If we don't use delete, then cleanDBonPurge() is not call and the NetworkPorts are not // clean properly $networkPortObject = new NetworkPort(); $networkPortObject->cleanDBonItemDelete($this->getType(), $this->getID()); // Manage networkportmigration if exists if ($DB->tableExists('glpi_networkportmigrations')) { $networkPortMigObject = new NetworkPortMigration(); $networkPortMigObject->cleanDBonItemDelete($this->getType(), $this->getID()); } } // If this type is RESERVABLE clean one associated to purged item if (in_array($this->getType(), $CFG_GLPI['reservation_types'])) { $rr = new ReservationItem(); $rr->cleanDBonItemDelete($this->getType(), $this->fields['id']); } // If this type have CONTRACT, clean one associated to purged item if (in_array($this->getType(), $CFG_GLPI['contract_types'])) { $ci = new Contract_Item(); $ci->cleanDBonItemDelete($this->getType(), $this->fields['id']); } // If this type have DOCUMENT, clean one associated to purged item if (Document::canApplyOn($this)) { $di = new Document_Item(); $di->cleanDBonItemDelete($this->getType(), $this->fields['id']); } // If this type have NOTEPAD, clean one associated to purged item if ($this->usenotepad) { $note = new Notepad(); $note->cleanDBonItemDelete($this->getType(), $this->fields['id']); } // Delete relations with KB if (in_array($this->getType(), $CFG_GLPI['kb_types'])) { $kbitem_item = new KnowbaseItem_Item(); $kbitem_item->cleanDBonItemDelete($this->getType(), $this->fields['id']); } if (in_array($this->getType(), $CFG_GLPI['ticket_types'])) { //delete relation beetween item and changes/problems $this->deleteChildrenAndRelationsFromDb( [ Change_Item::class, Item_Problem::class, ] ); } if (in_array($this->getType(), $CFG_GLPI['rackable_types'])) { //delete relation beetween rackable type and its rack $item_rack = new Item_Rack(); $item_rack->deleteByCriteria( [ 'itemtype' => $this->getType(), 'items_id' => $this->fields['id'] ] ); $item_enclosure = new Item_Enclosure(); $item_enclosure->deleteByCriteria( [ 'itemtype' => $this->getType(), 'items_id' => $this->fields['id'] ] ); } if (in_array($this->getType(), $CFG_GLPI['cluster_types'])) { //delete relation beetween clusterable elements type and their cluster $this->deleteChildrenAndRelationsFromDb( [ Item_Cluster::class, ] ); } if (in_array($this->getType(), $CFG_GLPI['operatingsystem_types'])) { $this->deleteChildrenAndRelationsFromDb([ Item_OperatingSystem::class ]); } if (in_array($this->getType(), $CFG_GLPI['software_types'])) { $this->deleteChildrenAndRelationsFromDb([ Item_SoftwareVersion::class ]); } if (in_array($this->getType(), $CFG_GLPI['kanban_types'])) { $this->deleteChildrenAndRelationsFromDb([ Item_Kanban::class ]); } if (in_array($this->getType(), $CFG_GLPI['domain_types'])) { $this->deleteChildrenAndRelationsFromDb([ Domain_Item::class ]); } } /** * Actions done when item flag deleted is set to an item * * @return void **/ function cleanDBonMarkDeleted() { } /** * Save the input data in the Session * * @since 0.84 * * @return void **/ protected function saveInput() { $_SESSION['saveInput'][$this->getType()] = $this->input; } /** * Clear the saved data stored in the session * * @since 0.84 * * @return void **/ protected function clearSavedInput() { unset($_SESSION['saveInput'][$this->getType()]); } /** * Get the data saved in the session * * @since 0.84 * * @param array $default Array of value used if session is empty * * @return array Array of value **/ protected function restoreInput(Array $default = []) { if (isset($_SESSION['saveInput'][$this->getType()])) { $saved = Html::cleanPostForTextArea($_SESSION['saveInput'][$this->getType()]); // clear saved data when restored (only need once) $this->clearSavedInput(); return $saved; } return $default; } // Common functions /** * Add an item in the database with all it's items. * * @param array $input the _POST vars returned by the item form when press add * @param array $options with the insert options * - unicity_message : do not display message if item it a duplicate (default is yes) * @param boolean $history do history log ? (true by default) * * @return integer the new ID of the added item (or false if fail) **/ function add(array $input, $options = [], $history = true) { global $DB, $CFG_GLPI; if ($DB->isSlave()) { return false; } // This means we are not adding a cloned object if (!isset($input['clone'])) { // This means we are asked to clone the object (old way). This will clone the clone method // that will set the clone parameter to true if (isset($input['_oldID'])) { $id_to_clone = $input['_oldID']; } if (isset($input['id'])) { $id_to_clone = $input['id']; } if (isset($id_to_clone) && $this->getFromDB($id_to_clone)) { if ($clone_id = $this->clone($input, $history)) { $this->getFromDB($clone_id); // Load created items fields } return $clone_id; } } // Store input in the object to be available in all sub-method / hook $this->input = $input; // Manage the _no_history if (!isset($this->input['_no_history'])) { $this->input['_no_history'] = !$history; } if (isset($this->input['add'])) { // Input from the interface // Save this data to be available if add fail $this->saveInput(); } // Call the plugin hook - $this->input can be altered // This hook get the data from the form, not yet altered Plugin::doHook("pre_item_add", $this); if ($this->input && is_array($this->input)) { if (isset($this->input['add'])) { $this->input['_add'] = $this->input['add']; unset($this->input['add']); } $this->input = $this->prepareInputForAdd($this->input); } if ($this->input && is_array($this->input)) { // Call the plugin hook - $this->input can be altered // This hook get the data altered by the object method Plugin::doHook("post_prepareadd", $this); } if ($this->input && is_array($this->input)) { //Check values to inject $this->filterValues(!isCommandLine()); } //Process business rules for assets $this->assetBusinessRules(\RuleAsset::ONADD); if ($this->input && is_array($this->input)) { $this->fields = []; $table_fields = $DB->listFields($this->getTable()); // fill array for add foreach (array_keys($this->input) as $key) { if (($key[0] != '_') && isset($table_fields[$key])) { $this->fields[$key] = $this->input[$key]; } } // Auto set date_creation if exsist if (isset($table_fields['date_creation']) && !isset($this->input['date_creation'])) { $this->fields['date_creation'] = $_SESSION["glpi_currenttime"]; } // Auto set date_mod if exsist if (isset($table_fields['date_mod']) && !isset($this->input['date_mod'])) { $this->fields['date_mod'] = $_SESSION["glpi_currenttime"]; } if ($this->checkUnicity(true, $options)) { if ($this->addToDB() !== false) { $this->post_addItem(); $this->addMessageOnAddAction(); if ($this->dohistory && $history) { $changes = [ 0, '', '', ]; Log::history($this->fields["id"], $this->getType(), $changes, 0, Log::HISTORY_CREATE_ITEM); } // Auto create infocoms if (isset($CFG_GLPI["auto_create_infocoms"]) && $CFG_GLPI["auto_create_infocoms"] && Infocom::canApplyOn($this)) { $ic = new Infocom(); if (!$ic->getFromDBforDevice($this->getType(), $this->fields['id'])) { $ic->add(['itemtype' => $this->getType(), 'items_id' => $this->fields['id']]); } } // If itemtype is in infocomtype and if states_id field is filled // and item is not a template if (Infocom::canApplyOn($this) && isset($this->input['states_id']) && (!isset($this->input['is_template']) || !$this->input['is_template'])) { //Check if we have to automatical fill dates Infocom::manageDateOnStatusChange($this); } Plugin::doHook("item_add", $this); // As add have suceed, clean the old input value if (isset($this->input['_add'])) { $this->clearSavedInput(); } if ($this->notificationqueueonaction) { QueuedNotification::forceSendFor($this->getType(), $this->fields['id']); } return $this->fields['id']; } } } return false; } /** * Clones the current item * * @since 9.5 * * @param array $override_input custom input to override * @param boolean $history do history log ? (true by default) * * @return integer the new ID of the clone (or false if fail) */ function clone(array $override_input = [], bool $history = true) { global $DB, $CFG_GLPI; if ($DB->isSlave()) { return false; } $new_item = new static(); $input = Toolbox::addslashes_deep($this->fields); foreach ($override_input as $key => $value) { $input[$key] = $value; } $input = $new_item->prepareInputForClone($input); if (isset($input['id'])) { $input['_oldID'] = $input['id']; unset($input['id']); } unset($input['date_creation']); unset($input['date_mod']); if (isset($input['template_name'])) { unset($input['template_name']); } if (isset($input['is_template'])) { unset($input['is_template']); } $input['clone'] = true; $newID = $new_item->add($input, [], $history); // If the item needs post clone (recursive cloning for example) $new_item->post_clone($this, $history); return $newID; } /** * Get the link to an item * * @param array $options array of options * - comments : boolean / display comments * - complete : boolean / display completename instead of name * - additional : boolean / display additionals information * - linkoption : string / additional options to add to * * @return string HTML link **/ function getLink($options = []) { $p = [ 'linkoption' => '', ]; if (isset($options['linkoption'])) { $p['linkoption'] = $options['linkoption']; } if (!isset($this->fields['id'])) { return ''; } if ($this->no_form_page || !$this->can($this->fields['id'], READ)) { return $this->getNameID($options); } $link = $this->getLinkURL(); $label = $this->getNameID($options); $title = ''; if (!preg_match('/title=/', $p['linkoption'])) { $thename = $this->getName(['complete' => true]); if ($thename != NOT_AVAILABLE) { $title = ' title="' . htmlentities($thename, ENT_QUOTES, 'utf-8') . '"'; } } return "$label"; } /** * Get the link url to an item * * @return string HTML link **/ function getLinkURL() { if (!isset($this->fields['id'])) { return ''; } $link = $this->getFormURLWithID($this->getID()); $link .= ($this->isTemplate() ? "&withtemplate=1" : ""); return $link; } /** * Add a message on add action * * @return void **/ function addMessageOnAddAction() { $addMessAfterRedirect = false; if (isset($this->input['_add'])) { $addMessAfterRedirect = true; } if (isset($this->input['_no_message']) || !$this->auto_message_on_action) { $addMessAfterRedirect = false; } if ($addMessAfterRedirect) { $link = $this->getFormURL(); if (!isset($link)) { return; } if ($this->getName() == NOT_AVAILABLE) { //TRANS: %1$s is the itemtype, %2$d is the id of the item $this->fields['name'] = sprintf(__('%1$s - ID %2$d'), $this->getTypeName(1), $this->fields['id']); } $display = (isset($this->input['_no_message_link'])?$this->getNameID() :$this->getLink()); // Do not display quotes //TRANS : %s is the description of the added item Session::addMessageAfterRedirect(sprintf(__('%1$s: %2$s'), __('Item successfully added'), stripslashes($display))); } } /** * Add needed information to $input (example entities_id) * * @param array $input datas used to add the item * * @since 0.84 * * @return array the modified $input array **/ function addNeededInfoToInput($input) { return $input; } /** * Prepare input datas for adding the item * * @param array $input datas used to add the item * * @return array the modified $input array **/ function prepareInputForAdd($input) { return $input; } /** * Prepare input datas for cloning the item * * @since 9.5 * * @param array $input datas used to add the item * * @return array the modified $input array **/ function prepareInputForClone($input) { unset($input['id']); unset($input['date_mod']); unset($input['date_creation']); return $input; } /** * Actions done after the ADD of the item in the database * * @return void **/ function post_addItem() { } /** * Actions done after the clone of the item in the database * * @since 9.5 * * @param $source the item that is being cloned * @param $history do history log ? * * @return void **/ function post_clone($source, $history) { } /** * Update some elements of an item in the database. * * @param array $input the _POST vars returned by the item form when press update * @param boolean $history do history log ? (default 1) * @param array $options with the insert options * * @return boolean true on success **/ function update(array $input, $history = 1, $options = []) { global $DB, $GLPI_CACHE; if ($DB->isSlave()) { return false; } if (!$this->getFromDB($input[static::getIndexName()])) { return false; } // Store input in the object to be available in all sub-method / hook $this->input = $input; // Manage the _no_history if (!isset($this->input['_no_history'])) { $this->input['_no_history'] = !$history; } // Plugin hook - $this->input can be altered Plugin::doHook("pre_item_update", $this); if ($this->input && is_array($this->input)) { $this->input = $this->prepareInputForUpdate($this->input); if (isset($this->input['update'])) { $this->input['_update'] = $this->input['update']; unset($this->input['update']); } $this->filterValues(!isCommandLine()); } //Process business rules for assets $this->assetBusinessRules(\RuleAsset::ONUPDATE); // Valid input for update if ($this->checkUnicity(false, $options)) { if ($this->input && is_array($this->input)) { // Fill the update-array with changes $x = 0; $this->updates = []; $this->oldvalues = []; foreach (array_keys($this->input) as $key) { if (array_key_exists($key, $this->fields)) { // Prevent history for date statement (for date for example) if (is_null($this->fields[$key]) && ($this->input[$key] == 'NULL')) { $this->fields[$key] = 'NULL'; } // Compare item $ischanged = true; $searchopt = $this->getSearchOptionByField('field', $key, $this->getTable()); if (isset($searchopt['datatype'])) { switch ($searchopt['datatype']) { case 'string' : case 'text' : $ischanged = (strcmp($DB->escape($this->fields[$key]), $this->input[$key]) != 0); break; case 'itemlink' : if ($key == 'name') { $ischanged = (strcmp($DB->escape($this->fields[$key]), $this->input[$key]) != 0); break; } // else default default : $ischanged = ($DB->escape($this->fields[$key]) != $this->input[$key]); break; } } else { // No searchoption case $ischanged = ($DB->escape($this->fields[$key]) != $this->input[$key]); } if ($ischanged) { if ($key != "id") { // Store old values if (!in_array($key, $this->history_blacklist)) { $this->oldvalues[$key] = $this->fields[$key]; } $this->fields[$key] = $this->input[$key]; $this->updates[$x] = $key; $x++; } } } } if (count($this->updates)) { if (array_key_exists('date_mod', $this->fields)) { // is a non blacklist field exists if (count(array_diff($this->updates, $this->history_blacklist)) > 0) { $this->fields['date_mod'] = $_SESSION["glpi_currenttime"]; $this->updates[$x++] = 'date_mod'; } } $this->pre_updateInDB(); if (count($this->updates)) { if ($this->updateInDB($this->updates, ($this->dohistory && $history ? $this->oldvalues : []))) { $this->addMessageOnUpdateAction(); Plugin::doHook("item_update", $this); //Fill forward_entity_to array with itemtypes coming from plugins if (isset(self::$plugins_forward_entity[$this->getType()])) { foreach (self::$plugins_forward_entity[$this->getType()] as $itemtype) { static::$forward_entity_to[] = $itemtype; } } // forward entity information if needed if (count(static::$forward_entity_to) && (in_array("entities_id", $this->updates) || in_array("is_recursive", $this->updates))) { $this->forwardEntityInformations(); } // If itemtype is in infocomtype and if states_id field is filled // and item not a template if (Infocom::canApplyOn($this) && in_array('states_id', $this->updates) && ($this->getField('is_template') != NOT_AVAILABLE)) { //Check if we have to automatical fill dates Infocom::manageDateOnStatusChange($this, false); } } } } $this->post_updateItem($history); if ($this->notificationqueueonaction) { QueuedNotification::forceSendFor($this->getType(), $this->fields['id']); } return true; } } return false; } /** * Forward entity information to linked items * * @return void **/ protected function forwardEntityInformations() { global $DB; if (!isset($this->fields['id']) || !($this->fields['id'] >= 0)) { return false; } if (count(static::$forward_entity_to)) { foreach (static::$forward_entity_to as $type) { $item = new $type(); $query = [ 'SELECT' => ['id'], 'FROM' => $item->getTable() ]; $OR = []; if ($item->isField('itemtype')) { $OR[] = [ 'itemtype' => $this->getType(), 'items_id' => $this->getID() ]; } if ($item->isField($this->getForeignKeyField())) { $OR[] = [$this->getForeignKeyField() => $this->getID()]; } $query['WHERE'][] = ['OR' => $OR]; $input = [ 'entities_id' => $this->getEntityID(), '_transfer' => 1 ]; if ($this->maybeRecursive()) { $input['is_recursive'] = $this->isRecursive(); } $iterator = $DB->request($query); while ($data = $iterator->next()) { $input['id'] = $data['id']; // No history for such update $item->update($input, 0); } } } } /** * Add a message on update action * * @return void **/ function addMessageOnUpdateAction() { $addMessAfterRedirect = false; if (isset($this->input['_update'])) { $addMessAfterRedirect = true; } if (isset($this->input['_no_message']) || !$this->auto_message_on_action) { $addMessAfterRedirect = false; } if ($addMessAfterRedirect) { $link = $this->getFormURL(); if (!isset($link)) { return; } // Do not display quotes if (isset($this->fields['name'])) { $this->fields['name'] = stripslashes($this->fields['name']); } else { //TRANS: %1$s is the itemtype, %2$d is the id of the item $this->fields['name'] = sprintf(__('%1$s - ID %2$d'), $this->getTypeName(1), $this->fields['id']); } if (isset($this->input['_no_message_link'])) { $display = $this->getNameID(); } else { $display = $this->getLink(); } //TRANS : %s is the description of the updated item Session::addMessageAfterRedirect(sprintf(__('%1$s: %2$s'), __('Item successfully updated'), $display)); } } /** * Prepare input datas for updating the item * * @param array $input data used to update the item * * @return array the modified $input array **/ function prepareInputForUpdate($input) { return $input; } /** * Actions done after the UPDATE of the item in the database * * @param boolean $history store changes history ? (default 1) * * @return void **/ function post_updateItem($history = 1) { } /** * Actions done before the UPDATE of the item in the database * * @return void **/ function pre_updateInDB() { } /** * Delete an item in the database. * * @param array $input the _POST vars returned by the item form when press delete * @param boolean $force force deletion (default 0) * @param boolean $history do history log ? (default 1) * * @return boolean true on success **/ function delete(array $input, $force = 0, $history = 1) { global $DB; if ($DB->isSlave()) { return false; } if (!$this->getFromDB($input[static::getIndexName()])) { return false; } // Force purge for templates / may not to be deleted / not dynamic lockable items if ($this->isTemplate() || !$this->maybeDeleted() // Do not take into account deleted field if maybe dynamic but not dynamic || ($this->useDeletedToLockIfDynamic() && !$this->isDynamic())) { $force = 1; } // Store input in the object to be available in all sub-method / hook $this->input = $input; if (isset($this->input['purge'])) { $this->input['_purge'] = $this->input['purge']; unset($this->input['purge']); } else if ($force) { $this->input['_purge'] = 1; $this->input['_no_message'] = $this->input['_no_message'] ?? 1; } if (isset($this->input['delete'])) { $this->input['_delete'] = $this->input['delete']; unset($this->input['delete']); } else if (!$force) { $this->input['_delete'] = 1; $this->input['_no_message'] = $this->input['_no_message'] ?? 1; } if (!isset($this->input['_no_history'])) { $this->input['_no_history'] = !$history; } // Purge if ($force) { Plugin::doHook("pre_item_purge", $this); } else { Plugin::doHook("pre_item_delete", $this); } if (!is_array($this->input)) { // $input clear by a hook to cancel delete return false; } if ($this->pre_deleteItem()) { if ($this->deleteFromDB($force)) { if ($force) { $this->addMessageOnPurgeAction(); $this->post_purgeItem(); Plugin::doHook("item_purge", $this); Impact::clean($this); } else { $this->addMessageOnDeleteAction(); if ($this->dohistory && $history) { $changes = [ 0, '', '', ]; $logaction = Log::HISTORY_DELETE_ITEM; if ($this->useDeletedToLockIfDynamic() && $this->isDynamic()) { $logaction = Log::HISTORY_LOCK_ITEM; } Log::history($this->fields["id"], $this->getType(), $changes, 0, $logaction); } $this->post_deleteItem(); Plugin::doHook("item_delete", $this); } if ($this->notificationqueueonaction) { QueuedNotification::forceSendFor($this->getType(), $this->fields['id']); } return true; } } return false; } /** * Actions done after the DELETE (mark as deleted) of the item in the database * * @return void **/ function post_deleteItem() { } /** * Actions done after the PURGE of the item in the database * * @return void **/ function post_purgeItem() { } /** * Add a message on delete action * * @return void **/ function addMessageOnDeleteAction() { if (!$this->maybeDeleted()) { return; } $addMessAfterRedirect = false; if (isset($this->input['_delete'])) { $addMessAfterRedirect = true; } if (isset($this->input['_no_message']) || !$this->auto_message_on_action) { $addMessAfterRedirect = false; } if ($addMessAfterRedirect) { $link = $this->getFormURL(); if (!isset($link)) { return; } if (isset($this->input['_no_message_link'])) { $display = $this->getNameID(); } else { $display = $this->getLink(); } //TRANS : %s is the description of the updated item Session::addMessageAfterRedirect(sprintf(__('%1$s: %2$s'), __('Item successfully deleted'), $display)); } } /** * Add a message on purge action * * @return void **/ function addMessageOnPurgeAction() { $addMessAfterRedirect = false; if (isset($this->input['_purge']) || isset($this->input['_delete'])) { $addMessAfterRedirect = true; } if (isset($this->input['_purge'])) { $this->input['_no_message_link'] = true; } if (isset($this->input['_no_message']) || !$this->auto_message_on_action) { $addMessAfterRedirect = false; } if ($addMessAfterRedirect) { $link = $this->getFormURL(); if (!isset($link)) { return; } if (isset($this->input['_no_message_link'])) { $display = $this->getNameID(); } else { $display = $this->getLink(); } //TRANS : %s is the description of the updated item Session::addMessageAfterRedirect(sprintf(__('%1$s: %2$s'), __('Item successfully purged'), $display)); } } /** * Actions done before the DELETE of the item in the database / * Maybe used to add another check for deletion * * @return boolean true if item need to be deleted else false **/ function pre_deleteItem() { return true; } /** * Restore an item put in the trashbin in the database. * * @param array $input the _POST vars returned by the item form when press restore * @param boolean $history do history log ? (default 1) * * @return boolean true on success **/ function restore(array $input, $history = 1) { if (!$this->getFromDB($input[static::getIndexName()])) { return false; } if (isset($input['restore'])) { $input['_restore'] = $input['restore']; unset($input['restore']); } else { $this->input['_restore'] = 1; $this->input['_no_message'] = $this->input['_no_message'] ?? 1; } // Store input in the object to be available in all sub-method / hook $this->input = $input; Plugin::doHook("pre_item_restore", $this); if (!is_array($this->input)) { // $input clear by a hook to cancel retore return false; } if ($this->restoreInDB()) { $this->addMessageOnRestoreAction(); if ($this->dohistory && $history) { $changes = [ 0, '', '', ]; $logaction = Log::HISTORY_RESTORE_ITEM; if ($this->useDeletedToLockIfDynamic() && $this->isDynamic()) { $logaction = Log::HISTORY_UNLOCK_ITEM; } Log::history($this->input["id"], $this->getType(), $changes, 0, $logaction); } $this->post_restoreItem(); Plugin::doHook("item_restore", $this); if ($this->notificationqueueonaction) { QueuedNotification::forceSendFor($this->getType(), $this->fields['id']); } return true; } return false; } /** * Actions done after the restore of the item * * @return void **/ function post_restoreItem() { } /** * Add a message on restore action * * @return void **/ function addMessageOnRestoreAction() { $addMessAfterRedirect = false; if (isset($this->input['_restore'])) { $addMessAfterRedirect = true; } if (isset($this->input['_no_message']) || !$this->auto_message_on_action) { $addMessAfterRedirect = false; } if ($addMessAfterRedirect) { $link = $this->getFormURL(); if (!isset($link)) { return; } if (isset($this->input['_no_message_link'])) { $display = $this->getNameID(); } else { $display = $this->getLink(); } //TRANS : %s is the description of the updated item Session::addMessageAfterRedirect(sprintf(__('%1$s: %2$s'), __('Item successfully restored'), $display)); } } /** * Reset fields of the item * * @return void **/ function reset() { $this->fields = []; } /** * Have I the global right to add an item for the Object * May be overloaded if needed (ex Ticket) * * @since 0.83 * * @param string $type itemtype of object to add * * @return boolean **/ function canAddItem($type) { return $this->can($this->getID(), UPDATE); } /** * Have I the right to "create" the Object * * Default is true and check entity if the objet is entity assign * * May be overloaded if needed * * @return boolean **/ function canCreateItem() { if (!$this->checkEntity()) { return false; } return true; } /** * Have I the right to "update" the Object * * Default is true and check entity if the objet is entity assign * * May be overloaded if needed * * @return boolean **/ function canUpdateItem() { if (!$this->checkEntity()) { return false; } return true; } /** * Have I the right to "delete" the Object * * Default is true and check entity if the objet is entity assign * * May be overloaded if needed * * @return boolean **/ function canDeleteItem() { if (!$this->checkEntity()) { return false; } return true; } /** * Have I the right to "purge" the Object * * Default is true and check entity if the objet is entity assign * * @since 0.85 * * @return boolean **/ function canPurgeItem() { if (!$this->checkEntity()) { return false; } // Can purge an object with Infocom only if can purge Infocom if (Infocom::canApplyOn($this)) { $infocom = new Infocom(); if ($infocom->getFromDBforDevice($this->getType(), $this->fields['id'])) { return $infocom->canPurge(); } } return true; } /** * Have I the right to "view" the Object * May be overloaded if needed * * @return boolean **/ function canViewItem() { if (!$this->checkEntity(true)) { return false; } // else : Global item return true; } /** * Have i right to see action button * * @param integer $ID ID to check * * @since 0.85 * * @return boolean **/ function canEdit($ID) { if ($this->maybeDeleted()) { return ($this->can($ID, CREATE) || $this->can($ID, UPDATE) || $this->can($ID, DELETE) || $this->can($ID, PURGE)); } return ($this->can($ID, CREATE) || $this->can($ID, UPDATE) || $this->can($ID, PURGE)); } /** * Can I change recursive flag to false * check if there is "linked" object in another entity * * May be overloaded if needed * * @return boolean **/ function canUnrecurs() { global $DB; $ID = $this->fields['id']; if (($ID < 0) || !$this->fields['is_recursive']) { return true; } $entities = getAncestorsOf('glpi_entities', $this->fields['entities_id']); $entities[] = $this->fields['entities_id']; $RELATION = getDbRelations(); if ($this instanceof CommonTreeDropdown) { $f = getForeignKeyFieldForTable($this->getTable()); if (countElementsInTable($this->getTable(), [ $f => $ID, 'NOT' => [ 'entities_id' => $entities ]]) > 0) { return false; } } if (isset($RELATION[$this->getTable()])) { foreach ($RELATION[$this->getTable()] as $tablename => $field) { if ($tablename[0] != '_') { $itemtype = getItemTypeForTable($tablename); $item = new $itemtype(); if ($item->isEntityAssign()) { // 1->N Relation if (is_array($field)) { foreach ($field as $f) { if (countElementsInTable($tablename, [ $f => $ID, 'NOT' => [ 'entities_id' => $entities ]]) > 0) { return false; } } } else { if (countElementsInTable($tablename, [ $field => $ID, 'NOT' => [ 'entities_id' => $entities ]]) > 0) { return false; } } } else { foreach ($RELATION as $othertable => $rel) { // Search for a N->N Relation with devices if (($othertable == "_virtual_device") && isset($rel[$tablename])) { $devfield = $rel[$tablename][0]; // items_id... $typefield = $rel[$tablename][1]; // itemtype... $iterator = $DB->request([ 'SELECT' => $typefield, 'DISTINCT' => true, 'FROM' => $tablename, 'WHERE' => [$field => $ID] ]); // Search linked device of each type while ($data = $iterator->next()) { $itemtype = $data[$typefield]; $itemtable = getTableForItemType($itemtype); $item = new $itemtype(); if ($item->isEntityAssign()) { if (countElementsInTable([$tablename, $itemtable], ["$tablename.$field" => $ID, "$tablename.$typefield" => $itemtype, 'FKEY' => [$tablename => $devfield, $itemtable => 'id'], 'NOT' => [$itemtable.'.entities_id' => $entities ]]) > '0') { return false; } } } } else if (($othertable != $this->getTable()) && isset($rel[$tablename])) { // Search for another N->N Relation $itemtype = getItemTypeForTable($othertable); $item = new $itemtype(); if ($item->isEntityAssign()) { if (is_array($rel[$tablename])) { foreach ($rel[$tablename] as $otherfield) { if (countElementsInTable([$tablename, $othertable], ["$tablename.$field" => $ID, 'FKEY' => [$tablename => $otherfield, $othertable => 'id'], 'NOT' => [$othertable.'.entities_id' => $entities ]]) > '0') { return false; } } } else { $otherfield = $rel[$tablename]; if (countElementsInTable([$tablename, $othertable], ["$tablename.$field" => $ID, 'FKEY' => [$tablename => $otherfield, $othertable =>'id'], 'NOT' => [ $othertable.'.entities_id' => $entities ]]) > '0') { return false; } } } } } } } } } // Doc links to this item if (($this->getType() > 0) && countElementsInTable(['glpi_documents_items', 'glpi_documents'], ['glpi_documents_items.items_id'=> $ID, 'glpi_documents_items.itemtype'=> $this->getType(), 'FKEY' => ['glpi_documents_items' => 'documents_id','glpi_documents' => 'id'], 'NOT' => ['glpi_documents.entities_id' => $entities]]) > '0') { return false; } // TODO : do we need to check all relations in $RELATION["_virtual_device"] for this item // check connections of a computer $connectcomputer = ['Monitor', 'Peripheral', 'Phone', 'Printer']; if (in_array($this->getType(), $connectcomputer)) { return Computer_Item::canUnrecursSpecif($this, $entities); } return true; } /** * check if this action can be done on this field of this item by massive actions * * @since 0.83 * * @param string $action name of the action * @param integer $field id of the field * @param string $value value of the field * * @return boolean **/ function canMassiveAction($action, $field, $value) { return true; } /** * @since 9.1 * * @param array $options Options * * @return boolean **/ function showDates($options = []) { $isNewID = ((isset($options['withtemplate']) && ($options['withtemplate'] == 2)) || $this->isNewID($this->getID())); if ($isNewID) { return true; } $date_creation_exists = ($this->getField('date_creation') != NOT_AVAILABLE); $date_mod_exists = ($this->getField('date_mod') != NOT_AVAILABLE); $colspan = $options['colspan']; if ((!isset($options['withtemplate']) || ($options['withtemplate'] == 0)) && !empty($this->fields['template_name'])) { $colspan = 1; } echo "