<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2021 Teclib' and contributors.
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * GLPI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 */

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) {
         trigger_error(
            sprintf(
               'getFromDBByCrit expects to get one result, %1$s found in query "%2$s".',
               count($iter),
               $iter->getSql()
            ),
            E_USER_WARNING
         );
      }
      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);
         $this->getFromDB($this->fields['id']);
      }

      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();
            }

            $this->getFromDB($this->fields['id']);

            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;
   }

   /**
    * Restore data saved in the session to $this->input
    *
    * @since 9.5.5
    *
    * @param array $saved Array of values saved in session
    *
    * @return array Array of values
   **/
   protected function restoreSavedValues(Array $saved = []) {
      if (count($saved)) {
         //restore saved values as input (to manage uploaded img)
         $this->input = $saved;

         foreach ($saved as $name => $value) {
            if (isset($this->fields[$name])) {
               $this->fields[$name] = $saved[$name];
            }
         }
      }
   }


   // 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 <a>
    *
    * @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 "<a ".$p['linkoption']." href='$link' $title>$label</a>";
   }


   /**
    * 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 (!array_key_exists(static::getIndexName(), $input)) {
         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;
      }

      if (isset($this->input['update'])) {
         // Input from the interface
         // Save this data to be available if add fail
         $this->saveInput();
      }

      // 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);

                     // As update have suceed, clean the old input value
                     if (isset($this->input['_update'])) {
                        $this->clearSavedInput();
                     }

                     //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 "<tr class='tab_bg_1 footerRow'>";
      //Display when it's not a new asset being created
      if ($date_creation_exists
         && $this->getID() > 0
            && (!isset($options['withtemplate']) || $options['withtemplate'] == 0)) {
         echo "<th colspan='$colspan'>";
         printf(__('Created on %s'), Html::convDateTime($this->fields["date_creation"]));
         echo "</th>";
      } else if (!isset($options['withtemplate']) || $options['withtemplate'] == 0) {
         echo "<th colspan='$colspan'>";
         echo "</th>";
      }

      if (isset($options['withtemplate']) && $options['withtemplate']) {
         echo "<th colspan='$colspan'>";
         //TRANS: %s is the datetime of insertion
         printf(__('Created on %s'), Html::convDateTime($_SESSION["glpi_currenttime"]));
         echo "</th>";
      }

      if ($date_mod_exists) {
         echo "<th colspan='$colspan'>";
         //TRANS: %s is the datetime of update
         printf(__('Last update on %s'), Html::convDateTime($this->fields["date_mod"]));
         echo "</th>";
      } else {
         echo "<th colspan='$colspan'>";
         echo "</th>";
      }

      if ((!isset($options['withtemplate']) || ($options['withtemplate'] == 0))
          && !empty($this->fields['template_name'])) {
         echo "<th colspan='".($colspan * 2)."'>";
         printf(__('Created from the template %s'), $this->fields['template_name']);
         echo "</th>";
      }

      echo "</tr>";
   }

   /**
    * Display a 2 columns Footer for Form buttons
    * Close the form is user can edit
    *
    * @param array $options array of possible options:
    *     - withtemplate : 1 for newtemplate, 2 for newobject from template
    *     - colspan for each column (default 2)
    *     - candel : set to false to hide "delete" button
    *     - canedit : set to false to hide all buttons
    *     - addbuttons : array of buttons to add
    *
    * @return void
   **/
   function showFormButtons($options = []) {

      // for single object like config
      if (isset($this->fields['id'])) {
         $ID = $this->fields['id'];
      } else {
         $ID = 1;
      }

      $params = [
         'colspan'      => 2,
         'withtemplate' => '',
         'candel'       => true,
         'canedit'      => true,
         'addbuttons'   => [],
         'formfooter'   => null,
      ];

      if (is_array($options) && count($options)) {
         foreach ($options as $key => $val) {
            $params[$key] = $val;
         }
      }

      Plugin::doHook("post_item_form", ['item' => $this, 'options' => &$params]);

      if ($params['formfooter'] === null) {
          $this->showDates($params);
      }

      if (!$params['canedit']
          || !$this->canEdit($ID)) {
         echo "</table></div>";
         // Form Header always open form
         Html::closeForm();
         return false;
      }

      echo "<tr class='tab_bg_2'>";

      if ($params['withtemplate']
          ||$this->isNewID($ID)) {

         echo "<td class='center' colspan='".($params['colspan']*2)."'>";

         if (($ID <= 0) || ($params['withtemplate'] == 2)) {
            echo Html::submit(
               "<i class='fas fa-plus'></i>&nbsp;"._x('button', 'Add'),
               ['name' => 'add']
            );
         } else {
            //TRANS : means update / actualize
            echo Html::submit(
               "<i class='fas fa-save'></i>&nbsp;"._x('button', 'Save'),
               ['name' => 'update']
            );
         }

      } else {
         if ($params['candel']
             && !$this->can($ID, DELETE)
             && !$this->can($ID, PURGE)) {
            $params['candel'] = false;
         }

         if ($params['canedit'] && $this->can($ID, UPDATE)) {
            echo "<td class='center' colspan='".($params['colspan']*2)."'>\n";
            echo Html::submit(
               "<i class='fas fa-save'></i>&nbsp;"._x('button', 'Save'),
               ['name' => 'update']
            );
         }

         if ($params['candel']) {
            if ($params['canedit'] && $this->can($ID, UPDATE)) {
               echo "</td></tr><tr class='tab_bg_2'>\n";
            }
            if ($this->isDeleted()) {
               if ($this->can($ID, DELETE)) {
                  echo "<td class='right' colspan='".($params['colspan']*2)."' >\n";
                  echo Html::submit(
                     "<i class='fas fa-trash-restore'></i>&nbsp;"._x('button', 'Restore'),
                     ['name' => 'restore']
                  );
               }

               if ($this->can($ID, PURGE)) {
                  echo "<span class='very_small_space'>";
                  if (in_array($this->getType(), Item_Devices::getConcernedItems())) {
                     Html::showToolTip(__('Check to keep the devices while deleting this item'));
                     echo "&nbsp;";
                     echo "<input type='checkbox' name='keep_devices' value='1'";
                     if (!empty($_SESSION['glpikeep_devices_when_purging_item'])) {
                        echo " checked";
                     }
                     echo ">&nbsp;";
                  }
                  echo Html::submit(
                     "<i class='fas fa-trash-alt'></i>&nbsp;"._x('button', 'Delete permanently'),
                     ['name' => 'purge']
                  );
                  echo "</span>";
               }

            } else {
               echo "<td class='right' colspan='".($params['colspan']*2)."' >\n";
               // If maybe dynamic : do not take into account  is_deleted  field
               if (!$this->maybeDeleted()
                   || $this->useDeletedToLockIfDynamic()) {
                  if ($this->can($ID, PURGE)) {
                     echo Html::submit(
                        "<i class='fas fa-trash-alt'></i>&nbsp;"._x('button', 'Delete permanently'),
                        [
                           'name'    => 'purge',
                           'confirm' => __('Confirm the final deletion?')
                        ]
                     );
                  }
               } else if (!$this->isDeleted()
                          && $this->can($ID, DELETE)) {
                  echo Html::submit(
                     "<i class='fas fa-trash-alt'></i>&nbsp;"._x('button', 'Put in trashbin'),
                     ['name' => 'delete']
                  );
               }
            }

         }
         if ($this->isField('date_mod')) {
            echo "<input type='hidden' name='_read_date_mod' value='".$this->getField('date_mod')."'>";
         }
      }

      if (!$this->isNewID($ID)) {
         echo "<input type='hidden' name='id' value='$ID'>";
      }
      echo "</td>";
      echo "</tr>\n";

      if ($params['canedit']
          && count($params['addbuttons'])) {
         echo "<tr class='tab_bg_2'>";
         echo "<td class='right' colspan='".($params['colspan']*2)."'>";
         foreach ($params['addbuttons'] as $key => $val) {
            echo "<button type='submit' class='vsubmit' name='$key' value='1'>
                  $val
               </button>&nbsp;";
         }
         echo "</td>";
         echo "</tr>";
      }

      // Close for Form
      echo "</table></div>";
      Html::closeForm();
   }


   /**
    * Initialize item and check right before managing the edit form
    *
    * @since 0.84
    *
    * @param integer $ID      ID of the item/template
    * @param array   $options Array of possible options:
    *     - withtemplate : 1 for newtemplate, 2 for newobject from template
    *
    * @return integer|void value of withtemplate option (exit of no right)
   **/
   function initForm($ID, Array $options = []) {

      if (isset($options['withtemplate'])
          && ($options['withtemplate'] == 2)
          && !$this->isNewID($ID)) {
         // Create item from template
         // Check read right on the template
         $this->check($ID, READ);

         // Restore saved input or template data
         $input = $this->restoreInput($this->fields);

         // If entity assign force current entity to manage recursive templates
         if ($this->isEntityAssign()) {
            $input['entities_id'] = $_SESSION['glpiactive_entity'];
         }

         // Check create right
         $this->check(-1, CREATE, $input);

      } else if ($this->isNewID($ID)) {
         // Restore saved input if available
         $input = $this->restoreInput($options);
         // Create item
         $this->check(-1, CREATE, $input);
      } else {
         // Existing item
         $this->check($ID, READ);
      }

      return (isset($options['withtemplate']) ? $options['withtemplate'] : '');
   }


   /**
    *
    * Display a 2 columns Header 1 for ID, 1 for recursivity menu
    * Open the form is user can edit
    *
    * @param array $options array of possible options:
    *     - target for the Form
    *     - withtemplate : 1 for newtemplate, 2 for newobject from template
    *     - colspan for each column (default 2)
    *     - formoptions string (javascript p.e.)
    *     - canedit boolean edit mode of form ?
    *     - formtitle specific form title
    *     - noid Set to true if ID should not be append (eg. already done in formtitle)
    *
    * @return void
   **/
   function showFormHeader($options = []) {

      $ID     = $this->fields['id'];

      $params = [
         'target'       => $this->getFormURL(),
         'colspan'      => 2,
         'withtemplate' => '',
         'formoptions'  => '',
         'canedit'      => true,
         'formtitle'    => null,
         'noid'         => false
      ];

      if (is_array($options) && count($options)) {
         foreach ($options as $key => $val) {
            $params[$key] = $val;
         }
      }

      // Template case : clean entities data
      if (($params['withtemplate'] == 2)
          && $this->isEntityAssign()) {
         $this->fields['entities_id']  = $_SESSION['glpiactive_entity'];
      }

      $rand = mt_rand();
      if ($this->canEdit($ID)) {
         echo "<form name='form' method='post' action='".$params['target']."' ".
                $params['formoptions']." enctype=\"multipart/form-data\">";

         //Should add an hidden entities_id field ?
         //If the table has an entities_id field
         if ($this->isField("entities_id")) {
            //The object type can be assigned to an entity
            if ($this->isEntityAssign()) {
               if (isset($params['entities_id'])) {
                  $entity = $this->fields['entities_id'] = $params['entities_id'];
               } else if (isset($this->fields['entities_id'])) {
                  //It's an existing object to be displayed
                  $entity = $this->fields['entities_id'];
               } else if ($this->isNewID($ID)
                          || ($params['withtemplate'] == 2)) {
                  //It's a new object to be added
                  $entity = $_SESSION['glpiactive_entity'];
               }

               echo "<input type='hidden' name='entities_id' value='$entity'>";

            } else if ($this->getType() != 'User') {
               // For Rules except ruleticket and slalevel
               echo "<input type='hidden' name='entities_id' value='0'>";

            }
         }
      }

      echo "<div class='spaced' id='tabsbody'>";
      echo "<table class='tab_cadre_fixe' id='mainformtable'>";

      if ($params['formtitle'] !== '' && $params['formtitle'] !== false) {
         echo "<tr class='headerRow'><th colspan='".$params['colspan']."'>";

         if (!empty($params['withtemplate']) && ($params['withtemplate'] == 2)
            && !$this->isNewID($ID)) {

            echo "<input type='hidden' name='template_name' value='".$this->fields["template_name"]."'>";

            //TRANS: %s is the template name
            printf(__('Created from the template %s'), $this->fields["template_name"]);

         } else if (!empty($params['withtemplate']) && ($params['withtemplate'] == 1)) {
            echo "<input type='hidden' name='is_template' value='1'>\n";
            echo "<label for='textfield_template_name$rand'>" . __('Template name') . "</label>";
            Html::autocompletionTextField(
               $this,
               'template_name',
               [
                  'size'      => 25,
                  'required'  => true,
                  'rand'      => $rand
               ]
            );
         } else if ($this->isNewID($ID)) {
            $nametype = $params['formtitle'] !== null ? $params['formtitle'] : $this->getTypeName(1);
            printf(__('%1$s - %2$s'), __('New item'), $nametype);
         } else {
            $nametype = $params['formtitle'] !== null ? $params['formtitle'] : $this->getTypeName(1);
            if (!$params['noid'] && ($_SESSION['glpiis_ids_visible'] || empty($nametype))) {
               //TRANS: %1$s is the Itemtype name and $2$d the ID of the item
               $nametype = sprintf(__('%1$s - ID %2$d'), $nametype, $ID);
            }
            echo $nametype;
         }
         $entityname = '';
         if (isset($this->fields["entities_id"])
            && Session::isMultiEntitiesMode()
            && $this->isEntityAssign()) {
            $entityname = Dropdown::getDropdownName("glpi_entities", $this->fields["entities_id"]);
         }

         echo "</th><th colspan='".$params['colspan']."'>";
         if (get_class($this) != 'Entity') {
            if ($this->maybeRecursive()) {
               if (Session::isMultiEntitiesMode()) {
                  echo "<table class='tab_format'><tr class='headerRow responsive_hidden'><th>".$entityname."</th>";
                  echo "<th class='right'><label for='dropdown_is_recursive$rand'>".__('Child entities')."</label></th><th>";
                  if ($params['canedit']) {
                     if ($this instanceof CommonDBChild) {
                        echo Dropdown::getYesNo($this->isRecursive());
                        if (isset($this->fields["is_recursive"])) {
                           echo "<input type='hidden' name='is_recursive' value='".$this->fields["is_recursive"]."'>";
                        }
                        $comment = __("Can't change this attribute. It's inherited from its parent.");
                        // CommonDBChild : entity data is get or copy from parent

                     } else if (!$this->can($ID, 'recursive')) {
                        echo Dropdown::getYesNo($this->fields["is_recursive"]);
                        $comment = __('You are not allowed to change the visibility flag for child entities.');

                     } else if (!$this->canUnrecurs()) {
                        echo Dropdown::getYesNo($this->fields["is_recursive"]);
                        $comment = __('Flag change forbidden. Linked items found.');

                     } else {
                        Dropdown::showYesNo("is_recursive", $this->fields["is_recursive"], -1, ['rand' => $rand]);
                        $comment = __('Change visibility in child entities');
                     }
                     echo " ";
                     Html::showToolTip($comment);
                  } else {
                     echo Dropdown::getYesNo($this->fields["is_recursive"]);
                  }
                  echo "</th></tr></table>";
               } else {
                  echo $entityname;
                  echo "<input type='hidden' name='is_recursive' value='0'>";
               }
            } else {
               echo $entityname;
            }
         }
         echo "</th></tr>\n";
      }

      Plugin::doHook("pre_item_form", ['item' => $this, 'options' => &$params]);

      // If in modal : do not display link on message after redirect
      if (isset($_REQUEST['_in_modal']) && $_REQUEST['_in_modal']) {
         echo "<input type='hidden' name='_no_message_link' value='1'>";
      }

   }


   /**
    * is the parameter ID must be considered as new one ?
    * Default is empty of <0 may be overriden (for entity for example)
    *
    * @param integer $ID ID of the item (-1 if new item)
    *
    * @return boolean
   **/
   static function isNewID($ID) {
      return (empty($ID) || ($ID <= 0));
   }


   /**
    * is the current object a new  one
    *
    * @since 0.83
    *
    * @return boolean
   **/
   function isNewItem() {

      if (isset($this->fields['id'])) {
         return $this->isNewID($this->fields['id']);
      }
      return true;
   }


   /**
    * Check right on an item
    *
    * @param integer $ID    ID of the item (-1 if new item)
    * @param mixed   $right Right to check : r / w / recursive / READ / UPDATE / DELETE
    * @param array   $input array of input data (used for adding item) (default NULL)
    *
    * @return boolean
   **/
   function can($ID, $right, array &$input = null) {
      // Clean ID :
      $ID = Toolbox::cleanInteger($ID);

      // Create process
      if ($this->isNewID($ID)) {
         if (!isset($this->fields['id'])) {
            // Only once
            $this->getEmpty();
         }

         if (is_array($input)) {
            $input = $this->addNeededInfoToInput($input);
            // Copy input field to allow getEntityID() to work
            // from entites_id field or from parent item ref
            foreach ($input as $key => $val) {
               if (isset($this->fields[$key])) {
                  $this->fields[$key] = $val;
               }
            }
            // Store to be available for others functions
            $this->input = $input;
         }

         if ($this->isPrivate()
             && ($this->fields['users_id'] == Session::getLoginUserID())) {
            return true;
         }
         return (static::canCreate() && $this->canCreateItem());

      }
      // else : Get item if not already loaded
      if (!isset($this->fields['id']) || ($this->fields['id'] != $ID)) {
         // Item not found : no right
         if (!$this->getFromDB($ID)) {
            return false;
         }
      }

      /* Hook to restrict user right on current item @since 9.2 */
      $this->right = $right;
      Plugin::doHook("item_can", $this);
      if ($this->right !== $right) {
         return false;
      }
      unset($this->right);

      switch ($right) {
         case READ :
            // Personnal item
            if ($this->isPrivate()
                && ($this->fields['users_id'] === Session::getLoginUserID())) {
               return true;
            }
            return (static::canView() && $this->canViewItem());

         case UPDATE :
            // Personnal item
            if ($this->isPrivate()
                && ($this->fields['users_id'] === Session::getLoginUserID())) {
               return true;
            }
            return (static::canUpdate() && $this->canUpdateItem());

         case DELETE :
            // Personnal item
            if ($this->isPrivate()
                && ($this->fields['users_id'] === Session::getLoginUserID())) {
               return true;
            }
            return (static::canDelete() && $this->canDeleteItem());

         case PURGE :
            // Personnal item
            if ($this->isPrivate()
                && ($this->fields['users_id'] === Session::getLoginUserID())) {
               return true;
            }
            return (static::canPurge() && $this->canPurgeItem());

         case CREATE :
            // Personnal item
            if ($this->isPrivate()
                && ($this->fields['users_id'] === Session::getLoginUserID())) {
               return true;
            }
            return (static::canCreate() && $this->canCreateItem());

         case 'recursive' :
            if ($this->isEntityAssign()
                && $this->maybeRecursive()) {
               if (static::canCreate()
                   && Session::haveAccessToEntity($this->getEntityID())) {
                  // Can make recursive if recursive access to entity
                  return Session::haveRecursiveAccessToEntity($this->getEntityID());
               }
            }
            break;

      }
      return false;
   }


   /**
    * Check right on an item with block
    *
    * @param integer $ID    ID of the item (-1 if new item)
    * @param mixed   $right Right to check : r / w / recursive
    * @param array   $input array of input data (used for adding item) (default NULL)
    *
    * @return void
   **/
   function check($ID, $right, array &$input = null) {

      // Check item exists
      if (!$this->isNewID($ID)
          && (!isset($this->fields['id']) || $this->fields['id'] != $ID)
          && !$this->getFromDB($ID)) {
         // Gestion timeout session
         Session::redirectIfNotLoggedIn();
         Html::displayNotFoundError();

      } else {
         if (!$this->can($ID, $right, $input)) {
            // Gestion timeout session
            Session::redirectIfNotLoggedIn();
            Html::displayRightError();
         }
      }
   }


   /**
    * Check if have right on this entity
    *
    * @param boolean $recursive set true to accept recursive items of ancestors
    *                           of active entities (View case for example) (default false)
    * @since 0.85
    *
    * @return boolean
   **/
   function checkEntity($recursive = false) {

      // Is an item assign to an entity
      if ($this->isEntityAssign()) {
         // Can be recursive check
         if ($recursive && $this->maybeRecursive()) {
            return Session::haveAccessToEntity($this->getEntityID(), $this->isRecursive());
         }
         //  else : No recursive item         // Have access to entity
         return Session::haveAccessToEntity($this->getEntityID());
      }
      // else : Global item
      return true;
   }


   /**
    * Check global right on an object
    *
    * @param mixed $right Right to check : c / r / w / d
    *
    * @return void
   **/
   function checkGlobal($right) {

      if (!$this->canGlobal($right)) {
         // Gestion timeout session
         Session::redirectIfNotLoggedIn();
         Html::displayRightError();
      }
   }


   /**
    * Get global right on an object
    *
    * @param mixed $right Right to check : c / r / w / d / READ / UPDATE / CREATE / DELETE
    *
    * @return void
   **/
   function canGlobal($right) {

      switch ($right) {
         case READ :
            return static::canView();

         case UPDATE :
            return static::canUpdate();

         case CREATE :
            return static::canCreate();

         case DELETE :
            return static::canDelete();

         case PURGE :
            return static::canPurge();

      }

      return false;
   }


   /**
    * Get the ID of entity assigned to the object
    *
    * Can be overloaded (ex : infocom)
    *
    * @return integer ID of the entity
   **/
   function getEntityID() {

      if ($this->isEntityAssign()) {
         return $this->fields["entities_id"];
      }
      return  -1;
   }


   /**
    * Is the object assigned to an entity
    *
    * Can be overloaded (ex : infocom)
    *
    * @return boolean
   **/
   function isEntityAssign() {

      if (!array_key_exists('id', $this->fields)) {
         $this->getEmpty();
      }
      return array_key_exists('entities_id', $this->fields);
   }


   /**
    * Is the object may be recursive
    *
    * Can be overloaded (ex : infocom)
    *
    * @return boolean
   **/
   function maybeRecursive() {

      if (!array_key_exists('id', $this->fields)) {
         $this->getEmpty();
      }
      return array_key_exists('is_recursive', $this->fields);
   }


   /**
    * Is the object recursive
    *
    * Can be overloaded (ex : infocom)
    *
    * @return boolean
   **/
   function isRecursive() {

      if ($this->maybeRecursive()) {
         return $this->fields["is_recursive"];
      }
      // Return integer value to be used to fill is_recursive field
      return 0;
   }


   /**
    * Is the object may be deleted
    *
    * @return boolean
   **/
   function maybeDeleted() {

      if (!isset($this->fields['id'])) {
         $this->getEmpty();
      }
      return array_key_exists('is_deleted', $this->fields);
   }


   /**
    * Is the object deleted
    *
    * @return boolean
   **/
   function isDeleted() {

      if ($this->maybeDeleted()) {
         return $this->fields["is_deleted"];
      }
      // Return integer value to be used to fill is_deleted field
      return 0;

   }


   /**
    * Can object be activated
    *
    * @since 9.2
    *
    * @return boolean
    **/
   function maybeActive() {

      if (!isset($this->fields['id'])) {
         $this->getEmpty();
      }
      return array_key_exists('is_active', $this->fields);
   }


   /**
    * Is the object active
    *
    * @since 9.2
    *
    * @return boolean
    **/
   function isActive() {

      if ($this->maybeActive()) {
         return $this->fields["is_active"];
      }
      // Return integer value to be used to fill is_active field
      return 1;

   }


   /**
    * Is the object may be a template
    *
    * @return boolean
   **/
   function maybeTemplate() {

      if (!isset($this->fields['id'])) {
         $this->getEmpty();
      }
      return isset($this->fields['is_template']);
   }


   /**
    * Is the object a template
    *
    * @return boolean
   **/
   function isTemplate() {

      if ($this->maybeTemplate()) {
         return $this->fields["is_template"];
      }
      // Return integer value to be used to fill is_template field
      return 0;
   }


   /**
    * Can the object be dynamic
    *
    * @since 0.84
    *
    * @return boolean
   **/
   function maybeDynamic() {

      if (!isset($this->fields['id'])) {
         $this->getEmpty();
      }
      return array_key_exists('is_dynamic', $this->fields);
   }


   /**
    * Use deleted field in case of dynamic management to lock ?
    *
    * need to be overriden if object need to use standard deleted management (Computer...)
    * @since 0.84
    *
    * @return boolean
   **/
   function useDeletedToLockIfDynamic() {
      return $this->maybeDynamic();
   }


   /**
    * Is an object dynamic or not
    *
    * @since 0.84
    *
    * @return boolean
   **/
   function isDynamic() {

      if ($this->maybeDynamic()) {
         return (bool)$this->fields['is_dynamic'];
      }
      return false;
   }


   /**
    * Is the object may be private
    *
    * @return boolean
   **/
   function maybePrivate() {

      if (!isset($this->fields['id'])) {
         $this->getEmpty();
      }
      return (array_key_exists('is_private', $this->fields)
              && array_key_exists('users_id', $this->fields));
   }


   /**
    * Is the object private
    *
    * @return boolean
   **/
   function isPrivate() {

      if ($this->maybePrivate()) {
         return (bool)$this->fields["is_private"];
      }
      return false;
   }

   /**
    * Can object have a location
    *
    * @since 9.3
    *
    * @return boolean
    */
   function maybeLocated() {

      if (!array_key_exists('id', $this->fields)) {
         $this->getEmpty();
      }
      return array_key_exists('locations_id', $this->fields);
   }

   /**
    * Return the linked items (in computers_items)
    *
    * @return array an array of linked items  like array('Computer' => array(1,2), 'Printer' => array(5,6))
    * @since 0.84.4
   **/
   function getLinkedItems() {
      return [];
   }


   /**
    * Return the count of linked items (in computers_items)
    *
    * @return integer number of linked items
    * @since 0.84.4
   **/
   function getLinkedItemsCount() {

      $linkeditems = $this->getLinkedItems();
      $nb          = 0;
      if (count($linkeditems)) {
         foreach ($linkeditems as $tab) {
            $nb += count($tab);
         }
      }
      return $nb;
   }


   /**
    * Return a field Value if exists
    *
    * @param string $field field name
    *
    * @return mixed value of the field / false if not exists
   **/
   function getField($field) {

      if (array_key_exists($field, $this->fields)) {
         return $this->fields[$field];
      }
      return NOT_AVAILABLE;
   }


   /**
    * Determine if a field exists
    *
    * @param string $field field name
    *
    * @return boolean
   **/
   function isField($field) {

      if (!isset($this->fields['id'])) {
         $this->getEmpty();
      }
       return array_key_exists($field, $this->fields);
   }


   /**
    * Get comments of the Object
    *
    * @return string comments of the object in the current language (HTML)
   **/
   function getComments() {

      $comment = "";
      $toadd   = [];
      if ($this->isField('completename')) {
         $toadd[] = ['name'  => __('Complete name'),
                          'value' => nl2br($this->getField('completename'))];
      }

      if ($this->isField('serial')) {
         $toadd[] = ['name'  => __('Serial number'),
                          'value' => nl2br($this->getField('serial'))];
      }

      if ($this->isField('otherserial')) {
         $toadd[] = ['name'  => __('Inventory number'),
                          'value' => nl2br($this->getField('otherserial'))];
      }

      if ($this->isField('states_id') && $this->getType()!='State') {
         $tmp = Dropdown::getDropdownName('glpi_states', $this->getField('states_id'));
         if ((strlen($tmp) != 0) && ($tmp != '&nbsp;')) {
            $toadd[] = ['name'  => __('Status'),
                             'value' => $tmp];
         }
      }

      if ($this->isField('locations_id') && $this->getType()!='Location') {
         $tmp = Dropdown::getDropdownName("glpi_locations", $this->getField('locations_id'));
         if ((strlen($tmp) != 0) && ($tmp != '&nbsp;')) {
            $toadd[] = ['name'  => Location::getTypeName(1),
                             'value' => $tmp];
         }
      }

      if ($this->isField('users_id')) {
         $tmp = getUserName($this->getField('users_id'));
         if ((strlen($tmp) != 0) && ($tmp != '&nbsp;')) {
            $toadd[] = ['name'  => User::getTypeName(1),
                             'value' => $tmp];
         }
      }

      if ($this->isField('groups_id')
          && ($this->getType() != 'Group')) {
         $tmp = Dropdown::getDropdownName("glpi_groups", $this->getField('groups_id'));
         if ((strlen($tmp) != 0) && ($tmp != '&nbsp;')) {
            $toadd[] = ['name'  => Group::getTypeName(1),
                             'value' => $tmp];
         }
      }

      if ($this->isField('users_id_tech')) {
         $tmp = getUserName($this->getField('users_id_tech'));
         if ((strlen($tmp) != 0) && ($tmp != '&nbsp;')) {
            $toadd[] = ['name'  => __('Technician in charge of the hardware'),
                             'value' => $tmp];
         }
      }

      if ($this->isField('contact')) {
         $toadd[] = ['name'  => __('Alternate username'),
                          'value' => nl2br($this->getField('contact'))];
      }

      if ($this->isField('contact_num')) {
         $toadd[] = ['name'  => __('Alternate username number'),
                          'value' => nl2br($this->getField('contact_num'))];
      }

      if (Infocom::canApplyOn($this)) {
         $infocom = new Infocom();
         if ($infocom->getFromDBforDevice($this->getType(), $this->fields['id'])) {
            $toadd[] = ['name'  => __('Warranty expiration date'),
                             'value' => Infocom::getWarrantyExpir($infocom->fields["warranty_date"],
                                                                  $infocom->fields["warranty_duration"],
                                                                  0, true)];
         }
      }

      if (($this instanceof CommonDropdown)
          && $this->isField('comment')) {
         $toadd[] = ['name'  => __('Comments'),
                          'value' => nl2br($this->getField('comment'))];
      }

      if (count($toadd)) {
         foreach ($toadd as $data) {
            // Do not use SPAN here
            $comment .= sprintf(__('%1$s: %2$s')."<br>",
                                "<strong>".$data['name'], "</strong>".$data['value']);
         }
      }

      if (!empty($comment)) {
         return Html::showToolTip($comment, ['display' => false]);
      }

      return $comment;
   }


   /**
    * @since 0.84
    *
    * Get field used for name
    *
    * @return string
   **/
   static function getNameField() {
      return 'name';
   }


   /**
    * @since 0.84
    *
    * Get field used for completename
    *
    * @return string
   **/
   static function getCompleteNameField() {
      return 'completename';
   }


   /**
    * Get raw name of the object
    * Maybe overloaded
    *
    * @deprecated 9.5
    * @see CommonDBTM::getNameField
    * @since 0.85
    *
    * @return string
   **/
   function getRawName() {
      \Toolbox::deprecated('Use CommonDBTM::getFriendlyName()');

      return $this->getFriendlyName();
   }


   /** Get raw completename of the object
    * Maybe overloaded
    *
    * @see CommonDBTM::getCompleteNameField
    *
    * @since 0.85
    *
    * @return string
   **/
   function getRawCompleteName() {

      if (isset($this->fields[static::getCompleteNameField()])) {
         return $this->fields[static::getCompleteNameField()];
      }
      return '';
   }


   /**
    * Get the name of the object
    *
    * @param array $options array of options
    *    - comments     : boolean / display comments
    *    - complete     : boolean / display completename instead of name
    *    - additional   : boolean / display aditionals information
    *
    * @return string name of the object in the current language
    *
    * @see CommonDBTM::getRawCompleteName
    * @see CommonDBTM::getFriendlyName
   **/
   function getName($options = []) {

      $p = [
         'comments'   => false,
         'complete'   => false,
         'additional' => false,
      ];

      if (is_array($options)) {
         foreach ($options as $key => $val) {
            $p[$key] = $val;
         }
      }

      $name = '';
      if ($p['complete']) {
         $name = $this->getRawCompleteName();
      }
      if (empty($name)) {
         $name = $this->getFriendlyName();
      }

      if (strlen($name) != 0) {
         if ($p['additional']) {
            $pre = $this->getPreAdditionalInfosForName();
            if (!empty($pre)) {
               $name = sprintf(__('%1$s - %2$s'), $pre, $name);
            }
            $post = $this->getPostAdditionalInfosForName();
            if (!empty($post)) {
               $name = sprintf(__('%1$s - %2$s'), $name, $post);
            }
         }
         if ($p['comments']) {
            $comment = $this->getComments();
            if (!empty($comment)) {
               $name = sprintf(__('%1$s - %2$s'), $name, $comment);
            }
         }
         return $name;
      }
      return NOT_AVAILABLE;
   }


   /**
    * Get additionals information to add before name
    *
    * @since 0.84
    *
    * @return string string to add
   **/
   function getPreAdditionalInfosForName() {
      return '';
   }

   /**
    * Get additionals information to add after name
    *
    * @since 0.84
    *
    * @return string string to add
   **/
   function getPostAdditionalInfosForName() {
      return '';
   }


   /**
    * Get the name of the object with the ID if the config is set
    * Should Not be overloaded (overload getName() instead)
    *
    * @see CommonDBTM::getName
    *
    * @param array $options array of options
    *    - comments     : boolean / display comments
    *    - complete     : boolean / display completename instead of name
    *    - additional   : boolean / display aditionals information
    *    - forceid      : boolean  override config and display item's ID (false by default)
    *
    * @return string name of the object in the current language
   **/
   function getNameID($options = []) {

      $p = [
         'forceid'  => false,
         'comments' => false,
      ];

      if (is_array($options)) {
         foreach ($options as $key => $val) {
            $p[$key] = $val;
         }
      }

      if ($p['forceid']
          || $_SESSION['glpiis_ids_visible']) {
         $addcomment = $p['comments'];

         // unset comment
         $p['comments'] = false;
         $name = $this->getName($p);

         //TRANS: %1$s is a name, %2$s is ID
         $name = sprintf(__('%1$s (%2$s)'), $name, $this->getField('id'));

         if ($addcomment) {
            $comment = $this->getComments();
            if (!empty($comment)) {
               $name = sprintf(__('%1$s - %2$s'), $name, $comment);
            }
         }
         return $name;
      }
      return $this->getName($options);
   }

   /**
    * Get the Search options for the given Type
    * If you want to work on search options, @see CommonDBTM::rawSearchOptions
    *
    * @return array an *indexed* array of search options
    *
    * @see https://glpi-developer-documentation.rtfd.io/en/master/devapi/search.html
   **/
   public final function searchOptions() {
      static $options;

      if (!isset($options)) {
         $options = [];

         foreach ($this->rawSearchOptions() as $opt) {
            $missingFields = [];
            if (!isset($opt['id'])) {
               $missingFields[] = 'id';
            }
            if (!isset($opt['name'])) {
               $missingFields[] = 'name';
            }
            if (count($missingFields) > 0) {
               throw new \Exception(
                  vsprintf(
                     'Invalid search option in "%1$s": missing "%2$s" field(s). %3$s',
                     [
                        get_called_class(),
                        implode('", "', $missingFields),
                        print_r($opt, true)
                     ]
                  )
               );
            }

            $optid = $opt['id'];
            unset($opt['id']);

            if (isset($options[$optid])) {
               $message = "Duplicate key $optid ({$options[$optid]['name']}/{$opt['name']}) in ".
                   get_class($this) . " searchOptions!";

               Toolbox::logError($message);
            }

            foreach ($opt as $k => $v) {
               $options[$optid][$k] = $v;
            }
         }
      }

      return $options;
   }


   /**
    * Provides search options configuration. Do not rely directly
    * on this, @see CommonDBTM::searchOptions instead.
    *
    * @since 9.3
    *
    * This should be overloaded in Class
    *
    * @return array a *not indexed* array of search options
    *
    * @see https://glpi-developer-documentation.rtfd.io/en/master/devapi/search.html
   **/
   public function rawSearchOptions() {
      $tab = [];

      $tab[] = [
          'id'   => 'common',
          'name' => __('Characteristics')
      ];

      if ($this->isField('name')) {
         $tab[] = [
            'id'            => 1,
            'table'         => $this->getTable(),
            'field'         => 'name',
            'name'          => __('Name'),
            'datatype'      => 'itemlink',
            'massiveaction' => false,
            'autocomplete'  => true,
         ];
      }

      if ($this->isField('is_recursive')) {
         $tab[] = [
            'id'       => 86,
            'table'      => $this->getTable(),
            'field'      => 'is_recursive',
            'name'       => __('Child entities'),
            'datatype'   => 'bool',
            'searchtype' => 'equals',
         ];
      }

      // add objectlock search options
      $tab = array_merge($tab, ObjectLock::rawSearchOptionsToAdd(get_class($this)));

      return $tab;
   }

   /**
    * Summary of getSearchOptionsToAdd
    * @since 9.2
    *
    * @param string $itemtype Item type, defaults to null
    *
    * @return array
   **/
   static function getSearchOptionsToAdd($itemtype = null) {
      $options = [];

      $classname = get_called_class();
      $method_name = 'rawSearchOptionsToAdd';
      if (!method_exists($classname, $method_name)) {
         return $options;
      }

      if (defined('TU_USER') && $itemtype != null && $itemtype != 'AllAssets') {
         $item = new $itemtype;
         $all_options = $item->searchOptions();
      }

      foreach ($classname::$method_name($itemtype) as $opt) {
         if (!isset($opt['id'])) {
            throw new \Exception(get_called_class() . ': invalid search option! ' . print_r($opt, true));
         }
         $optid = $opt['id'];
         unset($opt['id']);

         if (defined('TU_USER') && $itemtype != null) {
            if (isset($all_options[$optid])) {
               $message = "Duplicate key $optid ({$all_options[$optid]['name']}/{$opt['name']}) in ".
                  self::class . " searchOptionsToAdd for $itemtype!";

               Toolbox::logError($message);
            }
         }

         foreach ($opt as $k => $v) {
            $options[$optid][$k] = $v;
            if (defined('TU_USER') && $itemtype != null) {
               $all_options[$optid][$k] = $v;
            }
         }
      }

      return $options;
   }

   /**
    * Get all the massive actions available for the current class regarding given itemtype
    *
    * @since 0.85
    *
    * @param array      $actions    array of the actions to update
    * @param string     $itemtype   the type of the item for which we want the actions
    * @param boolean    $is_deleted (default 0)
    * @param CommonDBTM $checkitem  (default NULL)
    *
    * @return void (update is set inside $actions)
   **/
   static function getMassiveActionsForItemtype(array &$actions, $itemtype, $is_deleted = 0,
                                                CommonDBTM $checkitem = null) {
   }


   /**
    * Class-specific method used to show the fields to specify the massive action
    *
    * @since 0.85
    *
    * @param MassiveAction $ma the current massive action object
    *
    * @return boolean false if parameters displayed ?
   **/
   static function showMassiveActionsSubForm(MassiveAction $ma) {
      return false;
   }


   /**
    * Class specific execution of the massive action (new system) by itemtypes
    *
    * @since 0.85
    *
    * @param MassiveAction $ma   the current massive action object
    * @param CommonDBTM    $item the item on which apply the massive action
    * @param array         $ids  an array of the ids of the item on which apply the action
    *
    * @return void (direct submit to $ma object)
   **/
   static function processMassiveActionsForOneItemtype(MassiveAction $ma, CommonDBTM $item,
                                                       array $ids) {
   }


   /**
    * Get the standard massive actions which are forbidden
    *
    * @since 0.84
    *
    * This should be overloaded in Class
    *
    * @return array an array of massive actions
   **/
   function getForbiddenStandardMassiveAction() {
      return [];
   }


   /**
    * Get forbidden single action
    *
    * @since 9.5.0
    *
    * @return array
   **/
   public function getForbiddenSingleMassiveActions() {
      $excluded = [
         '*:update',
         '*:delete',
         '*:remove',
         '*:purge',
         '*:unlock'
      ];

      if (Infocom::canApplyOn($this)) {
         $ic = new Infocom();
         if ($ic->getFromDBforDevice($this->getType(), $this->fields['id'])) {
            $excluded[] = 'Infocom:activate';
         }
      }

      return $excluded;
   }

   /**
    * Get whitelisted single actions
    *
    * @since 9.5.0
    *
    * @return array
   **/
   public function getWhitelistedSingleMassiveActions() {
      return ['MassiveAction:add_transfer_list'];
   }


   /**
    * Get the specific massive actions
    *
    * @since 0.84
    *
    * This should be overloaded in Class
    *
    * @param object $checkitem link item to check right (default NULL)
    *
    * @return array an array of massive actions
   **/
   function getSpecificMassiveActions($checkitem = null) {
      global $DB;

      $actions = [];
      // test if current profile has rights to unlock current item type
      if (Session::haveRight( static::$rightname, UNLOCK)) {
         $actions['ObjectLock'.MassiveAction::CLASS_ACTION_SEPARATOR.'unlock']
                        = _x('button', 'Unlock items');
      }
      if ($DB->fieldExists(static::getTable(), 'entities_id') && static::canUpdate()) {
         MassiveAction::getAddTransferList($actions);
      }

      if (in_array(static::getType(), Appliance::getTypes(true)) && static::canUpdate()) {
         $actions['Appliance' . MassiveAction::CLASS_ACTION_SEPARATOR . 'add_item'] = _x('button', 'Associate to an appliance');
      }

      return $actions;
   }


   /**
    * Print out an HTML "<select>" for a dropdown
    *
    * This should be overloaded in Class
    *
    * @param array $options array of possible options:
    * Parameters which could be used in options array :
    *    - name : string / name of the select (default is depending itemtype)
    *    - value : integer / preselected value (default 0)
    *    - comments : boolean / is the comments displayed near the dropdown (default true)
    *    - entity : integer or array / restrict to a defined entity or array of entities
    *                   (default -1 : no restriction)
    *    - toupdate : array / Update a specific item on select change on dropdown
    *                   (need value_fieldname, to_update, url (see Ajax::updateItemOnSelectEvent for information)
    *                   and may have moreparams)
    *    - used : array / Already used items ID: not to display in dropdown (default empty)
    *
    * @return string|void display the dropdown
   **/
   static function dropdown($options = []) {
      /// TODO try to revert usage : Dropdown::show calling this function
      /// TODO use this function instead of Dropdown::show
      return Dropdown::show(get_called_class(), $options);
   }


   /**
    * Return a search option by looking for a value of a specific field and maybe a specific table
    *
    * @param string $field the field in which looking for the value (for example : table, name, etc)
    * @param string $value the value to look for in the field
    * @param string $table the table (default '')
    *
    * @return array the search option array, or an empty array if not found
   **/
   function getSearchOptionByField($field, $value, $table = '') {

      foreach ($this->searchOptions() as $id => $searchOption) {
         if ((isset($searchOption['linkfield']) && ($searchOption['linkfield'] == $value))
             || (isset($searchOption[$field]) && ($searchOption[$field] == $value))) {
            if (($table == '')
                || (($table != '') && ($searchOption['table'] == $table))) {
               // Set ID;
               $searchOption['id'] = $id;
               return $searchOption;
            }
         }
      }
      return [];
   }


   /**
    * Get search options
    *
    * @since 0.85
    *
    * @return array the search option array
   **/
   function getOptions() {

      if (!$this->searchopt) {
         $this->searchopt = Search::getOptions($this->getType());
      }

      return $this->searchopt;
   }


   /**
    * Return a search option ID by looking for a value of a specific field and maybe a specific table
    *
    * @since 0.83
    *
    * @param string $field the field in which looking for the value (for example : table, name, etc)
    * @param string $value the value to look for in the field
    * @param string $table the table (default '')
    *
    * @return mixed the search option id, or -1 if not found
   **/
   function getSearchOptionIDByField($field, $value, $table = '') {

      $tab = $this->getSearchOptionByField($field, $value, $table);
      if (isset($tab['id'])) {
         return $tab['id'];
      }
      return -1;
   }


   /**
    * Check float and decimal values
    *
    * @param boolean $display display or not messages in and addAfterRedirect (true by default)
    *
    * @return void
   **/
   function filterValues($display = true) {
      // MoYo : comment it because do not understand why filtering is disable
      // if (in_array('CommonDBRelation', class_parents($this))) {
      //    return true;
      // }
      //Type mismatched fields
      $fails = [];
      if (isset($this->input) && is_array($this->input) && count($this->input)) {

         foreach ($this->input as $key => $value) {
            $unset        = false;
            $regs         = [];
            $searchOption = $this->getSearchOptionByField('field', $key);

            if (isset($searchOption['datatype'])
                && (is_null($value) || ($value == '') || ($value == 'NULL'))) {

               switch ($searchOption['datatype']) {
                  case 'date' :
                  case 'datetime' :
                     // don't use $unset', because this is not a failure
                     $this->input[$key] = 'NULL';
                     break;
               }
            } else if (isset($searchOption['datatype'])
                       && !is_null($value)
                       && ($value != '')
                       && ($value != 'NULL')) {

               switch ($searchOption['datatype']) {
                  case 'integer' :
                  case 'count' :
                  case 'number' :
                  case 'decimal' :
                     $value = str_replace(',', '.', $value);
                     if ($searchOption['datatype'] == 'decimal') {
                        $this->input[$key] = floatval(Toolbox::cleanDecimal($value));
                     } else {
                        $this->input[$key] = intval(Toolbox::cleanInteger($value));
                     }
                     if (!is_numeric($this->input[$key])) {
                        $unset = true;
                     }
                     break;

                  case 'bool' :
                     if (!in_array($value, [0,1])) {
                        $unset = true;
                     }
                     break;

                  case 'ip' :
                     $address = new IPAddress();
                     if (!$address->setAddressFromString($value)) {
                        $unset = true;
                     } else if (!$address->is_ipv4()) {
                        $unset = true;
                     }
                     break;

                  case 'mac' :
                     preg_match("/([0-9a-fA-F]{1,2}([:-]|$)){6}$/", $value, $regs);
                     if (empty($regs)) {
                        $unset = true;
                     }
                     // Define the MAC address to lower to reduce complexity of SQL queries
                     $this->input[$key] = strtolower ($value);
                     break;

                  case 'date' :
                  case 'datetime' :
                     // Date is already "reformat" according to getDateFormat()
                     $pattern  = "/^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})";
                     $pattern .= "([_][01][0-9]|2[0-3]:[0-5][0-9]:[0-5]?[0-9])?/";
                     preg_match($pattern, $value, $regs);
                     if (empty($regs)) {
                        $unset = true;
                     }
                     break;

                  case 'itemtype' :
                     //Want to insert an itemtype, but the associated class doesn't exists
                     if (!class_exists($value)) {
                        $unset = true;
                     }

                  case 'email' :
                  case 'string' :
                     if (strlen($value) > 255) {
                        Toolbox::logWarning("$value exceed 255 characters long (".strlen($value)."), it will be truncated.");
                        $this->input[$key] = substr($value, 0, 254);
                     }
                     break;

                  default :
                     //Plugins can implement their own checks
                     if (!$this->checkSpecificValues($searchOption['datatype'], $value)) {
                        $unset = true;
                     }
                     // Copy value if check have update it
                     $this->input[$key] = $value;
                     break;
               }
            }

            if ($unset) {
               $fails[] = $searchOption['name'];
               unset($this->input[$key]);
            }
         }
      }
      if ($display && count($fails)) {
         //Display a message to indicate that one or more value where filtered
         //TRANS: %s is the list of the failed fields
         $message = sprintf(__('%1$s: %2$s'), __('At least one field has an incorrect value'),
                            implode(',', $fails));
         Session::addMessageAfterRedirect($message, INFO, true);
      }
   }


   /**
    * Add more check for values
    *
    * @param string $datatype datatype of the value
    * @param array  $value    value to check (pass by reference)
    *
    * @return boolean true if value is ok, false if not
   **/
   function checkSpecificValues($datatype, &$value) {
      return true;
   }


   /**
    * Get fields to display in the unicity error message
    *
    * @return array an array which contains field => label
   **/
   function getUnicityFieldsToDisplayInErrorMessage() {

      return ['id'          => __('ID'),
                   'serial'      => __('Serial number'),
                   'entities_id' => Entity::getTypeName(1)];
   }


   function getUnallowedFieldsForUnicity() {
      return ['alert', 'comment', 'date_mod', 'id', 'is_recursive', 'items_id'];
   }


   /**
    * Build an unicity error message
    *
    * @param array $msgs    the string not transleted to be display on the screen, or to be sent in a notification
    * @param array $unicity the unicity criterion that failed to match
    * @param array $doubles the items that are already present in DB
    *
    * @return string
   **/
   function getUnicityErrorMessage($msgs, $unicity, $doubles) {

      $message = [];
      foreach ($msgs as $field => $value) {
         $table = getTableNameForForeignKeyField($field);
         if ($table != '') {
            $searchOption = $this->getSearchOptionByField('field', 'name', $table);
         } else {
            $searchOption = $this->getSearchOptionByField('field', $field);
         }
         $message[] = sprintf(__('%1$s = %2$s'), $searchOption['name'], $value);
      }

      if ($unicity['action_refuse']) {
         $message_text = sprintf(__('Impossible record for %s'),
                                 implode('&nbsp;&amp;&nbsp;', $message));
      } else {
         $message_text = sprintf(__('Item successfully added but duplicate record on %s'),
                                 implode('&nbsp;&amp;&nbsp;', $message));
      }
      $message_text .= '<br>'.__('Other item exist');

      foreach ($doubles as $double) {
         if (in_array('CommonDBChild', class_parents($this))) {
            if ($this->getField($this->itemtype)) {
               $item = new $double['itemtype']();
            } else {
               $item = new $this->itemtype();
            }

            $item->getFromDB($double['items_id']);
         } else {
            $item = clone $this;
            $item->getFromDB($double['id']);
         }

         $double_text = '';
         if ($item->canView() && $item->canViewItem()) {
            $double_text = $item->getLink();
         }

         foreach ($this->getUnicityFieldsToDisplayInErrorMessage() as $key => $value) {
            $field_value = $item->getField($key);
            if ($field_value != NOT_AVAILABLE) {
               if (getTableNameForForeignKeyField($key) != '') {
                  $field_value = Dropdown::getDropdownName(getTableNameForForeignKeyField($key),
                                                           $field_value);
               }
               $new_text = sprintf(__('%1$s: %2$s'), $value, $field_value);
               if (empty($double_text)) {
                  $double_text = $new_text;
               } else {
                  $double_text = sprintf(__('%1$s - %2$s'), $double_text, $new_text);
               }
            }
         }
         // Add information on item in trashbin
         if ($item->isField('is_deleted') && $item->getField('is_deleted')) {
            $double_text = sprintf(__('%1$s - %2$s'), $double_text, __('Item in the trashbin'));
         }

         $message_text .= "<br>[$double_text]";
      }
      return $message_text;
   }


   /**
    * Check field unicity before insert or update
    *
    * @param boolean $add     true for insert, false for update (false by default)
    * @param array   $options array
    *
    * @return boolean true if item can be written in DB, false if not
   **/
   function checkUnicity($add = false, $options = []) {
      global $CFG_GLPI;

      $p = [
         'unicity_error_message'  => true,
         'add_event_on_duplicate' => true,
         'disable_unicity_check'  => false,
      ];

      if (is_array($options) && count($options)) {
         foreach ($options as $key => $value) {
            $p[$key] = $value;
         }
      }

      // Do not check for template
      if (isset($this->input['is_template']) && $this->input['is_template']) {
         return true;
      }

      $result = true;

      //Do not check unicity when creating infocoms or if checking is expliclty disabled
      if ($p['disable_unicity_check']) {
         return $result;
      }

      //Get all checks for this itemtype and this entity
      if (in_array(get_class($this), $CFG_GLPI["unicity_types"])) {
         // Get input entities if set / else get object one
         if (isset($this->input['entities_id'])) {
            $entities_id = $this->input['entities_id'];
         } else if (isset($this->fields['entities_id'])) {
            $entities_id = $this->fields['entities_id'];
         } else {
            $message = 'Missing entity ID!';
            Toolbox::logError($message);
         }

         $all_fields =  FieldUnicity::getUnicityFieldsConfig(get_class($this), $entities_id);
         foreach ($all_fields as $key => $fields) {

            //If there's fields to check
            if (!empty($fields) && !empty($fields['fields'])) {
               $where    = [];
               $continue = true;
               foreach (explode(',', $fields['fields']) as $field) {
                  if (isset($this->input[$field]) //Field is set
                      //Standard field not null
                      && (((getTableNameForForeignKeyField($field) == '')
                           && ($this->input[$field] != ''))
                          //Foreign key and value is not 0
                          || ((getTableNameForForeignKeyField($field) != '')
                              && ($this->input[$field] > 0)))
                      && !Fieldblacklist::isFieldBlacklisted(get_class($this), $entities_id, $field,
                                                             $this->input[$field])) {
                     $where[$this->getTable() . '.' . $field] = $this->input[$field];
                  } else {
                     $continue = false;
                  }
               }

               if ($continue
                   && count($where)) {
                  $entities = $fields['entities_id'];
                  if ($fields['is_recursive']) {
                     $entities = getSonsOf('glpi_entities', $fields['entities_id']);
                  }
                  $where[] = getEntitiesRestrictCriteria($this->getTable(), '', $entities);

                  $tmp = clone $this;
                  if ($tmp->maybeTemplate()) {
                     $where['is_template'] = 0;
                  }

                  //If update, exclude ID of the current object
                  if (!$add) {
                     $where['NOT'] = [$this->getTable() . '.id' => $this->input['id']];
                  }

                  if (countElementsInTable($this->getTable(), $where) > 0) {
                     if ($p['unicity_error_message']
                         || $p['add_event_on_duplicate']) {
                        $message = [];
                        foreach (explode(',', $fields['fields']) as $field) {
                           $message[$field] = $this->input[$field];
                        }

                        $doubles      = getAllDataFromTable($this->getTable(), $where);
                        $message_text = $this->getUnicityErrorMessage($message, $fields, $doubles);
                        if ($p['unicity_error_message']) {
                           if (!$fields['action_refuse']) {
                              $show_other_messages = ($fields['action_refuse']?true:false);
                           } else {
                              $show_other_messages = true;
                           }
                           Session::addMessageAfterRedirect($message_text, true,
                                                            $show_other_messages,
                                                            $show_other_messages);
                        }
                        if ($p['add_event_on_duplicate']) {
                           Event::log ((!$add?$this->fields['id']:0), get_class($this), 4,
                                       'inventory',
                                       //TRANS: %1$s is the user login, %2$s the message
                                       sprintf(__('%1$s trying to add an item that already exists: %2$s'),
                                               $_SESSION["glpiname"], $message_text));
                        }
                     }
                     if ($fields['action_refuse']) {
                        $result = false;
                     }
                     if ($fields['action_notify']) {
                        $params = [
                           'action_type' => $add,
                           'action_user' => getUserName(Session::getLoginUserID()),
                           'entities_id' => $entities_id,
                           'itemtype'    => get_class($this),
                           'date'        => $_SESSION['glpi_currenttime'],
                           'refuse'      => $fields['action_refuse'],
                           'label'       => $message,
                           'field'       => $fields,
                           'double'      => $doubles];
                        NotificationEvent::raiseEvent('refuse', new FieldUnicity(), $params);
                     }
                  }
               }
            }
         }

      }

      return $result;
   }


   /**
    * Clean all infos which match some criteria
    *
    * @param array   $crit    array of criteria (ex array('is_active'=>'1'))
    * @param boolean $force   force purge not on put in trashbin (default 0)
    * @param boolean $history do history log ? (true by default)
    *
    * @return boolean
   **/
   function deleteByCriteria($crit = [], $force = 0, $history = 1) {
      global $DB;

      $ok = false;
      if (is_array($crit) && (count($crit) > 0)) {
         $crit['FIELDS'] = [$this::getTable() => 'id'];
         $ok = true;
         $iterator = $DB->request($this->getTable(), $crit);
         foreach ($iterator as $row) {
            if (!$this->delete($row, $force, $history)) {
               $ok = false;
            }
         }

      }
      return $ok;
   }


   /**
    * get the Entity of an Item
    *
    * @param string  $itemtype item type
    * @param integer $items_id id of the item
    *
    * @return integer ID of the entity or -1
   **/
   static function getItemEntity($itemtype, $items_id) {

      if ($itemtype
          && ($item = getItemForItemtype($itemtype))) {

         if ($item->getFromDB($items_id)) {
            return $item->getEntityID();
         }

      }
      return -1;
   }


   /**
    * display a specific field value
    *
    * @since 0.83
    *
    * @param string       $field   name of the field
    * @param string|array $values  with the value to display or a Single value
    * @param array        $options Array of options
    *
    * @return string the string to display
   **/
   static function getSpecificValueToDisplay($field, $values, array $options = []) {

      switch ($field) {
         case '_virtual_datacenter_position':
            if (method_exists(static::class, 'getDcBreadcrumbSpecificValueToDisplay')) {
               return static::getDcBreadcrumbSpecificValueToDisplay($values['id']);
            }
      }

      return '';
   }


   /**
    * display a field using standard system
    *
    * @since 0.83
    *
    * @param integer|string|array $field_id_or_search_options id of the search option field
    *                                                             or field name
    *                                                             or search option array
    * @param mixed                $values                     value to display
    * @param array                $options                    array of possible options:
    * Parameters which could be used in options array :
    *    - comments : boolean / is the comments displayed near the value (default false)
    *    - any others options passed to specific display method
    *
    * @return string the string to display
   **/
   function getValueToDisplay($field_id_or_search_options, $values, $options = []) {
      global $CFG_GLPI;

      $param = [
         'comments' => false,
         'html'     => false,
      ];
      foreach ($param as $key => $val) {
         if (!isset($options[$key])) {
            $options[$key] = $val;
         }
      }

      $searchoptions = [];
      if (is_array($field_id_or_search_options)) {
         $searchoptions = $field_id_or_search_options;
      } else {
         $searchopt = $this->searchOptions();

         // Get if id of search option is passed
         if (is_numeric($field_id_or_search_options)) {
            if (isset($searchopt[$field_id_or_search_options])) {
               $searchoptions = $searchopt[$field_id_or_search_options];
            }
         } else { // Get if field name is passed
            $searchoptions = $this->getSearchOptionByField('field', $field_id_or_search_options,
                                                           $this->getTable());
         }
      }

      if (count($searchoptions)) {
         $field = $searchoptions['field'];

         // Normalize option
         if (is_array($values)) {
            $value = $values[$field];
         } else {
            $value  = $values;
            $values = [$field => $value];
         }

         if (isset($searchoptions['datatype'])) {
            $unit = '';
            if (isset($searchoptions['unit'])) {
               $unit = $searchoptions['unit'];
            }

            switch ($searchoptions['datatype']) {
               case "count" :
               case "number" :
                  if (isset($searchoptions['toadd']) && isset($searchoptions['toadd'][$value])) {
                     return $searchoptions['toadd'][$value];
                  }
                  if ($options['html']) {
                     return Dropdown::getValueWithUnit($value, $unit);
                  }
                  return $value;

               case "decimal" :
                  if ($options['html']) {
                     return Dropdown::getValueWithUnit($value, $unit, $CFG_GLPI["decimal_number"]);
                  }
                  return $value;

               case "string" :
               case "mac" :
               case "ip" :
                  return $value;

               case "text" :

                  if ($options['html']) {
                     $text = nl2br($value);
                  } else {
                     $text = $value;
                  }
                  if (isset($searchoptions['htmltext']) && $searchoptions['htmltext']) {
                     $text = Html::clean(Toolbox::unclean_cross_side_scripting_deep($text));
                  }
                  return $text;

               case "bool" :
                  return Dropdown::getYesNo($value);

               case "date" :
               case "date_delay" :
                  if (isset($options['relative_dates']) && $options['relative_dates']) {
                     $dates = Html::getGenericDateTimeSearchItems(['with_time'   => true,
                                                                        'with_future' => true]);
                     return $dates[$value];
                  }
                  return Html::convDate(Html::computeGenericDateTimeSearch($value, true));

               case "datetime" :
                  if (isset($options['relative_dates']) && $options['relative_dates']) {
                     $dates = Html::getGenericDateTimeSearchItems(['with_time'   => true,
                                                                        'with_future' => true]);
                     return $dates[$value];
                  }
                  return Html::convDateTime(Html::computeGenericDateTimeSearch($value, false));

               case "timestamp" :
                  if (($value == 0)
                      && isset($searchoptions['emptylabel'])) {
                     return $searchoptions['emptylabel'];
                  }
                  $withseconds = false;
                  if (isset($searchoptions['withseconds'])) {
                     $withseconds = $searchoptions['withseconds'];
                  }
                  return Html::timestampToString($value, $withseconds);

               case "email" :
                  if ($options['html']) {
                     return "<a href='mailto:$value'>$value</a>";
                  }
                  return $value;

               case "weblink" :
                  $orig_link = trim($value);
                  if (!empty($orig_link)) {
                     // strip begin of link
                     $link = preg_replace('/https?:\/\/(www[^\.]*\.)?/', '', $orig_link);
                     $link = preg_replace('/\/$/', '', $link);
                     if (Toolbox::strlen($link) > $CFG_GLPI["url_maxlength"]) {
                        $link = Toolbox::substr($link, 0, $CFG_GLPI["url_maxlength"])."...";
                     }
                     return "<a href=\"".Toolbox::formatOutputWebLink($orig_link)."\" target='_blank'>$link".
                            "</a>";
                  }
                  return "&nbsp;";

               case "itemlink" :
                  if ($searchoptions['table'] == $this->getTable()) {
                     break;
                  }

               case "dropdown" :
                  if (isset($searchoptions['toadd']) && isset($searchoptions['toadd'][$value])) {
                     return $searchoptions['toadd'][$value];
                  }
                  if (!is_numeric($value)) {
                     return $value;
                  }

                  if (($value == 0)
                      && isset($searchoptions['emptylabel'])) {
                     return $searchoptions['emptylabel'];
                  }

                  if ($searchoptions['table'] == 'glpi_users') {
                     if ($param['comments']) {
                        $tmp = getUserName($value, 2);
                        return $tmp['name'].'&nbsp;'.Html::showToolTip($tmp['comment'],
                                                                       ['display' => false]);
                     }
                     return getUserName($value);
                  }
                  if ($param['comments']) {
                     $tmp = Dropdown::getDropdownName($searchoptions['table'], $value, 1);
                     return $tmp['name'].'&nbsp;'.Html::showToolTip($tmp['comment'],
                                                                    ['display' => false]);
                  }
                  return Dropdown::getDropdownName($searchoptions['table'], $value);

               case "itemtypename" :
                  if ($obj = getItemForItemtype($value)) {
                     return $obj->getTypeName(1);
                  }
                  break;

               case "language" :
                  if (isset($CFG_GLPI['languages'][$value])) {
                     return $CFG_GLPI['languages'][$value][0];
                  }
                  return __('Default value');

            }
         }
         // Get specific display if available
         $itemtype = getItemTypeForTable($searchoptions['table']);
         if ($item = getItemForItemtype($itemtype)) {
            $options['searchopt'] = $searchoptions;
            $specific = $item->getSpecificValueToDisplay($field, $values, $options);
            if (!empty($specific)) {
               return $specific;
            }
         }

      }
      return $value;
   }

   /**
    * display a specific field selection system
    *
    * @since 0.83
    *
    * @param string       $field   name of the field
    * @param string       $name    name of the select (if empty use linkfield) (default '')
    * @param string|array $values  with the value to select or a Single value (default '')
    * @param array        $options aArray of options
    *
    * @return string the string to display
   **/
   static function getSpecificValueToSelect($field, $name = '', $values = '', array $options = []) {
      return '';
   }


   /**
    * Select a field using standard system
    *
    * @since 0.83
    *
    * @param integer|string|array $field_id_or_search_options id of the search option field
    *                                                             or field name
    *                                                             or search option array
    * @param string               $name                       name of the select (if empty use linkfield)
    *                                                         (default '')
    * @param mixed                $values                     default value to display
    *                                                         (default '')
    * @param array                $options                    array of possible options:
    * Parameters which could be used in options array :
    *    - comments : boolean / is the comments displayed near the value (default false)
    *    - any others options passed to specific display method
    *
    * @return string the string to display
   **/
   function getValueToSelect($field_id_or_search_options, $name = '', $values = '', $options = []) {
      global $CFG_GLPI;

      $param = [
         'comments' => false,
         'html'     => false,
      ];
      foreach ($param as $key => $val) {
         if (!isset($options[$key])) {
            $options[$key] = $val;
         }
      }

      $searchoptions = [];
      if (is_array($field_id_or_search_options)) {
         $searchoptions = $field_id_or_search_options;
      } else {
         $searchopt = $this->searchOptions();

         // Get if id of search option is passed
         if (is_numeric($field_id_or_search_options)) {
            if (isset($searchopt[$field_id_or_search_options])) {
               $searchoptions = $searchopt[$field_id_or_search_options];
            }
         } else { // Get if field name is passed
            $searchoptions = $this->getSearchOptionByField('field', $field_id_or_search_options,
                                                           $this->getTable());
         }
      }
      if (count($searchoptions)) {
         $field = $searchoptions['field'];
         // Normalize option
         if (is_array($values)) {
            $value = $values[$field];
         } else {
            $value  = $values;
            $values = [$field => $value];
         }

         if (empty($name)) {
            $name = $searchoptions['linkfield'];
         }
         // If not set : set to specific
         if (!isset($searchoptions['datatype'])) {
            $searchoptions['datatype'] = 'specific';
         }

         $options['display'] = false;

         if (isset($options[$searchoptions['table'].'.'.$searchoptions['field']])) {
            $options = array_merge($options,
                                   $options[$searchoptions['table'].'.'.$searchoptions['field']]);
         }

         switch ($searchoptions['datatype']) {
            case "count" :
            case "number" :
            case "integer" :
               $copytooption = ['min', 'max', 'step', 'toadd', 'unit'];
               foreach ($copytooption as $key) {
                  if (isset($searchoptions[$key]) && !isset($options[$key])) {
                     $options[$key] = $searchoptions[$key];
                  }
               }
               $options['value'] = $value;
               return Dropdown::showNumber($name, $options);

            case "decimal" :
            case "mac" :
            case "ip" :
            case "string" :
            case "email" :
            case "weblink" :
               $this->fields[$name] = $value;
               return Html::autocompletionTextField($this, $name, $options);

            case "text" :
               $out = '';
               if (isset($searchoptions['htmltext']) && $searchoptions['htmltext']) {
                  $out = Html::initEditorSystem($name, '', false);
               }
               return $out."<textarea cols='45' rows='5' name='$name'>$value</textarea>";

            case "bool" :
               return Dropdown::showYesNo($name, $value, -1, $options);

            case "color" :
               return Html::showColorField($name, $options);

            case "date" :
            case "date_delay" :
               if (isset($options['relative_dates']) && $options['relative_dates']) {
                  if (isset($searchoptions['maybefuture']) && $searchoptions['maybefuture']) {
                     $options['with_future'] = true;
                  }
                  return Html::showGenericDateTimeSearch($name, $value, $options);
               }
               $copytooption = ['min', 'max', 'maybeempty', 'showyear'];
               foreach ($copytooption as $key) {
                  if (isset($searchoptions[$key]) && !isset($options[$key])) {
                     $options[$key] = $searchoptions[$key];
                  }
               }
               $options['value'] = $value;
               return Html::showDateField($name, $options);

            case "datetime" :
               if (isset($options['relative_dates']) && $options['relative_dates']) {
                  if (isset($searchoptions['maybefuture']) && $searchoptions['maybefuture']) {
                     $options['with_future'] = true;
                  }
                  $options['with_time'] = true;
                  return Html::showGenericDateTimeSearch($name, $value, $options);
               }
               $copytooption = ['mindate', 'maxdate', 'mintime', 'maxtime',
                                     'maybeempty', 'timestep'];
               foreach ($copytooption as $key) {
                  if (isset($searchoptions[$key]) && !isset($options[$key])) {
                     $options[$key] = $searchoptions[$key];
                  }
               }
               $options['value'] = $value;
               return Html::showDateTimeField($name, $options);

            case "timestamp" :
               $copytooption = ['addfirstminutes', 'emptylabel', 'inhours',  'max', 'min',
                                     'step', 'toadd', 'display_emptychoice'];
               foreach ($copytooption as $key) {
                  if (isset($searchoptions[$key]) && !isset($options[$key])) {
                     $options[$key] = $searchoptions[$key];
                  }
               }
               $options['value'] = $value;
               return Dropdown::showTimeStamp($name, $options);

            case "itemlink" :
               // Do not use dropdown if wanted to select string value instead of ID
               if (isset($options['itemlink_as_string']) && $options['itemlink_as_string']) {
                  break;
               }

            case "dropdown" :
               $copytooption     = ['condition', 'displaywith', 'emptylabel',
                                         'right', 'toadd'];
               $options['name']  = $name;
               $options['value'] = $value;
               foreach ($copytooption as $key) {
                  if (isset($searchoptions[$key]) && !isset($options[$key])) {
                     $options[$key] = $searchoptions[$key];
                  }
               }
               if (!isset($options['entity'])) {
                  $options['entity'] = $_SESSION['glpiactiveentities'];
               }
               $itemtype = getItemTypeForTable($searchoptions['table']);

               return $itemtype::dropdown($options);

            case "right" :
                return Profile::dropdownRights(Profile::getRightsFor($searchoptions['rightclass']),
                                               $name, $value, ['multiple' => false,
                                                                    'display'  => false]);

            case "itemtypename" :
               if (isset($searchoptions['itemtype_list'])) {
                  $options['types'] = $CFG_GLPI[$searchoptions['itemtype_list']];
               }
               $copytooption     = ['types'];
               $options['value'] = $value;
               foreach ($copytooption as $key) {
                  if (isset($searchoptions[$key]) && !isset($options[$key])) {
                     $options[$key] = $searchoptions[$key];
                  }
               }
               if (isset($options['types'])) {
                  return Dropdown::showItemTypes($name, $options['types'],
                                                   $options);
               }
               return false;

            case "language" :
               $copytooption = ['emptylabel', 'display_emptychoice'];
               foreach ($copytooption as $key) {
                  if (isset($searchoptions[$key]) && !isset($options[$key])) {
                     $options[$key] = $searchoptions[$key];
                  }
               }
               $options['value'] = $value;
               return Dropdown::showLanguages($name, $options);

         }
         // Get specific display if available
         $itemtype = getItemTypeForTable($searchoptions['table']);
         if ($item = getItemForItemtype($itemtype)) {
            $specific = $item->getSpecificValueToSelect($searchoptions['field'], $name,
                                                        $values, $options);
            if (strlen($specific)) {
               return $specific;
            }
         }
      }
      // default case field text
      $this->fields[$name] = $value;
      return Html::autocompletionTextField($this, $name, $options);
   }


   /**
    * @param string  $itemtype Item type
    * @param string  $target   Target
    * @param boolean $add      (default 0)
    *
    * @return false|void
    */
   static function listTemplates($itemtype, $target, $add = 0) {
      global $DB;

      if (!($item = getItemForItemtype($itemtype))) {
         return false;
      }

      if (!$item->maybeTemplate()) {
         return false;
      }

      // Avoid to get old data
      $item->clearSavedInput();

      //Check is user have minimum right r
      if (!$item->canView()
          && !$item->canCreate()) {
         return false;
      }

      $request = [
         'FROM'   => $item->getTable(),
         'WHERE'  => [
            'is_template'  => 1
         ],
         'ORDER'  => ['template_name']
      ];

      if ($item->isEntityAssign()) {
         $request['WHERE'] = $request['WHERE'] + getEntitiesRestrictCriteria(
            $item->getTable(),
            'entities_id',
            $_SESSION['glpiactiveentities'],
            $item->maybeRecursive()
         );
      }

      if (Session::isMultiEntitiesMode()) {
         $colspan=3;
      } else {
         $colspan=2;
      }

      $iterator = $DB->request($request);
      $blank_params = (strpos($target, '?') ? '&' : '?') . "id=-1&withtemplate=2";
      $target_blank = $target . $blank_params;

      if ($add && count($iterator) == 0) {
         //if there is no template, just use blank
         Html::redirect($target_blank);
      }

      echo "<div class='center'><table class='tab_cadre'>";
      if ($add) {
         echo "<tr><th>" . $item->getTypeName(1)."</th>";
         echo "<th>".__('Choose a template')."</th></tr>";
         echo "<tr><td class='tab_bg_1 center' colspan='$colspan'>";
         echo "<a href=\"" . Html::entities_deep($target_blank) . "\">".__('Blank Template')."</a></td>";
         echo "</tr>";
      } else {
         echo "<tr><th>".$item->getTypeName(1)."</th>";
         if (Session::isMultiEntitiesMode()) {
            echo "<th>".Entity::getTypeName(1)."</th>";
         }
         echo "<th>".__('Templates')."</th></tr>";
      }

      while ($data = $iterator->next()) {
         $templname = $data["template_name"];
         if ($_SESSION["glpiis_ids_visible"] || empty($data["template_name"])) {
            $templname = sprintf(__('%1$s (%2$s)'), $templname, $data["id"]);
         }
         if (Session::isMultiEntitiesMode()) {
            $entity = Dropdown::getDropdownName('glpi_entities', $data['entities_id']);
         }
         if ($item->canCreate() && !$add) {
            $modify_params =
               (strpos($target, '?') ? '&amp;' : '?')
               . "id=".$data['id']
               . "&amp;withtemplate=1";
            $target_modify = $target . $modify_params;

            echo "<tr><td class='tab_bg_1 center'>";
            echo "<a href=\"$target_modify\">";
            echo "&nbsp;&nbsp;&nbsp;$templname&nbsp;&nbsp;&nbsp;</a></td>";
            if (Session::isMultiEntitiesMode()) {
               echo "<td class='tab_bg_1 center'>$entity</td>";
            }
            echo "<td class='tab_bg_2 center b'>";
            if ($item->can($data['id'], PURGE)) {
               Html::showSimpleForm($target, 'purge', _x('button', 'Delete permanently'),
                                    ['withtemplate' => 1,
                                       'id'           => $data['id']]);
            }
            echo "</td>";
         } else {
            $add_params =
               (strpos($target, '?') ? '&amp;' : '?')
               . "id=".$data['id']
               . "&amp;withtemplate=2";
            $target_add = $target . $add_params;

            echo "<tr><td class='tab_bg_1 center' colspan='2'>";
            echo "<a href=\"$target_add\">";
            echo "&nbsp;&nbsp;&nbsp;$templname&nbsp;&nbsp;&nbsp;</a></td>";
         }
         echo "</tr>";
      }

      if ($item->canCreate() && !$add) {
         $create_params =
            (strpos($target, '?') ? '&amp;' : '?')
            . "withtemplate=1";
         $target_create = $target . $create_params;
         echo "<tr><td class='tab_bg_2 center b' colspan='3'>";
         echo "<a href=\"$target_create\">" . __('Add a template...') . "</a>";
         echo "</td></tr>";
      }
      echo "</table></div>\n";
   }


   /**
    * Specificy a plugin itemtype for which entities_id and is_recursive should be forwarded
    *
    * @since 0.83
    *
    * @param string $for_itemtype change of entity for this itemtype will be forwarder
    * @param string $to_itemtype  change of entity will affect this itemtype
    *
    * @return void
   **/
   static function addForwardEntity($for_itemtype, $to_itemtype) {
      self::$plugins_forward_entity[$for_itemtype][] = $to_itemtype;
   }


   /**
    * Is entity informations forward To ?
    *
    * @since 0.84
    *
    * @param string $itemtype itemtype to check
    *
    * @return boolean
   **/
   static function isEntityForwardTo($itemtype) {

      if (in_array($itemtype, static::$forward_entity_to)) {
         return true;
      }
      //Fill forward_entity_to array with itemtypes coming from plugins
      if (isset(static::$plugins_forward_entity[static::getType()])
          && in_array($itemtype, static::$plugins_forward_entity[static::getType()])) {
         return true;
      }
      return false;
   }


   /**
    * Get rights for an item _ may be overload by object
    *
    * @since 0.85
    *
    * @param string $interface (defalt 'central')
    *
    * @return array array of rights to display
   **/
   function getRights($interface = 'central') {

      $values = [CREATE  => __('Create'),
                      READ    => __('Read'),
                      UPDATE  => __('Update'),
                      PURGE   => ['short' => __('Purge'),
                                       'long'  => _x('button', 'Delete permanently')]];

      $values += ObjectLock::getRightsToAdd( get_class($this), $interface );

      if ($this->maybeDeleted()) {
         $values[DELETE] = ['short' => __('Delete'),
                                 'long'  => _x('button', 'Put in trashbin')];
      }
      if ($this->usenotepad) {
         $values[READNOTE] = ['short' => __('Read notes'),
                                   'long' => __("Read the item's notes")];
         $values[UPDATENOTE] = ['short' => __('Update notes'),
                                     'long' => __("Update the item's notes")];
      }

      return $values;
   }

   /**
    * Generate link
    *
    * @since 9.1
    *
    * @param string     $link original string content
    * @param CommonDBTM $item item used to make replacements
    *
    * @return array of link contents (may have several when item have several IP / MAC cases)
   **/
   static function generateLinkContents($link, CommonDBTM $item) {
      return Link::generateLinkContents($link, $item);
   }


   /**
    * add files from a textarea (from $this->input['content'])
    * or a file input (from $this->input['_filename']) to an CommonDBTM object
    * create document if needed
    * create link from document to CommonDBTM object
    *
    * @since 9.2
    *
    * @param array $input   Input data
    * @param array $options array with theses keys
    *                        - force_update (default false) update the content field of the object
    *                        - content_field (default content) the field who receive the main text
    *                                                          (with images)
    *                        - name (default filename) name of the HTML input containing files
    *                        - date  Date to set on document_items
    * @return array the input param transformed
   **/
   function addFiles(array $input, $options = []) {
      global $CFG_GLPI;

      $default_options = [
         'force_update'  => false,
         'content_field' => 'content',
         'name'          => 'filename',
         'date'          => null,
      ];
      $options = array_merge($default_options, $options);

      $uploadName = '_' . $options['name'];
      $tagUploadName = '_tag_' . $options['name'];
      $prefixUploadName = '_prefix_' . $options['name'];

      if (!isset($input[$uploadName])
          || (count($input[$uploadName]) == 0)) {
         return $input;
      }
      $docadded     = [];
      $donotif      = isset($input['_donotif']) ? $input['_donotif'] : 0;
      $disablenotif = isset($input['_disablenotif']) ? $input['_disablenotif'] : 0;

      foreach ($input[$uploadName] as $key => $file) {
         $doc      = new Document();
         $docitem  = new Document_Item();
         $docID    = 0;
         $filename = GLPI_TMP_DIR."/".$file;
         $input2   = [];

         //If file tag is present
         if (isset($input[$tagUploadName])
             && !empty($input[$tagUploadName][$key])) {
            $input['_tag'][$key] = $input[$tagUploadName][$key];
         }

         //retrieve entity
         $entities_id = isset($_SESSION['glpiactive_entity']) ? $_SESSION['glpiactive_entity'] : 0;
         if (isset($this->fields["entities_id"])) {
            $entities_id = $this->fields["entities_id"];
         } else if (isset($input['entities_id'])) {
            $entities_id = $input['entities_id'];
         } else if (isset($input['_job']->fields['entities_id'])) {
            $entities_id = $input['_job']->fields['entities_id'];
         }

         // Check for duplicate
         if ($doc->getFromDBbyContent($entities_id, $filename)) {
            if (!$doc->fields['is_blacklisted']) {
               $docID = $doc->fields["id"];
            }
            // File already exist, we replace the tag by the existing one
            if (isset($input['_tag'][$key])
                && ($docID > 0)
                && isset($input[$options['content_field']])) {

               $input[$options['content_field']] = str_replace(
                  $input['_tag'][$key],
                  $doc->fields["tag"],
                  $input[$options['content_field']]
               );
               $docadded[$docID]['tag'] = $doc->fields["tag"];
            }

         } else {
            if ($this->getType() == 'Ticket') {
               //TRANS: Default document to files attached to tickets : %d is the ticket id
               $input2["name"] = addslashes(sprintf(__('Document Ticket %d'), $this->getID()));
               $input2["tickets_id"] = $this->getID();
            }

            if (isset($input['_tag'][$key])) {
               // Insert image tag
               $input2["tag"] = $input['_tag'][$key];
            }

            $input2["entities_id"]             = $entities_id;
            $input2["is_recursive"]            = 1;
            $input2["documentcategories_id"]   = $CFG_GLPI["documentcategories_id_forticket"];
            $input2["_only_if_upload_succeed"] = 1;
            $input2["_filename"]               = [$file];
            if (isset($this->input[$prefixUploadName][$key])) {
               $input2[$prefixUploadName]  = [$this->input[$prefixUploadName][$key]];
            }
            $docID = $doc->add($input2);

            if (isset($input['_tag'][$key])) {
               // Store image tag
               $docadded[$docID]['tag'] = $doc->fields["tag"];
            }
         }

         if ($docID > 0) {
            // complete doc information
            $docadded[$docID]['data'] = sprintf(__('%1$s - %2$s'),
                                                stripslashes($doc->fields["name"]),
                                                stripslashes($doc->fields["filename"]));
            $docadded[$docID]['filepath'] = $doc->fields["filepath"];

            // add doc - item link
            $toadd = [
               'documents_id'  => $docID,
               '_do_notif'     => $donotif,
               '_disablenotif' => $disablenotif,
               'itemtype'      => $this->getType(),
               'items_id'      => $this->getID()
            ];
            // Set date, needed if it differs from the creation date
            $toadd['date'] = $options['date'] ?? $_SESSION['glpi_currenttime'];

            if (isset($input['users_id'])) {
               $toadd['users_id'] = $input['users_id'];
            }
            if (isset($input[$options['content_field']])
                && strpos($input[$options['content_field']], $doc->fields["tag"]) !== false
                && strpos($doc->fields['mime'], 'image/') !== false) {
               //do not display inline docs in timeline
               $toadd['timeline_position'] = CommonITILObject::NO_TIMELINE;
            }

            $docitem->add($toadd);
         }
         // Only notification for the first New doc
         $donotif = false;
      }

      // manage content transformation
      if (isset($input[$options['content_field']])) {
         $input[$options['content_field']] = Toolbox::convertTagToImage(
            $input[$options['content_field']],
            $this,
            $docadded
         );

         if (isset($this->input['_forcenotif'])) {
            $input['_forcenotif'] = $this->input['_forcenotif'];
            unset($input['_disablenotif']);
         }

         // force update of content on add process (we are in post_addItem function)
         if ($options['force_update']) {
            $this->fields[$options['content_field']] = $input[$options['content_field']];
            $this->updateInDB([$options['content_field']]);
         }
      }

      return $input;
   }

   /**
    * Get autofill mark for/from templates
    *
    * @param string $field   Field name
    * @param array  $options Withtemplate parameter
    * @param string $value   Optional value (if field to check is not part of current itemtype)
    *
    * @return string
    */
   public function getAutofillMark($field, $options, $value = null) {
      $mark = '';
      $title = null;
      if (($this->isTemplate() || $this->isNewItem()) && $options['withtemplate'] == 1) {
         $title = __s('You can define an autofill template');
      } else if ($this->isTemplate()) {
         if ($value === null) {
            $value = $this->getField($field);
         }
         $len = Toolbox::strlen($value);
         if ($len > 8
            && Toolbox::substr($value, 0, 4) === '&lt;'
            && Toolbox::substr($value, $len -4, 4) === '&gt;'
            && preg_match("/\\#{1,10}/", Toolbox::substr($value, 4, $len - 8))
         ) {
            $title = __s('Autofilled from template');
         } else {
            return '';
         }
      }
      if ($title !== null) {
         $mark = "<i class='fa fa-magic' title='$title'></i>";
      }
      return $mark;
   }

   /**
   * Manage business rules for assets
   *
   * @since 9.4
   *
   * @param boolean $condition the condition (RuleAsset::ONADD or RuleAsset::ONUPDATE)
   *
   * @return void
   */
   private function assetBusinessRules($condition) {
      global $CFG_GLPI;

      //Only process itemtype that are assets
      if (in_array($this->getType(), $CFG_GLPI['asset_types'])) {
         $ruleasset          = new RuleAssetCollection();
         $input              = $this->input;
         $input['_itemtype'] = $this->getType();

         //If _auto is not defined : it's a manual process : set it's value to 0
         if (!isset($this->input['_auto'])) {
            $input['_auto'] = 0;
         }
         //Set the condition (add or update)
         $params = [
            'condition' => $condition
         ];
         $output = $ruleasset->processAllRules($input, [], $params);
         //If at least one rule has matched
         if (isset($output['_rule_process'])) {
            foreach ($output as $key => $value) {
               if ($key == '_rule_process' || $key == '_no_rule_matches') {
                  continue;
               }
               //Add the rule output to the input array
               $this->input[$key] = $value;
            }
         }
      }
   }

   /**
    * Ensure the relation would not create a circular parent-child relation.
    * @since 9.5.0
    * @param int    $items_id The ID of the item to evaluate.
    * @param int    $parents_id  The wanted parent of the specified item.
    * @return bool True if there is a circular relation.
    */
   static function checkCircularRelation($items_id, $parents_id) {
      global $DB;

      $fk = static::getForeignKeyField();
      if ($items_id == 0 || $parents_id == 0 || !$DB->fieldExists(static::getTable(), $fk)) {
         return false;
      }

      $next_parent = $parents_id;
      while ($next_parent > 0) {
         if ($next_parent == $items_id) {
            // This item is a parent higher up
            return true;
         }
         $iterator = $DB->request([
            'SELECT' => [$fk],
            'FROM'   => static::getTable(),
            'WHERE'  => ['id' => $next_parent]
         ]);
         if ($iterator->count()) {
            $next_parent = $iterator->next()[$fk];
         } else {
            // Invalid parent
            return false;
         }
      }
      // No circular relations
      return false;
   }

   /**
    * Get incidents, request, changes and problem linked to this object
    *
    * @return array
    */
   public function getITILTickets(bool $count = false) {
      $ticket = new Ticket();
      $problem = new Problem();
      $change = new Change();

      $data = [
         'incidents' => iterator_to_array(
            $ticket->getActiveTicketsForItem(
               get_class($this),
               $this->getID(),
               Ticket::INCIDENT_TYPE
            ),
            false
         ),
         'requests'  => iterator_to_array(
            $ticket->getActiveTicketsForItem(
               get_class($this),
               $this->getID(),
               Ticket::DEMAND_TYPE
            ),
            false
         ),
         'changes'   => iterator_to_array(
            $change->getActiveChangesForItem(
               get_class($this),
               $this->getID()
            ),
            false
         ),
         'problems'  => iterator_to_array(
            $problem->getActiveProblemsForItem(
               get_class($this),
               $this->getID()
            ),
            false
         )
      ];

      if ($count) {
         $data['count'] = count($data['incidents'])
            + count($data['requests'])
            + count($data['changes'])
            + count($data['problems']);
      }

      return $data;
   }

   static function getIcon() {
      return "fas fa-empty-icon";
   }

   /**
    * Get cache key containing raw name for a given itemtype and id
    *
    * @since 9.5
    *
    * @param string  $itemtype
    * @param int     $id
    */
   public static function getCacheKeyForFriendlyName($itemtype, $id) {
      return "raw_name__{$itemtype}__{$id}";
   }

   /**
    * Get friendly name by items id
    * The purpose of this function is to try to access the friendly name
    * without having to read the object from the database
    *
    * @since 9.5
    *
    * @param int $id
    *
    * @return string Friendly name of the object
    */
   public static function getFriendlyNameById($id) {
      $item = new static();
      $item->getFromDB($id);
      return $item->getFriendlyName();
   }

   /**
    * Return the computed friendly name and set the cache.
    *
    * @since 9.5
    *
    * @return string
    */
   final public function getFriendlyName() {
      return $this->computeFriendlyName();
   }

   /**
    * Compute the friendly name of the object
    *
    * @since 9.5
    *
    * @return string
    */
   protected function computeFriendlyName() {
      if (isset($this->fields[static::getNameField()])) {
         return $this->fields[static::getNameField()];
      }
      return '';
   }

   /**
    * Retrieve an item from the database
    *
    * @param integer $ID ID of the item to get
    *
    * @return boolean true if succeed else false
   */
   public static function getById(int $id) {
      $item = new static();

      if (!$item->getFromDB($id)) {
         return false;
      }

      return $item;
   }

   /**
    * Correct entity id if needed when cloning a template
    *
    * @param array  $data
    * @param string $parent_field
    *
    * @return array
    */
   public static function checkTemplateEntity(
      array $data,
      $parent_id,
      $parent_itemtype
   ) {
      // No entity field -> no modification needed
      if (!isset($data['entities_id'])) {
         return $data;
      }

      // If the entity used in the template in not allowed for our current user,
      // fallback to the parent template entity
      if (!Session::haveAccessToEntity($data['entities_id'])) {
         // Load parent
         $parent = new $parent_itemtype();

         if (!$parent->getFromDB($parent_id)) {
            // Can't load parent -> no modification
            return $data;
         }

         $data['entities_id'] = $parent->getEntityID();
      }

      return $data;
   }

   /**
    * Friendly names may uses multiple fields (e.g user: first name + last name)
    * Return the computed criteria to use in a WHERE clause.
    *
    * @param string $filter
    * @return array
    */
   public static function getFriendlyNameSearchCriteria(string $filter): array {
      $table      = static::getTable();
      $name_field = static::getNameField();
      $name       = DBmysql::quoteName("$table.$name_field");
      $filter     = strtolower($filter);

      return [
         'RAW' => [
            "LOWER($name)" => ['LIKE', "%$filter%"],
         ]
      ];
   }

   /**
    * Friendly names may uses multiple fields (e.g user: first name + last name)
    * Return the computed field name to use in a SELECT clause.
    *
    * @param string $alias
    * @return mixed
    */
   public static function getFriendlyNameFields(string $alias = "name") {
      $table = static::getTable();
      $name_field = static::getNameField();

      return "$table.$name_field AS $alias";
   }
}
