<?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/>.
 * ---------------------------------------------------------------------
 */

/**
 * @since 9.1
 */

namespace Glpi\Api;

use APIClient;
use Auth;
use Change;
use CommonDevice;
use CommonGLPI;
use CommonITILObject;
use Config;
use Contract;
use Document;
use Dropdown;
use Glpi\Exception\ForgetPasswordException;
use Glpi\Exception\PasswordTooWeakException;
use Html;
use Infocom;
use Item_Devices;
use Log;
use Michelf\MarkdownExtra;
use NetworkEquipment;
use NetworkPort;
use Notepad;
use Problem;
use QueryExpression;
use SavedSearch;
use Search;
use Session;
use Software;
use Ticket;
use Toolbox;
use User;

abstract class API extends CommonGLPI {

   // permit writing to $_SESSION
   protected $session_write = false;

   static $api_url = "";
   static $content_type = "application/json";
   protected $format;
   protected $iptxt         = "";
   protected $ipnum         = "";
   protected $app_tokens    = [];
   protected $apiclients_id = 0;
   protected $deprecated_item = null;

   /**
    * First function used on api call
    * Parse sended query/parameters and call the corresponding API::method
    *
    * @return void self::returnResponse called for output
    */
   abstract public function call();

   /**
    * Needed to transform params of called api in $this->parameters attribute
    *
    * @return string endpoint called
    */
   abstract protected function parseIncomingParams();

   /**
    * Generic messages
    *
    * @since 9.1
    *
    * @param mixed   $response          string message or array of data to send
    * @param integer $httpcode          http code (see : https://en.wikipedia.org/wiki/List_of_HTTP_status_codes)
    * @param array   $additionalheaders headers to send with http response (must be an array(key => value))
    *
    * @return void
    */
   abstract protected function returnResponse($response, $httpcode = 200, $additionalheaders = []);

   /**
    * Upload and validate files from request and append to $this->parameters['input']
    *
    * @return void
    */
   abstract protected function manageUploadedFiles();

   /**
    * Constructor
    *
    * @var array $CFG_GLPI
    * @var DBmysql $DB
    *
    * @return void
    */
   public function initApi() {
      global $CFG_GLPI;

      // Load GLPI configuration
      include_once (GLPI_ROOT . '/inc/includes.php');
      $variables = get_defined_vars();
      foreach ($variables as $var => $value) {
         if ($var === strtoupper($var)) {
            $GLOBALS[$var] = $value;
         }
      }

      // construct api url
      self::$api_url = trim($CFG_GLPI['url_base_api'], "/");

      // Don't display error in result
      ini_set('display_errors', 'Off');

      // Avoid keeping messages between api calls
      $_SESSION["MESSAGE_AFTER_REDIRECT"] = [];

      // check if api is enabled
      if (!$CFG_GLPI['enable_api']) {
         $this->returnError(__("API disabled"), "", "", false);
         exit;
      }

      // retrieve ip of client
      $this->iptxt = Toolbox::getRemoteIpAddress();
      $this->ipnum = (strstr($this->iptxt, ':')===false ? ip2long($this->iptxt) : '');

      // check ip access
      $apiclient = new APIClient;
      $where_ip = [];
      if ($this->ipnum) {
         $where_ip = [
            'OR' => [
               'ipv4_range_start' => null,
               [
                  'ipv4_range_start'   => ['<=', $this->ipnum],
                  'ipv4_range_end'     => ['>=', $this->ipnum]
               ]
            ]
         ];
      } else {
         $where_ip = [
            'OR' => [
               ['ipv6'  => null],
               ['ipv6'  => $this->iptxt]
            ]
         ];
      }
      $found_clients = $apiclient->find(['is_active' => 1] + $where_ip);
      if (count($found_clients) <= 0) {
         $this->returnError(__("There isn't an active API client matching your IP address in the configuration").
                            " (".$this->iptxt.")",
                            "", "ERROR_NOT_ALLOWED_IP", false);
      }
      $app_tokens = array_column($found_clients, 'app_token');
      $apiclients_id = array_column($found_clients, 'id');
      $this->app_tokens = array_combine($apiclients_id, $app_tokens);
   }

   /**
    * Set headers according to cross origin ressource sharing
    *
    * @param string $verb Http verb (GET, POST, PUT, DELETE, OPTIONS)
    *
    * @return void
    */
   protected function cors($verb = 'GET') {
      if (isset($_SERVER['HTTP_ORIGIN'])) {
         header("Access-Control-Allow-Origin: *");
      }

      if ($this->verb == 'GET' || $this->verb == 'OPTIONS') {
         header("Access-Control-Expose-Headers: content-type, content-range, accept-range");
      }

      if ($this->verb == "OPTIONS") {
         if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
            header("Access-Control-Allow-Methods: PUT, GET, POST, DELETE, OPTIONS");
         }

         if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
            header("Access-Control-Allow-Headers: ".
                   "origin, content-type, accept, session-token, authorization");
         }
         exit(0);
      }
   }


   /**
    * Init GLPI Session
    *
    * @param array $params array with theses options :
    *    - a couple 'name' & 'password' : 2 parameters to login with user authentication
    *         OR
    *    - an 'user_token' defined in User Configuration
    *
    * @return array with session_token
    */
   protected function initSession($params = []) {
      global $CFG_GLPI;

      $this->checkAppToken();
      $this->logEndpointUsage(__FUNCTION__);

      if ((!isset($params['login'])
           || empty($params['login'])
           || !isset($params['password'])
           || empty($params['password']))
         && (!isset($params['user_token'])
             || empty($params['user_token']))) {
         $this->returnError(__("parameter(s) login, password or user_token are missing"), 400,
                            "ERROR_LOGIN_PARAMETERS_MISSING");
      }

      $auth = new Auth();

      // fill missing params (in case of user_token)
      if (!isset($params['login'])) {
         $params['login'] = '';
      }
      if (!isset($params['password'])) {
         $params['password'] = '';
      }

      $noAuto = true;
      if (isset($params['user_token']) && !empty($params['user_token'])) {
         $_REQUEST['user_token'] = $params['user_token'];
         $noAuto = false;

      } else if (!$CFG_GLPI['enable_api_login_credentials']) {
         $this->returnError(__("usage of initSession resource with credentials is disabled"), 400,
                            "ERROR_LOGIN_WITH_CREDENTIALS_DISABLED", false);
      }

      if (!isset($params['auth'])) {
         $params['auth'] = '';
      }

      // login on glpi
      if (!$auth->login($params['login'], $params['password'], $noAuto, false, $params['auth'])) {
         $err = Html::clean($auth->getErr());
         if (isset($params['user_token'])
             && !empty($params['user_token'])) {
            return $this->returnError(__("parameter user_token seems invalid"), 401, "ERROR_GLPI_LOGIN_USER_TOKEN", false);
         }
         return $this->returnError($err, 401, "ERROR_GLPI_LOGIN", false);
      }

      // stop session and return session key
      session_write_close();
      $data = ['session_token' => $_SESSION['valid_id']];

      // Insert session data if requested
      $get_full_session = $params['get_full_session'] ?? false;
      if ($get_full_session) {
         $data['session'] = $_SESSION;
      }

      return $data;
   }


   /**
    * Kill GLPI Session
    * Use 'session_token' param in $this->parameters
    *
    * @return boolean
    */
   protected function killSession() {

      $this->initEndpoint(false, __FUNCTION__);
      return Session::destroy();
   }


   /**
    * Retrieve GLPI Session initialised by initSession function
    * Use 'session_token' param in $this->parameters
    *
    * @return void
    */
   protected function retrieveSession() {

      if (isset($this->parameters['session_token'])
          && !empty($this->parameters['session_token'])) {
         $current = session_id();
         $session = trim($this->parameters['session_token']);

         if (file_exists(GLPI_ROOT . '/inc/downstream.php')) {
            include_once (GLPI_ROOT . '/inc/downstream.php');
         }

         if ($session!=$current && !empty($current)) {
            session_destroy();
         }
         if ($session!=$current && !empty($session)) {
            session_id($session);
         }
      }
   }


   /**
    * Change active entity to the entities_id one.
    *
    * @param array $params array with theses options :
    *   - 'entities_id': (default 'all') ID of the new active entity ("all" = load all possible entities). Optionnal
    *   - 'is_recursive': (default false) Also display sub entities of the active entity.  Optionnal
    *
    * @return array|bool
    */
   protected function changeActiveEntities($params = []) {

      $this->initEndpoint();

      if (!isset($params['entities_id'])) {
         $entities_id = 'all';
      } else {
         $entities_id = intval($params['entities_id']);
      }

      if (!isset($params['is_recursive'])) {
         $params['is_recursive'] = false;
      } else if (!is_bool($params['is_recursive'])) {
         return $this->returnError();
      }

      if (!Session::changeActiveEntities($entities_id, $params['is_recursive'])) {
         return $this->returnError();
      }

      return true;
   }


   /**
    * Return all the possible entity of the current logged user (and for current active profile)
    *
    * @param array $params array with theses options :
    *   - 'is_recursive': (default false) Also display sub entities of the active entity. Optionnal
    *
    * @return array of entities (with id and name)
    */
   protected function getMyEntities($params = []) {

      $this->initEndpoint();

      if (!isset($params['is_recursive'])) {
         $params['is_recursive'] = false;
      }

      $myentities = [];
      foreach ($_SESSION['glpiactiveprofile']['entities'] as $entity) {
         if ($entity['is_recursive'] == 1 && $params['is_recursive'] == 1) {
            $sons = getSonsOf('glpi_entities', $entity['id']);
            foreach ($sons as $entity_id) {
               if ($entity_id != $entity['id']) {
                  $myentities[] = ['id'   => $entity_id,
                                   'name' => Dropdown::getDropdownName("glpi_entities",
                                                                       $entity_id)];
               }
            }
         }
         $myentities[] = ['id' => $entity['id'],
                          'name' => Dropdown::getDropdownName("glpi_entities",
                                                                   $entity['id'])];
      }
      return ['myentities' => $myentities];
   }




   /**
    * return active entities of current logged user
    *
    * @return array with 3 keys :
    *  - active_entity : current set entity
    *  - active_entity_recursive : boolean, if we see sons of this entity
    *  - active_entities : array all active entities (active_entity and its sons)
    */
   protected function getActiveEntities() {

      $this->initEndpoint();

      $actives_entities = [];
      foreach (array_values($_SESSION['glpiactiveentities']) as $active_entity) {
         $actives_entities[] = ['id' => $active_entity];
      }

      return ["active_entity" => [
                     "id"                      => $_SESSION['glpiactive_entity'],
                     "active_entity_recursive" => $_SESSION['glpiactive_entity_recursive'],
                     "active_entities"         => $actives_entities]];

   }




   /**
    * set a profile to active
    *
    * @param array $params with theses options :
    *    - profiles_id : identifier of profile to set
    *
    * @return boolean
    */
   protected function changeActiveProfile($params = []) {

      $this->initEndpoint();

      if (!isset($params['profiles_id'])) {
         $this->returnError();
      }

      $profiles_id = intval($params['profiles_id']);
      if (isset($_SESSION['glpiprofiles'][$profiles_id])) {
         return Session::changeProfile($profiles_id);
      }

      $this->messageNotfoundError();
   }




   /**
    * Return all the profiles associated to logged user
    *
    * @return array of profiles (with associated rights)
    */
   protected function getMyProfiles() {

      $this->initEndpoint();

      $myprofiles = [];
      foreach ($_SESSION['glpiprofiles'] as $profiles_id => $profile) {
         // append if of the profile into values
         $profile = ['id' => $profiles_id] + $profile;

         // don't keep keys for entities
         $profile['entities'] = array_values($profile['entities']);

         // don't keep keys for profiles
         $myprofiles[] = $profile;
      }
      return ['myprofiles' => $myprofiles];
   }




   /**
    * Return the current active profile
    *
    * @return integer the profiles_id
    */
   protected function getActiveProfile() {

      $this->initEndpoint();
      return ["active_profile" => $_SESSION['glpiactiveprofile']];
   }




   /**
    * Return the current php $_SESSION
    *
    * @return array
    */
   protected function getFullSession() {

      $this->initEndpoint();
      return ['session' => $_SESSION];
   }



   /**
    * Return the current $CFG_GLPI
    *
    * @return array
     */
   protected function getGlpiConfig() {
      $this->initEndpoint();

      return ['cfg_glpi' => Config::getSafeConfig()];
   }


   /**
    * Return the instance fields of itemtype identified by id
    *
    * @param string  $itemtype itemtype (class) of object
    * @param integer $id       identifier of object
    * @param array   $params   with theses options :
    *    - 'expand_dropdowns': Show dropdown's names instead of id. default: false. Optionnal
    *    - 'get_hateoas':      Show relation of current item in a links attribute. default: true. Optionnal
    *    - 'get_sha1':         Get a sha1 signature instead of the full answer. default: false. Optionnal
    *    - 'with_devices':  Only for [Computer, NetworkEquipment, Peripheral, Phone, Printer], Optionnal.
    *    - 'with_disks':       Only for Computer, retrieve the associated filesystems. Optionnal.
    *    - 'with_softwares':   Only for Computer, retrieve the associated softwares installations. Optionnal.
    *    - 'with_connections': Only for Computer, retrieve the associated direct connections (like peripherals and printers) .Optionnal.
    *    - 'with_networkports':Retrieve all network connections and advanced network informations. Optionnal.
    *    - 'with_infocoms':    Retrieve financial and administrative informations. Optionnal.
    *    - 'with_contracts':   Retrieve associated contracts. Optionnal.
    *    - 'with_documents':   Retrieve associated external documents. Optionnal.
    *    - 'with_tickets':     Retrieve associated itil tickets. Optionnal.
    *    - 'with_problems':    Retrieve associated itil problems. Optionnal.
    *    - 'with_changes':     Retrieve associated itil changes. Optionnal.
    *    - 'with_notes':       Retrieve Notes (if exists, not all itemtypes have notes). Optionnal.
    *    - 'with_logs':        Retrieve historical. Optionnal.
    *    - 'add_keys_names':   Get friendly names. Optionnal.
    *
    * @return array    fields of found object
    */
   protected function getItem($itemtype, $id, $params = []) {
      global $CFG_GLPI, $DB;

      $this->initEndpoint();
      $itemtype = $this->handleDepreciation($itemtype);

      // default params
      $default = ['expand_dropdowns'  => false,
                       'get_hateoas'       => true,
                       'get_sha1'          => false,
                       'with_devices'   => false,
                       'with_disks'        => false,
                       'with_softwares'    => false,
                       'with_connections'  => false,
                       'with_networkports' => false,
                       'with_infocoms'     => false,
                       'with_contracts'    => false,
                       'with_documents'    => false,
                       'with_tickets'      => false,
                       'with_problems'     => false,
                       'with_changes'      => false,
                       'with_notes'        => false,
                       'with_logs'         => false,
                       'add_keys_names'    => [],
      ];
      $params = array_merge($default, $params);

      $item = new $itemtype;
      if (!$item->getFromDB($id)) {
         return $this->messageNotfoundError();
      }
      if (!$item->can($id, READ)) {
         return $this->messageRightError();
      }

      $fields = $item->fields;

      // avoid disclosure of critical fields
      $item::unsetUndisclosedFields($fields);

      // retrieve devices
      if (isset($params['with_devices'])
          && $params['with_devices']
          && in_array($itemtype, Item_Devices::getConcernedItems())) {
         $all_devices = [];
         foreach (Item_Devices::getItemAffinities($item->getType()) as $device_type) {
            $found_devices = getAllDataFromTable(
               $device_type::getTable(), [
                  'items_id'     => $item->getID(),
                  'itemtype'     => $item->getType(),
                  'is_deleted'   => 0
               ],
               true
            );

            foreach ($found_devices as &$device) {
               unset($device['items_id']);
               unset($device['itemtype']);
               unset($device['is_deleted']);
            }

            if (!empty($found_devices)) {
               $all_devices[$device_type] = $found_devices;
            }
         }
         $fields['_devices'] = $all_devices;
      }

      // retrieve computer disks
      if (isset($params['with_disks'])
          && $params['with_disks']
          && in_array($itemtype, $CFG_GLPI['itemdeviceharddrive_types'])) {
         // build query to retrive filesystems
         $fs_iterator = $DB->request([
            'SELECT'    => [
               'glpi_filesystems.name AS fsname',
               'glpi_items_disks.*'
            ],
            'FROM'      => 'glpi_items_disks',
            'LEFT JOIN'  => [
               'glpi_filesystems' => [
                  'ON' => [
                     'glpi_items_disks'   => 'filesystems_id',
                     'glpi_filesystems'   => 'id'
                  ]
               ]
            ],
            'WHERE'     => [
               'items_id'     => $id,
               'itemtype'     => $itemtype,
               'is_deleted'   => 0
            ]
         ]);
         $fields['_disks'] = [];
         while ($data = $fs_iterator->next()) {
            unset($data['items_id']);
            unset($data['is_deleted']);
            $fields['_disks'][] = ['name' => $data];
         }
      }

      // retrieve computer softwares
      if (isset($params['with_softwares'])
            && $params['with_softwares']
            && in_array($itemtype, $CFG_GLPI['software_types'])) {
         $fields['_softwares'] = [];
         if (!Software::canView()) {
            $fields['_softwares'] = $this->arrayRightError();
         } else {
            $soft_iterator = $DB->request([
               'SELECT'    => [
                  'glpi_softwares.softwarecategories_id',
                  'glpi_softwares.id AS softwares_id',
                  'glpi_softwareversions.id AS softwareversions_id',
                  'glpi_items_softwareversions.is_dynamic',
                  'glpi_softwareversions.states_id',
                  'glpi_softwares.is_valid'
               ],
               'FROM'      => 'glpi_items_softwareversions',
               'LEFT JOIN' => [
                  'glpi_softwareversions' => [
                     'ON' => [
                        'glpi_items_softwareversions' => 'softwareversions_id',
                        'glpi_softwareversions'       => 'id'
                     ]
                  ],
                  'glpi_softwares'        => [
                     'ON' => [
                        'glpi_softwareversions' => 'softwares_id',
                        'glpi_softwares'        => 'id'
                     ]
                  ]
               ],
               'WHERE'     => [
                  'glpi_items_softwareversions.items_id'   => $id,
                  'glpi_items_softwareversions.itemtype'   => $itemtype,
                  'glpi_items_softwareversions.is_deleted' => 0
               ],
               'ORDERBY'   => [
                  'glpi_softwares.name',
                  'glpi_softwareversions.name'
               ]
            ]);
            while ($data = $soft_iterator->next()) {
               $fields['_softwares'][] = $data;
            }
         }
      }

      // retrieve item connections
      if (isset($params['with_connections'])
          && $params['with_connections']
          && $itemtype == "Computer") {
         $fields['_connections'] = [];
         foreach ($CFG_GLPI["directconnect_types"] as $connect_type) {
            $connect_item = new $connect_type();
            if ($connect_item->canView()) {
               $connect_table = getTableForItemType($connect_type);
               $iterator = $DB->request([
                  'SELECT'    => [
                     'glpi_computers_items.id AS assoc_id',
                     'glpi_computers_items.computers_id AS assoc_computers_id',
                     'glpi_computers_items.itemtype AS assoc_itemtype',
                     'glpi_computers_items.items_id AS assoc_items_id',
                     'glpi_computers_items.is_dynamic AS assoc_is_dynamic',
                     "$connect_table.*"
                  ],
                  'FROM'      => 'glpi_computers_items',
                  'LEFT JOIN' => [
                     $connect_table => [
                        'ON' => [
                           'glpi_computers_items'  => 'items_id',
                           $connect_table          => 'id'
                        ]
                     ]
                  ],
                  'WHERE'     => [
                     'computers_id'                      => $id,
                     'itemtype'                          => $connect_type,
                     'glpi_computers_items.is_deleted'   => 0
                  ]
               ]);
               while ($data = $iterator->next()) {
                  $fields['_connections'][$connect_type][] = $data;
               }
            }
         }
      }

      // retrieve item networkports
      if (isset($params['with_networkports'])
          && $params['with_networkports']) {
         $fields['_networkports'] = [];
         if (!NetworkEquipment::canView()) {
            $fields['_networkports'] = $this->arrayRightError();
         } else {
            foreach (NetworkPort::getNetworkPortInstantiations() as $networkport_type) {
               $netport_table = $networkport_type::getTable();
               $netp_iterator = $DB->request([
                  'SELECT'    => [
                     'netp.id AS netport_id',
                     'netp.entities_id',
                     'netp.is_recursive',
                     'netp.logical_number',
                     'netp.name',
                     'netp.mac',
                     'netp.comment',
                     'netp.is_dynamic',
                     'netp_subtable.*'
                  ],
                  'FROM'      => 'glpi_networkports AS netp',
                  'LEFT JOIN' => [
                     "$netport_table AS netp_subtable" => [
                        'ON' => [
                           'netp_subtable'   => 'networkports_id',
                           'netp'            => 'id'
                        ]
                     ]
                  ],
                  'WHERE'     => [
                     'netp.instantiation_type'  => $networkport_type,
                     'netp.items_id'            => $id,
                     'netp.itemtype'            => $itemtype,
                     'netp.is_deleted'          => 0
                  ]
               ]);

               while ($data = $netp_iterator->next()) {
                  if (isset($data['netport_id'])) {
                     // append network name
                     $concat_expr = new QueryExpression(
                        "GROUP_CONCAT(CONCAT(".$DB->quoteName('ipadr.id').", ".$DB->quoteValue(Search::SHORTSEP)." , ".$DB->quoteName('ipadr.name').")
                        SEPARATOR ".$DB->quoteValue(Search::LONGSEP).") AS ".$DB->quoteName('ipadresses')
                     );
                     $netn_iterator = $DB->request([
                        'SELECT'    => [
                           $concat_expr,
                           'netn.id AS networknames_id',
                           'netn.name AS networkname',
                           'netn.fqdns_id',
                           'fqdn.name AS fqdn_name',
                           'fqdn.fqdn'
                        ],
                        'FROM'      => [
                           'glpi_networknames AS netn'
                        ],
                        'LEFT JOIN' => [
                           'glpi_ipaddresses AS ipadr'               => [
                              'ON' => [
                                 'ipadr'  => 'items_id',
                                 'netn'   => 'id',
                                 [
                                    'AND' => ['ipadr.itemtype' => 'NetworkName']
                                 ]
                              ]
                           ],
                           'glpi_fqdns AS fqdn'                      => [
                              'ON' => [
                                 'fqdn'   => 'id',
                                 'netn'   => 'fqdns_id'
                              ]
                           ],
                           'glpi_ipaddresses_ipnetworks AS ipadnet'  => [
                              'ON' => [
                                 'ipadnet'   => 'ipaddresses_id',
                                 'ipadr'     => 'id'
                              ]
                           ],
                           'glpi_ipnetworks AS ipnet'                => [
                              'ON' => [
                                 'ipnet'     => 'id',
                                 'ipadnet'   => 'ipnetworks_id'
                              ]
                           ]
                        ],
                        'WHERE'     => [
                           'netn.itemtype'   => 'NetworkPort',
                           'netn.items_id'   => $data['netport_id']
                        ],
                        'GROUPBY'   => [
                           'netn.id',
                           'netn.name',
                           'netn.fqdns_id',
                           'fqdn.name',
                           'fqdn.fqdn'
                        ]
                     ]);

                     if (count($netn_iterator)) {
                        $data_netn = $netn_iterator->next();

                        $raw_ipadresses = explode(Search::LONGSEP, $data_netn['ipadresses']);
                        $ipadresses = [];
                        foreach ($raw_ipadresses as $ipadress) {
                           $ipadress = explode(Search::SHORTSEP, $ipadress);

                           //find ip network attached to these ip
                           $ipnetworks = [];
                           $ipnet_iterator = $DB->request([
                              'SELECT'       => [
                                 'ipnet.id',
                                 'ipnet.completename',
                                 'ipnet.name',
                                 'ipnet.address',
                                 'ipnet.netmask',
                                 'ipnet.gateway',
                                 'ipnet.ipnetworks_id',
                                 'ipnet.comment'
                              ],
                              'FROM'         => 'glpi_ipnetworks AS ipnet',
                              'INNER JOIN'   => [
                                 'glpi_ipaddresses_ipnetworks AS ipadnet' => [
                                    'ON' => [
                                       'ipadnet'   => 'ipnetworks_id',
                                       'ipnet'     => 'id'
                                    ]
                                 ]
                              ],
                              'WHERE'        => [
                                 'ipadnet.ipaddresses_id'  => $ipadress[0]
                              ]
                           ]);
                           while ($data_ipnet = $ipnet_iterator->next()) {
                              $ipnetworks[] = $data_ipnet;
                           }

                           $ipadresses[] = [
                              'id'        => $ipadress[0],
                              'name'      => $ipadress[1],
                              'IPNetwork' => $ipnetworks
                           ];
                        }

                        $data['NetworkName'] = [
                           'id'         => $data_netn['networknames_id'],
                           'name'       => $data_netn['networkname'],
                           'fqdns_id'   => $data_netn['fqdns_id'],
                           'FQDN'       => [
                              'id'   => $data_netn['fqdns_id'],
                              'name' => $data_netn['fqdn_name'],
                              'fqdn' => $data_netn['fqdn']
                           ],
                           'IPAddress' => $ipadresses
                        ];
                     }
                  }

                  $fields['_networkports'][$networkport_type][] = $data;
               }
            }
         }
      }

      // retrieve item infocoms
      if (isset($params['with_infocoms'])
          && $params['with_infocoms']) {
         $fields['_infocoms'] = [];
         if (!Infocom::canView()) {
            $fields['_infocoms'] = $this->arrayRightError();
         } else {
            $ic = new Infocom();
            if ($ic->getFromDBforDevice($itemtype, $id)) {
               $fields['_infocoms'] = $ic->fields;
            }
         }
      }

      // retrieve item contracts
      if (isset($params['with_contracts'])
          && $params['with_contracts']) {
         $fields['_contracts'] = [];
         if (!Contract::canView()) {
            $fields['_contracts'] = $this->arrayRightError();
         } else {
            $iterator = $DB->request([
               'SELECT'    => ['glpi_contracts_items.*'],
               'FROM'      => 'glpi_contracts_items',
               'LEFT JOIN' => [
                  'glpi_contracts'  => [
                     'ON' => [
                        'glpi_contracts_items'  => 'contracts_id',
                        'glpi_contracts'        => 'id'
                     ]
                  ],
                  'glpi_entities'   => [
                     'ON' => [
                        'glpi_contracts_items'  => 'entities_id',
                        'glpi_entities'         => 'id'
                     ]
                  ]
               ],
               'WHERE'     => [
                  'glpi_contracts_items.items_id'  => $id,
                  'glpi_contracts_items.itemtype'  => $itemtype
               ] + getEntitiesRestrictCriteria('glpi_contracts', '', '', true),
               'ORDERBY'   => 'glpi_contracts.name'
            ]);
            while ($data = $iterator->next()) {
               $fields['_contracts'][] = $data;
            }
         }
      }

      // retrieve item documents
      if (isset($params['with_documents'])
          && $params['with_documents']) {
         $fields['_documents'] = [];
         if (!($item instanceof CommonITILObject)
             && $itemtype != 'KnowbaseItem'
             && $itemtype != 'Reminder'
             && !Document::canView()) {
            $fields['_documents'] = $this->arrayRightError();
         } else {
            $doc_criteria = [
               'glpi_documents_items.items_id'  => $id,
               'glpi_documents_items.itemtype'  => $itemtype
            ];
            if ($item instanceof CommonITILObject) {
               $doc_criteria = [
                  $item->getAssociatedDocumentsCriteria(),
                  'timeline_position' => ['>', CommonITILObject::NO_TIMELINE], // skip inlined images
               ];
            }
            $doc_iterator = $DB->request([
               'SELECT'    => [
                  'glpi_documents_items.id AS assocID',
                  'glpi_documents_items.date_creation AS assocdate',
                  'glpi_entities.id AS entityID',
                  'glpi_entities.completename AS entity',
                  'glpi_documentcategories.completename AS headings',
                  'glpi_documents.*'
               ],
               'FROM'      => 'glpi_documents_items',
               'LEFT JOIN' => [
                  'glpi_documents'           => [
                     'ON' => [
                        'glpi_documents_items'  => 'documents_id',
                        'glpi_documents'        => 'id'
                     ]
                  ],
                  'glpi_entities'            => [
                     'ON' => [
                        'glpi_documents'  => 'entities_id',
                        'glpi_entities'   => 'id'
                     ]
                  ],
                  'glpi_documentcategories'  => [
                     'ON' => [
                        'glpi_documents'           => 'documentcategories_id',
                        'glpi_documentcategories'  => 'id'
                     ]
                  ]
               ],
               'WHERE'     => $doc_criteria,
            ]);
            while ($data = $doc_iterator->next()) {
               $fields['_documents'][] = $data;
            }
         }
      }

      // retrieve item tickets
      if (isset($params['with_tickets'])
          && $params['with_tickets']) {
         $fields['_tickets'] = [];
         if (!Ticket::canView()) {
            $fields['_tickets'] = $this->arrayRightError();
         } else {
            $criteria = Ticket::getCommonCriteria();
            $criteria['WHERE'] = [
               'glpi_items_tickets.items_id' => $id,
               'glpi_items_tickets.itemtype' => $itemtype
            ] + getEntitiesRestrictCriteria(Ticket::getTable());
            $iterator = $DB->request($criteria);
            while ($data = $iterator->next()) {
               $fields['_tickets'][] = $data;
            }
         }
      }

      // retrieve item problems
      if (isset($params['with_problems'])
          && $params['with_problems']) {
         $fields['_problems'] = [];
         if (!Problem::canView()) {
            $fields['_problems'] = $this->arrayRightError();
         } else {
            $criteria = Problem::getCommonCriteria();
            $criteria['WHERE'] = [
               'glpi_items_problems.items_id' => $id,
               'glpi_items_problems.itemtype' => $itemtype
            ] + getEntitiesRestrictCriteria(Problem::getTable());
            $iterator = $DB->request($criteria);
            while ($data = $iterator->next()) {
               $fields['_problems'][] = $data;
            }
         }
      }

      // retrieve item changes
      if (isset($params['with_changes'])
          && $params['with_changes']) {
         $fields['_changes'] = [];
         if (!Change::canView()) {
            $fields['_changes'] = $this->arrayRightError();
         } else {
            $criteria = Change::getCommonCriteria();
            $criteria['WHERE'] = [
               'glpi_changes_items.items_id' => $id,
               'glpi_changes_items.itemtype' => $itemtype
            ] + getEntitiesRestrictCriteria(Change::getTable());
            $iterator = $DB->request($criteria);
            while ($data = $iterator->next()) {
               $fields['_changes'][] = $data;
            }
         }
      }

      // retrieve item notes
      if (isset($params['with_notes'])
          && $params['with_notes']) {
         $fields['_notes'] = [];
         if (!Session::haveRight($itemtype::$rightname, READNOTE)) {
            $fields['_notes'] = $this->arrayRightError();
         } else {
            $fields['_notes'] = Notepad::getAllForItem($item);
         }
      }

      // retrieve item logs
      if (isset($params['with_logs'])
          && $params['with_logs']) {
         $fields['_logs'] = [];
         if (!Session::haveRight($itemtype::$rightname, READNOTE)) {
            $fields['_logs'] = $this->arrayRightError();
         } else {
            $fields['_logs'] = getAllDataFromTable(
               "glpi_logs", [
                  'items_id'  => $item->getID(),
                  'itemtype'  => $item->getType()
               ]
            );
         }
      }

      // expand dropdown (retrieve name of dropdowns) and get hateoas from foreign keys
      $fields = self::parseDropdowns($fields, $params);

      // get hateoas from children
      if ($params['get_hateoas']) {
         $hclasses = self::getHatoasClasses($itemtype);
         foreach ($hclasses as $hclass) {
            $fields['links'][] = ['rel'  => $hclass,
                                       'href' => self::$api_url."/$itemtype/".$item->getID()."/$hclass/"];
         }
      }

      // get sha1 footprint if needed
      if ($params['get_sha1']) {
         $fields = sha1(json_encode($fields, JSON_UNESCAPED_UNICODE
                                             | JSON_UNESCAPED_SLASHES
                                             | JSON_NUMERIC_CHECK));
      }

      if (count($params['add_keys_names']) > 0) {
         $fields["_keys_names"] = $this->getFriendlyNames(
            $fields,
            $params,
            $itemtype
         );
      }

      // Convert fields to the format expected by the deprecated type
      if ($this->isDeprecated()) {
         $fields = $this->deprecated_item->mapCurrentToDeprecatedFields($fields);
         $fields["links"] = $this->deprecated_item->mapCurrentToDeprecatedHateoas(
            $fields["links"] ?? []
         );
      }

      return $fields;
   }



   /**
    * Fill a sub array with a right error
    *
    * @return array
    */
   protected function arrayRightError() {

      return ['error'   => 401,
                   'message' => __("You don't have permission to perform this action.")];
   }





   /**
    * Return a collection of rows of the desired itemtype
    *
    * @param string  $itemtype   itemtype (class) of object
    * @param array   $params     with theses options :
    * - 'expand_dropdowns' (default: false): show dropdown's names instead of id. Optionnal
    * - 'get_hateoas'      (default: true): show relations of items in a links attribute. Optionnal
    * - 'only_id'          (default: false): keep only id in fields list. Optionnal
    * - 'range'            (default: 0-50): limit the list to start-end attributes
    * - 'sort'             (default: id): sort by the field.
    * - 'order'            (default: ASC): ASC(ending) or DESC(ending).
    * - 'searchText'       (default: NULL): array of filters to pass on the query (with key = field and value the search)
    * - 'is_deleted'       (default: false): show trashbin. Optionnal
    * - 'add_keys_names'   (default: []): insert raw name(s) for given itemtype(s) and fkey(s)
    * @param integer $totalcount output parameter who receive the total count of the query resulat.
    *                            As this function paginate results (with a mysql LIMIT),
    *                            we can have the full range. (default 0)
    *
    * @return array collection of fields
    */
   protected function getItems($itemtype, $params = [], &$totalcount = 0) {
      global $DB;

      $this->initEndpoint();
      $itemtype = $this->handleDepreciation($itemtype);

      // default params
      $default = ['expand_dropdowns' => false,
                       'get_hateoas'      => true,
                       'only_id'          => false,
                       'range'            => "0-".$_SESSION['glpilist_limit'],
                       'sort'             => "id",
                       'order'            => "ASC",
                       'searchText'       => null,
                       'is_deleted'       => false,
                       'add_keys_names'   => [],
      ];
      $params = array_merge($default, $params);

      if (!$itemtype::canView()) {
         return $this->messageRightError();
      }

      $found = [];
      $item = new $itemtype();
      $item->getEmpty();
      $table = getTableForItemType($itemtype);

      // transform range parameter in start and limit variables
      if (isset($params['range']) > 0) {
         if (preg_match("/^[0-9]+-[0-9]+\$/", $params['range'])) {
            $range = explode("-", $params['range']);
            $params['start']      = $range[0];
            $params['list_limit'] = $range[1]-$range[0]+1;
            $params['range']      = $range;
         } else {
            $this->returnError("range must be in format : [start-end] with integers");
         }
      } else {
         $params['range'] = [0, $_SESSION['glpilist_limit']];
      }

      // check parameters
      if (isset($params['order'])
          && !in_array(strtoupper($params['order']), ['DESC', 'ASC'])) {
         $this->returnError("order must be DESC or ASC");
      }
      if (!isset($item->fields[$params['sort']])) {
         $this->returnError("sort param is not a field of $table");
      }

      //specific case for restriction
      $already_linked_table = [];
      $join = Search::addDefaultJoin($itemtype, $table, $already_linked_table);
      $where = Search::addDefaultWhere($itemtype);
      if ($where == '') {
         $where = "1=1 ";
      }
      if ($item->maybeDeleted()) {
         $where.= "AND ".$DB->quoteName("$table.is_deleted")." = ".(int)$params['is_deleted'];
      }

      // add filter for a parent itemtype
      if (isset($this->parameters['parent_itemtype'])
          && isset($this->parameters['parent_id'])) {

         // check parent itemtype
         if (!Toolbox::isCommonDBTM($this->parameters['parent_itemtype'])
            && !Toolbox::isAPIDeprecated($this->parameters['parent_itemtype'])
         ) {
            $this->returnError(__("parent itemtype not found or not an instance of CommonDBTM"),
                               400,
                               "ERROR_ITEMTYPE_NOT_FOUND_NOR_COMMONDBTM");
         }

         $fk_parent = getForeignKeyFieldForItemType($this->parameters['parent_itemtype']);
         $fk_child = getForeignKeyFieldForItemType($itemtype);

         // check parent rights
         $parent_item = new $this->parameters['parent_itemtype'];
         if (!$parent_item->getFromDB($this->parameters['parent_id'])) {
            return $this->messageNotfoundError();
         }
         if (!$parent_item->can($this->parameters['parent_id'], READ)) {
            return $this->messageRightError();
         }

         // filter with parents fields
         if (isset($item->fields[$fk_parent])) {
            $where.= " AND ".$DB->quoteName("$table.$fk_parent")." = ".(int)$this->parameters['parent_id'];
         } else if (isset($item->fields['itemtype'])
                 && isset($item->fields['items_id'])) {
            $where.= " AND ".$DB->quoteName("$table.itemtype")." = ".$DB->quoteValue($this->parameters['parent_itemtype'])."
                       AND ".$DB->quoteName("$table.items_id")." = ".(int)$this->parameters['parent_id'];
         } else if (isset($parent_item->fields[$fk_child])) {
            $parentTable = getTableForItemType($this->parameters['parent_itemtype']);
            $join.= " LEFT JOIN ".$DB->quoteName($parentTable)." ON ".$DB->quoteName("$parentTable.$fk_child")." = ".$DB->quoteName("$table.id");
            $where.= " AND ".$DB->quoteName("$parentTable.id")." = " . (int)$this->parameters['parent_id'];
         } else if (isset($parent_item->fields['itemtype'])
                 && isset($parent_item->fields['items_id'])) {
            $parentTable = getTableForItemType($this->parameters['parent_itemtype']);
            $join.= " LEFT JOIN ".$DB->quoteName($parentTable)." ON ".$DB->quoteName("itemtype")."=".$DB->quoteValue($itemtype)." AND ".$DB->quoteName("$parentTable.items_id")." = ".$DB->quoteName("$table.id");
            $where.= " AND ".$DB->quoteName("$parentTable.id")." = " . (int)$this->parameters['parent_id'];
         }
      }

      // filter by searchText parameter
      if (is_array($params['searchText'])) {
         if (array_keys($params['searchText']) == ['all']) {
            $labelfield = "name";
            if ($item instanceof CommonDevice) {
               $labelfield = "designation";
            } else if ($item instanceof Item_Devices) {
               $labelfield = "itemtype";
            }
            $search_value                      = $params['searchText']['all'];
            $params['searchText'][$labelfield] = $search_value;
            if ($DB->fieldExists($table, 'comment')) {
               $params['searchText']['comment'] = $search_value;
            }
         }

         // make text search
         foreach ($params['searchText']  as $filter_field => $filter_value) {
            if (!empty($filter_value)) {
               $search_value = Search::makeTextSearch($DB->escape($filter_value));
               $where.= " AND (".$DB->quoteName("$table.$filter_field")." $search_value)";
            }
         }
      }

      // filter with entity
      if ($item->getType() == 'Entity') {
         $where.= " AND (" . getEntitiesRestrictRequest("", $itemtype::getTable()) . ")";
      } else if ($item->isEntityAssign()
          // some CommonDBChild classes may not have entities_id fields and isEntityAssign still return true (like ITILTemplateMandatoryField)
          && array_key_exists('entities_id', $item->fields)) {
         $where.= " AND (". getEntitiesRestrictRequest("",
                                             $itemtype::getTable(),
                                             '',
                                             $_SESSION['glpiactiveentities'],
                                             $item->maybeRecursive(),
                                             true);

         if ($item instanceof SavedSearch) {
            $where.= " OR ".$itemtype::getTable().".is_private = 1";
         }

         $where.= ")";
      }

      // Check if we need to add raw names later on
      $add_keys_names = count($params['add_keys_names']) > 0;

      // build query
      $query = "SELECT DISTINCT ".$DB->quoteName("$table.id").",  ".$DB->quoteName("$table.*")."
                FROM ".$DB->quoteName($table)."
                $join
                WHERE $where
                ORDER BY ".$DB->quoteName($params['sort'])." ".$params['order']."
                LIMIT ".(int)$params['start'].", ".(int)$params['list_limit'];
      if ($result = $DB->query($query)) {
         while ($data = $DB->fetchAssoc($result)) {
            if ($add_keys_names) {
               // Insert raw names into the data row
               $data["_keys_names"] = $this->getFriendlyNames(
                  $data,
                  $params,
                  $itemtype
               );
            }

            $found[] = $data;
         }
      }

      // get result full row counts
      $count_query = "SELECT COUNT(*) FROM {$DB->quoteName($table)} $join WHERE $where";
      $totalcount = $DB->query($count_query)->fetch_row()[0];

      if ($params['range'][0] > $totalcount) {
         $this->returnError("Provided range exceed total count of data: ".$totalcount,
                            400,
                            "ERROR_RANGE_EXCEED_TOTAL");
      }

      foreach ($found as &$fields) {
         // only keep id in field list
         if ($params['only_id']) {
            $fields = ['id' => $fields['id']];
         }

         // avioid disclosure of critical fields
         $item::unsetUndisclosedFields($fields);

         // expand dropdown (retrieve name of dropdowns) and get hateoas
         $fields = self::parseDropdowns($fields, $params);

         // get hateoas from children
         if ($params['get_hateoas']) {
            $hclasses = self::getHatoasClasses($itemtype);
            foreach ($hclasses as $hclass) {
               $fields['links'][] = ['rel' => $hclass,
                                          'href' => self::$api_url."/$itemtype/".$fields['id']."/$hclass/"];
            }
         }
      }
      // Break reference
      unset($fields);

      // Map values for deprecated itemtypes
      if ($this->isDeprecated()) {
         $found = array_map(function($fields) {
            return $this->deprecated_item->mapCurrentToDeprecatedFields($fields);
         }, $found);
      }

      return array_values($found);
   }

   /**
    * Return a collection of items queried in input ($items)
    *
    * Call self::getItem for each line of $items
    *
    * @param array $params with theses options :
    *    - items:               array containing lines with itemtype and items_id keys
    *                               Ex: [
    *                                      [itemtype => 'Ticket', id => 102],
    *                                      [itemtype => 'User',   id => 10],
    *                                      [itemtype => 'User',   id => 11],
    *                                   ]
    *    - 'expand_dropdowns':  Show dropdown's names instead of id. default: false. Optionnal
    *    - 'get_hateoas':       Show relation of current item in a links attribute. default: true. Optionnal
    *    - 'get_sha1':          Get a sha1 signature instead of the full answer. default: false. Optionnal
    *    - 'with_devices':   Only for [Computer, NetworkEquipment, Peripheral, Phone, Printer], Optionnal.
    *    - 'with_disks':        Only for Computer, retrieve the associated filesystems. Optionnal.
    *    - 'with_softwares':    Only for Computer, retrieve the associated softwares installations. Optionnal.
    *    - 'with_connections':  Only for Computer, retrieve the associated direct connections (like peripherals and printers) .Optionnal.
    *    - 'with_networkports': Retrieve all network connections and advanced network informations. Optionnal.
    *    - 'with_infocoms':     Retrieve financial and administrative informations. Optionnal.
    *    - 'with_contracts':    Retrieve associated contracts. Optionnal.
    *    - 'with_documents':    Retrieve associated external documents. Optionnal.
    *    - 'with_tickets':      Retrieve associated itil tickets. Optionnal.
    *    - 'with_problems':     Retrieve associated itil problems. Optionnal.
    *    - 'with_changes':      Retrieve associated itil changes. Optionnal.
    *    - 'with_notes':        Retrieve Notes (if exists, not all itemtypes have notes). Optionnal.
    *    - 'with_logs':         Retrieve historical. Optionnal.
    *
    * @return array collection of glpi object's fields
    */
   protected function getMultipleItems($params = []) {

      if (!is_array($params['items'])) {
         return $this->messageBadArrayError();
      }

      $allitems = [];
      foreach ($params['items'] as $item) {
         if (!isset($item['items_id']) && !isset($item['itemtype'])) {
            return $this->messageBadArrayError();
         }

         $fields = $this->getItem($item['itemtype'], $item['items_id'], $params);
         $allitems[] = $fields;
      }

      return $allitems;
   }


   /**
    * List the searchoptions of provided itemtype. To use with searchItems function
    *
    * @param string $itemtype             itemtype (class) of object
    * @param array  $params               parameters
    * @param bool   $check_depreciation   disable depreciation check, useful
    *                                     if depreciation have already been
    *                                     handled by a parent call (e.g. search)
    *
    * @return array all searchoptions of specified itemtype
    */
   protected function listSearchOptions(
      $itemtype,
      $params = [],
      bool $check_depreciation = true
   ) {
      $this->initEndpoint();

      if ($check_depreciation) {
         $itemtype = $this->handleDepreciation($itemtype);
      }

      $soptions = Search::getOptions($itemtype);

      if (isset($params['raw'])) {
         return $soptions;
      }

      $cleaned_soptions = [];
      foreach ($soptions as $sID => $option) {
         if (is_int($sID)) {
            $available_searchtypes = Search::getActionsFor($itemtype, $sID);
            unset($available_searchtypes['searchopt']);
            $available_searchtypes = array_keys($available_searchtypes);

            $cleaned_soptions[$sID] = ['name'                  => $option['name'],
                                            'table'                 => $option['table'],
                                            'field'                 => $option['field'],
                                            'datatype'              => isset($option['datatype'])
                                                                       ?$option['datatype']
                                                                       :"",
                                            'nosearch'              => isset($option['nosearch'])
                                                                       ?$option['nosearch']
                                                                       :false,
                                            'nodisplay'             => isset($option['nodisplay'])
                                                                       ?$option['nodisplay']
                                                                       :false,
                                            'available_searchtypes' => $available_searchtypes];
            $cleaned_soptions[$sID]['uid'] = $this->getSearchOptionUniqID($itemtype,
                                                                               $option);
         } else {
            $cleaned_soptions[$sID] = $option;
         }
      }

      if ($check_depreciation && $this->isDeprecated()) {
         $cleaned_soptions = $this->deprecated_item->mapCurrentToDeprecatedSearchOptions($cleaned_soptions);
      }

      return $cleaned_soptions;
   }


   /**
    * Generate an unique id of a searchoption based on:
    *  - itemtype
    *  - linkfield
    *  - joinparams
    *  - field
    *
    * It permits to identify a searchoption with an named index instead a numeric one
    *
    * @param CommonDBTM $itemtype current itemtype called on ressource listSearchOption
    * @param array      $option   current option to generate an unique id
    *
    * @return string the unique id
    */
   private function getSearchOptionUniqID($itemtype, $option = []) {

      $uid_parts = [$itemtype];

      $sub_itemtype = getItemTypeForTable($option['table']);

      if ((isset($option['joinparams']['beforejoin']['table'])
           || empty($option['joinparams']))
          && $option['linkfield'] != getForeignKeyFieldForItemType($sub_itemtype)
          && $option['linkfield'] != $option['field']) {
         $uid_parts[] = $option['linkfield'];
      }

      if (isset($option['joinparams'])) {
         if (isset($option['joinparams']['beforejoin'])) {
            $sub_parts  = $this->getSearchOptionUniqIDJoins($option['joinparams']['beforejoin']);
            $uid_parts = array_merge($uid_parts, $sub_parts);
         }
      }

      if (isset($option['joinparams']['beforejoin']['table'])
          || $sub_itemtype != $itemtype) {
         $uid_parts[] = $sub_itemtype;
      }

      $uid_parts[] = $option['field'];

      $uuid = implode('.', $uid_parts);

      return $uuid;
   }


   /**
    * Generate subpart of a unique id of a search option with parsing joinparams recursively
    *
    * @param array $option ['joinparams']['beforejoin'] subpart of a searchoption
    *
    * @return array unique id parts
    */
   private function getSearchOptionUniqIDJoins($option) {

      $uid_parts = [];
      if (isset($option['joinparams']['beforejoin'])) {
         $sub_parts  = $this->getSearchOptionUniqIDJoins($option['joinparams']['beforejoin']);
         $uid_parts = array_merge($uid_parts, $sub_parts);
      }

      if (isset($option['table'])) {
         $uid_parts[] = getItemTypeForTable($option['table']);
      }

      return $uid_parts;
   }


   /**
    * Expose the GLPI searchEngine
    *
    * @param string $itemtype itemtype (class) of object
    * @param array  $params   with theses options :
    *    - 'criteria': array of criterion object to filter search.
    *        Optionnal.
    *        Each criterion object must provide :
    *           - link: (optionnal for 1st element) logical operator in [AND, OR, AND NOT, AND NOT].
    *           - field: id of searchoptions.
    *           - searchtype: type of search in [contains, equals, notequals, lessthan, morethan, under, notunder].
    *           - value : value to search.
    *    - 'metacriteria' (optionnal): array of metacriterion object to filter search.
    *                                  Optionnal.
    *                                  A meta search is a link with another itemtype
    *                                  (ex: Computer with softwares).
    *         Each metacriterion object must provide :
    *            - link: logical operator in [AND, OR, AND NOT, AND NOT]. Mandatory
    *            - itemtype: second itemtype to link.
    *            - field: id of searchoptions.
    *            - searchtype: type of search in [contains, equals, notequals, lessthan, morethan, under, notunder].
    *            - value : value to search.
    *    - 'sort' :  id of searchoption to sort by (default 1). Optionnal.
    *    - 'order' : ASC - Ascending sort / DESC Descending sort (default ASC). Optionnal.
    *    - 'range' : a string with a couple of number for start and end of pagination separated by a '-'. Ex : 150-200. (default 0-50)
    *                Optionnal.
    *    - 'forcedisplay': array of columns to display (default empty = empty use display pref and search criterias).
    *                      Some columns will be always presents (1-id, 2-name, 80-Entity).
    *                      Optionnal.
    *    - 'rawdata': boolean for displaying raws data of Search engine of glpi (like sql request, and full searchoptions)
    *
    * @return array of raw rows from Search class
    */
   protected function searchItems($itemtype, $params = []) {
      global $DEBUG_SQL;

      $this->initEndpoint();
      $itemtype = $this->handleDepreciation($itemtype);

      // check rights
      if ($itemtype != 'AllAssets'
          && !$itemtype::canView()) {
         return $this->messageRightError();
      }

      // retrieve searchoptions
      $soptions = $this->listSearchOptions($itemtype, [], false);

      if ($this->isDeprecated()) {
         $criteria = $this->deprecated_item->mapDeprecatedToCurrentCriteria(
            $params['criteria'] ?? []
         );

         if (count($criteria)) {
            $params['criteria'] = $criteria;
         }
      }

      // Check the criterias are valid
      if (isset($params['criteria']) && is_array($params['criteria'])) {

         // use a recursive closure to check each nested criteria
         $check_criteria = function(&$criteria) use (&$check_criteria, $soptions) {
            foreach ($criteria as &$criterion) {
               // recursive call
               if (isset($criterion['criteria'])) {
                  return $check_criteria($criterion['criteria']);
               }

               if (!isset($criterion['field']) || !isset($criterion['searchtype'])
                   || !isset($criterion['value'])) {
                  return __("Malformed search criteria");
               }

               if (!ctype_digit((string) $criterion['field'])
                   || !array_key_exists($criterion['field'], $soptions)) {
                  return __("Bad field ID in search criteria");
               }

               if (isset($soptions[$criterion['field']])
                   && isset($soptions[$criterion['field']]['nosearch'])
                   && $soptions[$criterion['field']]['nosearch']) {
                  return __("Forbidden field ID in search criteria");
               }

               // Escape value to prevent SQL injection
               $criterion['value'] = Toolbox::addslashes_deep($criterion['value']);
            }

            return true;
         };

         // call the closure
         $check_criteria_result = $check_criteria($params['criteria']);
         if ($check_criteria_result !== true) {
            return $this->returnError($check_criteria_result);
         }
      }

      // manage forcedisplay
      if (isset($params['forcedisplay'])) {
         if (!is_array($params['forcedisplay'])) {
            $params['forcedisplay'] = [intval($params['forcedisplay'])];
         }
         $params['forcedisplay'] = array_combine($params['forcedisplay'], $params['forcedisplay']);
      } else {
         $params['forcedisplay'] = [];
      }
      foreach ($params['forcedisplay'] as $forcedisplay) {
         if (isset($soptions[$forcedisplay]) && isset($soptions[$forcedisplay]['nodisplay'])
             && $soptions[$forcedisplay]['nodisplay']) {
            return $this->returnError(__("ID is forbidden along with 'forcedisplay' parameter."));
         }
      }

      // transform range parameter in start and limit variables
      if (isset($params['range']) > 0) {
         if (preg_match("/^[0-9]+-[0-9]+\$/", $params['range'])) {
            $range = explode("-", $params['range']);
            $params['start']      = $range[0];
            $params['list_limit'] = $range[1]-$range[0]+1;
            $params['range']      = $range;
         } else {
            $this->returnError("range must be in format : [start-end] with integers");
         }
      } else {
         $params['range'] = [0, $_SESSION['glpilist_limit']];
      }

      // force reset
      $params['reset'] = 'reset';

      // force logging sql queries
      $_SESSION['glpi_use_mode'] = Session::DEBUG_MODE;

      // call Core Search method
      $rawdata = Search::getDatas($itemtype, $params, $params['forcedisplay']);

      // probably a sql error
      if (!isset($rawdata['data']) || count($rawdata['data']) === 0) {
         $this->returnError("Unexpected SQL Error : ".array_splice($DEBUG_SQL['errors'], -2)[0],
                            500, "ERROR_SQL", false);
      }

      $cleaned_data = ['totalcount' => $rawdata['data']['totalcount'],
                            'count'      => count($rawdata['data']['rows']),
                            'sort'       => $rawdata['search']['sort'],
                            'order'      => $rawdata['search']['order']];

      if ($params['range'][0] > $cleaned_data['totalcount']) {
         $this->returnError("Provided range exceed total count of data: ".$cleaned_data['totalcount'],
                            400,
                            "ERROR_RANGE_EXCEED_TOTAL");
      }

      // fix end range
      if ($params['range'][1] > $cleaned_data['totalcount'] - 1) {
         $params['range'][1] = $cleaned_data['totalcount'] - 1;
      }

      //prepare cols (searchoptions_id) for cleaned data
      $cleaned_cols = [];
      $uid_cols = [];
      foreach ($rawdata['data']['cols'] as $col) {
         $cleaned_cols[] = $col['id'];
         if (isset($params['uid_cols'])) {
            // prepare cols with uid
            $uid_cols[] = $soptions[$col['id']]['uid'];
         }
      }

      foreach ($rawdata['data']['rows'] as $row) {
         $raw = $row['raw'];
         $id = $raw['id'];

         // keep row itemtype for all asset
         if ($itemtype == 'AllAssets') {
            $current_id       = $raw['id'];
            $current_itemtype = $raw['TYPE'];
         }

         // retrive value (and manage multiple values)
         $clean_values = [];
         foreach ($rawdata['data']['cols'] as $col) {
            $rvalues = $row[$col['itemtype'] . '_' . $col['id']];

            // manage multiple values (ex: IP adresses)
            $current_values = [];
            for ($valindex= 0; $valindex < $rvalues['count']; $valindex++) {
               $current_values[] = $rvalues[$valindex]['name'];
            }
            if (count($current_values) == 1) {
               $current_values = $current_values[0];
            }

            $clean_values[] = $current_values;
         }

         // combine cols (searchoptions_id) with values (raws data)
         if (isset($params['uid_cols'])) {
            $current_line = array_combine($uid_cols, $clean_values);
         } else {
            $current_line = array_combine($cleaned_cols, $clean_values);
         }

         // if all asset, provide type in returned data
         if ($itemtype == 'AllAssets') {
            $current_line['id']       = $current_id;
            $current_line['itemtype'] = $current_itemtype;
         }

         // append to final array
         if (isset($params['withindexes'])) {
            $cleaned_data['data'][$id] = $current_line;
         } else {
            $cleaned_data['data'][] = $current_line;
         }
      }

      // add rows with their html
      if (isset($params['giveItems'])) {
         $cleaned_data['data_html'] = [];
         foreach ($rawdata['data']['rows'] as $row) {
            $new_row = [];
            foreach ($row as $cell_key => $cell) {
               if (isset($cell['displayname'])) {
                  $new_row[$cell_key] = $cell['displayname'];
               }
            }
            $new_row = array_combine($cleaned_cols, $new_row);

            if (isset($params['withindexes'])) {
               $cleaned_data['data_html'][$row['id']] = $new_row;
            } else {
               $cleaned_data['data_html'][] = $new_row;
            }
         }
      }

      if (isset($params['rawdata'])
          && $params['rawdata']) {
         $cleaned_data['rawdata'] = $rawdata;
      }

      $cleaned_data['content-range'] = implode('-', $params['range']).
                                       "/".$cleaned_data['totalcount'];

      // return data
      return $cleaned_data;
   }


   /**
    * Add an object to GLPI
    *
    * @param string $itemtype itemtype (class) of object
    * @param array  $params   with theses options :
    *    - 'input' : object with fields of itemtype to be inserted.
    *                You can add several items in one action by passing array of input object.
    *                Mandatory.
    *
    * @return array of id
    */
   protected function createItems($itemtype, $params = []) {
      $this->initEndpoint();
      $itemtype = $this->handleDepreciation($itemtype);

      $input    = isset($params['input']) ? $params["input"] : null;
      $item     = new $itemtype;

      if (is_object($input)) {
         $input = [$input];
         $isMultiple = false;
      } else {
         $isMultiple = true;
      }

      if ($this->isDeprecated()) {
         $input = array_map(function($item) {
            return $this->deprecated_item->mapDeprecatedToCurrentFields($item);
         }, $input);
      }

      if (is_array($input)) {
         $idCollection = [];
         $failed       = 0;
         $index        = 0;
         foreach ($input as $object) {
            $object      = $this->inputObjectToArray($object);
            $current_res = [];

            //check rights
            if (!$item->can(-1, CREATE, $object)) {
               $failed++;
               $current_res = ['id'      => false,
                               'message' => __("You don't have permission to perform this action.")];
            } else {
               // add missing entity
               if (!isset($object['entities_id'])) {
                  $object['entities_id'] = $_SESSION['glpiactive_entity'];
               }

               // add an entry to match gui post (which contains submit button)
               // to force having messages after redirect
               $object["_add"] = true;

               //add current item
               $object = Toolbox::sanitize($object);
               $new_id = $item->add($object);
               if ($new_id === false) {
                  $failed++;
               }
               $current_res = ['id'      => $new_id,
                               'message' => $this->getGlpiLastMessage()];
            }

            // attach fileupload answer
            if (isset($params['upload_result'])
                && isset($params['upload_result'][$index])) {
               $current_res['upload_result'] = $params['upload_result'][$index];
            }

            // append current result to final collection
            $idCollection[] = $current_res;
            $index++;
         }

         if ($isMultiple) {
            if ($failed == count($input)) {
               $this->returnError($idCollection, 400, "ERROR_GLPI_ADD", false);
            } else if ($failed > 0) {
               $this->returnError($idCollection, 207, "ERROR_GLPI_PARTIAL_ADD", false);
            }
         } else {
            if ($failed > 0) {
               $this->returnError($idCollection[0]['message'], 400, "ERROR_GLPI_ADD", false);
            } else {
               return $idCollection[0];
            }
         }
         return $idCollection;

      } else {
         $this->messageBadArrayError();
      }
   }

   /**
    * Transform all stdobject retrieved from a json_decode into arrays
    *
    * @since 9.1
    *
    * @param  mixed $input can be an object or array
    *
    * @return array the cleaned input
    */
   private function inputObjectToArray($input) {
      if (is_object($input)) {
         $input = get_object_vars($input);
      }

      if (is_array($input)) {
         foreach ($input as &$sub_input) {
            $sub_input = self::inputObjectToArray($sub_input);
         }
      }

      return $input;
   }


   /**
    * Update an object to GLPI
    *
    * @param string $itemtype itemtype (class) of object
    * @param array  $params   with theses options :
    *    - 'input' : Array of objects with fields of itemtype to be updated.
    *                Mandatory.
    *                You must provide in each object a key named 'id' to identify item to update.
    *
    * @return   array of boolean
    */
   protected function updateItems($itemtype, $params = []) {
      $this->initEndpoint();
      $itemtype = $this->handleDepreciation($itemtype);

      $input    = isset($params['input']) ? $params["input"] : null;
      $item     = new $itemtype;

      if (is_object($input)) {
         $input = [$input];
         $isMultiple = false;
      } else {
         $isMultiple = true;
      }

      if ($this->isDeprecated()) {
         $input = array_map(function($item) {
            return $this->deprecated_item->mapDeprecatedToCurrentFields($item);
         }, $input);
      }

      if (is_array($input)) {
         $idCollection = [];
         $failed       = 0;
         $index        = 0;
         foreach ($input as $object) {
            $current_res = [];
            if (isset($object->id)) {
               if (!$item->getFromDB($object->id)) {
                  $failed++;
                  $current_res = [$object->id => false,
                                  'message'   => __("Item not found")];
                  continue;
               }

               //check rights
               if (!$item->can($object->id, UPDATE)) {
                  $failed++;
                  $current_res = [$object->id => false,
                                 'message'    => __("You don't have permission to perform this action.")];
               } else {
                  // if parent key not provided in input and present in parameter
                  // (detected from url for example), try to appent it do input
                  // This is usefull to have logs in parent (and avoid some warnings in commonDBTM)
                  if (isset($params['parent_itemtype'])
                      && isset($params['parent_id'])) {
                     $fk_parent = getForeignKeyFieldForItemType($params['parent_itemtype']);
                     if (!property_exists($input, $fk_parent)) {
                        $input->$fk_parent = $params['parent_id'];
                     }
                  }

                  //update item
                  $object = Toolbox::sanitize((array)$object);
                  $update_return = $item->update($object);
                  if ($update_return === false) {
                     $failed++;
                  }
                  $current_res = [$item->fields["id"] => $update_return,
                                  'message'           => $this->getGlpiLastMessage()];
               }

            }

            // attach fileupload answer
            if (isset($params['upload_result'])
                && isset($params['upload_result'][$index])) {
               $current_res['upload_result'] = $params['upload_result'][$index];
            }

            // append current result to final collection
            $idCollection[] = $current_res;
            $index++;
         }
         if ($isMultiple) {
            if ($failed == count($input)) {
               $this->returnError($idCollection, 400, "ERROR_GLPI_UPDATE", false);
            } else if ($failed > 0) {
               $this->returnError($idCollection, 207, "ERROR_GLPI_PARTIAL_UPDATE", false);
            }
         } else {
            if ($failed > 0) {
               $this->returnError($idCollection[0]['message'], 400, "ERROR_GLPI_UPDATE", false);
            } else {
               return $idCollection; // Return collection, even if the request affects a single item
            }
         }
         return $idCollection;

      } else {
         $this->messageBadArrayError();
      }
   }


   /**
    * Delete one or more objects in GLPI
    *
    * @param string $itemtype itemtype (class) of object
    * @param array  $params   with theses options :
    *    - 'input' : Array of objects with fields of itemtype to be updated.
    *                Mandatory.
    *                You must provide in each object a key named 'id' to identify item to delete.*
    *    - 'force_purge' : boolean, if itemtype have a trashbin, you can force purge (delete finally).
    *                      Optionnal.
    *    - 'history' : boolean, default true, false to disable saving of deletion in global history.
    *                  Optionnal.
    *
    * @return boolean|boolean[]
    */
   protected function deleteItems($itemtype, $params = []) {

      $this->initEndpoint();
      $itemtype = $this->handleDepreciation($itemtype);

      $default  = ['force_purge' => false,
                        'history'     => true];
      $params   = array_merge($default, $params);
      $input    = $params['input'];
      $item     = new $itemtype;

      if (is_object($input)) {
         $input = [$input];
         $isMultiple = false;
      } else {
         $isMultiple = true;
      }

      if ($this->isDeprecated()) {
         $input = array_map(function($item) {
            return $this->deprecated_item->mapDeprecatedToCurrentFields($item);
         }, $input);
      }

      if (is_array($input)) {
         $idCollection = [];
         $failed = 0;
         foreach ($input as $object) {
            if (isset($object->id)) {
               if (!$item->getFromDB($object->id)) {
                  $failed++;
                  $idCollection[] = [$object->id => false, 'message' => __("Item not found")];
                  continue;
               }

               // Force purge for templates / may not to be deleted / not dynamic lockable items
               // see CommonDBTM::delete()
               // TODO Needs factorization
               if ($item->isTemplate()
                  || !$item->maybeDeleted()
                  // Do not take into account deleted field if maybe dynamic but not dynamic
                  || ($item->useDeletedToLockIfDynamic()
                        && !$item->isDynamic())) {
                  $params['force_purge'] = 1;
               } else {
                  $params['force_purge'] = filter_var($params['force_purge'], FILTER_VALIDATE_BOOLEAN);
               }

               //check rights
               if (($params['force_purge']
                    && !$item->can($object->id, PURGE))
                   || (!$params['force_purge']
                       && !$item->can($object->id, DELETE))) {
                  $failed++;
                  $idCollection[] = [
                        $object->id => false,
                        'message' => __("You don't have permission to perform this action.")
                  ];
               } else {
                  //delete item
                  $delete_return = $item->delete((array) $object,
                                                 $params['force_purge'],
                                                 $params['history']);
                  if ($delete_return === false) {
                     $failed++;
                  }
                  $idCollection[] = [$object->id => $delete_return, 'message' => $this->getGlpiLastMessage()];
               }
            }
         }
         if ($isMultiple) {
            if ($failed == count($input)) {
               $this->returnError($idCollection, 400, "ERROR_GLPI_DELETE", false);
            } else if ($failed > 0) {
               $this->returnError($idCollection, 207, "ERROR_GLPI_PARTIAL_DELETE", false);
            }
         } else {
            if ($failed > 0) {
               $this->returnError($idCollection[0]['message'], 400, "ERROR_GLPI_DELETE", false);
            } else {
               return $idCollection; // Return collection, even if the request affects a single item
            }
         }

         return $idCollection;

      } else {
         $this->messageBadArrayError();
      }
   }


   protected function lostPassword($params = []) {
      global $CFG_GLPI;

      if ($CFG_GLPI['use_notifications'] == '0' || $CFG_GLPI['notifications_mailing'] == '0') {
         return $this->returnError(__("Email notifications are disabled"));
      }

      if (!isset($params['email'])) {
         return $this->returnError(__("email parameter missing"));
      }

      if (isset($_SESSION['glpiID'])) {
         return $this->returnError(__("A session is active"));
      }

      $user = new User();
      if (!isset($params['password_forget_token'])) {
         $email = Toolbox::addslashes_deep($params['email']);
         try {
            $user->forgetPassword($email);
         } catch (ForgetPasswordException $e) {
            return $this->returnError($e->getMessage());
         }
         return $this->returnResponse([
            __("An email has been sent to your email address. The email contains information for reset your password.")
         ]);
      } else {
         $password = isset($params['password']) ? $params['password'] : '';
         $input = [
            'email'                    => Toolbox::addslashes_deep($params['email']),
            'password_forget_token'    => Toolbox::addslashes_deep($params['password_forget_token']),
            'password'                 => Toolbox::addslashes_deep($password),
            'password2'                => Toolbox::addslashes_deep($password),
         ];
         try {
            $user->updateForgottenPassword($input);
            return $this->returnResponse([__("Reset password successful.")]);
         } catch (ForgetPasswordException $e) {
            return $this->returnError($e->getMessage());
         } catch (PasswordTooWeakException $e) {
            implode('\n', $e->getMessages());
            return $this->returnError(implode('\n', $e->getMessages()));
         }
      }
   }


   /**
    * Function called by each common function of the API.
    *
    * We need for each of these to :
    *  - checks app_token
    *  - log
    *  - check session token
    *  - unlock session if needed (set ip to read-only to permit concurrent calls)
    *
    * @param boolean $unlock_session do we need to unlock session (default true)
    * @param string  $endpoint       name of the current function (default '')
    *
    * @return void
    */
   private function initEndpoint($unlock_session = true, $endpoint = "") {

      if ($endpoint === "") {
         $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
         $endpoint = $backtrace[1]['function'];
      }
      $this->checkAppToken();
      $this->logEndpointUsage($endpoint);
      $this->checkSessionToken();
      if ($unlock_session) {
         $this->unlockSessionIfPossible();
      }
   }


   /**
    * Check if the app_toke in case of config ask to
    *
    * @return void
    */
   private function checkAppToken() {

      // check app token (if needed)
      if (!isset($this->parameters['app_token'])) {
         $this->parameters['app_token'] = "";
      }
      if (!$this->apiclients_id = array_search($this->parameters['app_token'], $this->app_tokens)) {
         if ($this->parameters['app_token'] != "") {
            $this->returnError(__("parameter app_token seems wrong"), 400,
                               "ERROR_WRONG_APP_TOKEN_PARAMETER");
         } else {
            $this->returnError(__("missing parameter app_token"), 400,
                               "ERROR_APP_TOKEN_PARAMETERS_MISSING");
         }
      }
   }


   /**
    * Log usage of the api into glpi historical or log files (defined by api config)
    *
    * It stores the ip and the username of the current session.
    *
    * @param string $endpoint function called by api to log (default '')
    *
    * @return void
    */
   private function logEndpointUsage($endpoint = "") {

      $username = "";
      if (isset($_SESSION['glpiname'])) {
         $username = "(".$_SESSION['glpiname'].")";
      }

      $apiclient = new APIClient;
      if ($apiclient->getFromDB($this->apiclients_id)) {
         $changes = [
            0,
            "",
            "Enpoint '$endpoint' called by ".$this->iptxt." $username"
         ];

         switch ($apiclient->fields['dolog_method']) {
            case APIClient::DOLOG_HISTORICAL:
               Log::history($this->apiclients_id, 'APIClient', $changes, 0,
                            Log::HISTORY_LOG_SIMPLE_MESSAGE);
               break;

            case APIClient::DOLOG_LOGS:
               Toolbox::logInFile("api", $changes[2]."\n");
               break;
         }
      }
   }


   /**
    * Check that the session_token is provided and match to a valid php session
    *
    * @return boolean
    */
   protected function checkSessionToken() {

      if (!isset($this->parameters['session_token'])
          || empty($this->parameters['session_token'])) {
         return $this->messageSessionTokenMissing();
      }

      $current = session_id();
      if ($this->parameters['session_token'] != $current
          && !empty($current)
          || !isset($_SESSION['glpiID'])) {
         return $this->messageSessionError();
      }
   }


   /**
    * Unlock the current session (readonly) to permit concurrent call
    *
    * @return void
    */
   private function unlockSessionIfPossible() {

      if (!$this->session_write) {
         session_write_close();
      }
   }


   /**
    * Get last message added in $_SESSION by Session::addMessageAfterRedirect
    *
    * @return array  of messages
    */
   private function getGlpiLastMessage() {
      global $DEBUG_SQL;

      $all_messages             = [];

      $messages_after_redirect  = [];

      if (isset($_SESSION["MESSAGE_AFTER_REDIRECT"])
          && count($_SESSION["MESSAGE_AFTER_REDIRECT"]) > 0) {
         $messages_after_redirect = $_SESSION["MESSAGE_AFTER_REDIRECT"];
         // Clean messages
         $_SESSION["MESSAGE_AFTER_REDIRECT"] = [];
      };

      // clean html
      foreach ($messages_after_redirect as $messages) {
         foreach ($messages as $message) {
            $all_messages[] = Html::clean($message);
         }
      }

      // get sql errors
      if (count($all_messages) <= 0
          && $DEBUG_SQL['errors'] !== null) {
         $all_messages = $DEBUG_SQL['errors'];
      }

      if (!end($all_messages)) {
         return '';
      }
      return end($all_messages);
   }


   /**
    * Show API Debug
    *
    * @return void
    */
   protected function showDebug() {
      Html::printCleanArray($this);
   }


   /**
    * Show API header
    *
    * in debug, it add body and some libs (essentialy to colorise markdown)
    * otherwise, it change only Content-Type of the page
    *
    * @param boolean $html  (default false)
    * @param string  $title (default '')
    *
    * @return void
    */
   protected function header($html = false, $title = "") {

      // Send UTF8 Headers
      $content_type = static::$content_type;
      if ($html) {
         $content_type = "text/html";
      }
      header("Content-Type: $content_type; charset=UTF-8");

       // Send extra expires header
      Html::header_nocache();

      if ($html) {
         if (empty($title)) {
            $title = $this->getTypeName();
         }

         Html::includeHeader($title);

         // Body with configured stuff
         echo "<body>";
         echo "<div id='page'>";
      }
   }


   /**
    * Display the API Documentation in Html (parsed from markdown)
    *
    * @param string $file relative path of documentation file
    *
    * @return void
    */
   public function inlineDocumentation($file) {
      $this->header(true, __("API Documentation"));
      echo Html::css("public/lib/prismjs.css");
      echo Html::script("public/lib/prismjs.js");

      echo "<div class='documentation'>";
      $documentation = file_get_contents(GLPI_ROOT.'/'.$file);
      $md = new MarkdownExtra();
      $md->code_class_prefix = "language-";
      $md->header_id_func = function($headerName) {
         $headerName = str_replace(['(', ')'], '', $headerName);
         return rawurlencode(strtolower(strtr($headerName, [' ' => '-'])));
      };
      echo $md->transform($documentation);
      echo "</div>";

      Html::nullFooter();
   }


   /**
    * Transform array of fields passed in parameter :
    * change value from  integer id to string name of foreign key
    * You can pass an array of array, this method is recursive.
    *
    * @param array   $fields to check and transform
    * @param array   $params array of option to enable, could be :
    *                                 - expand_dropdowns (default false)
    *                                 - get_hateoas      (default true)
    *
    * @return array altered $fields
    */
   protected static function parseDropdowns($fields, $params = []) {

      // default params
      $default = ['expand_dropdowns' => false,
                       'get_hateoas'      => true];
      $params = array_merge($default, $params);

      // parse fields recursively
      foreach ($fields as $key => &$value) {
         if (is_array($value)) {
            $value = self::parseDropdowns($value, $params);
         }
         if (is_integer($key)) {
            continue;
         }
         if (isForeignKeyField($key)) {
            // specific key transformations
            if ($key == "items_id" && isset($fields['itemtype'])) {
               $key = getForeignKeyFieldForItemType($fields['itemtype']);
            }
            if ($key == "auths_id"
                && isset($fields['authtype']) && $fields['authtype'] == Auth::LDAP) {
               $key = "authldaps_id";
            }
            if ($key == "default_requesttypes_id") {
               $key = "requesttypes_id";
            }
            // mainitems_id mainitemtype
            if ($key == "mainitems_id" && isset($fields['mainitemtype'])) {
               $key = getForeignKeyFieldForItemType($fields['mainitemtype']);
            }

            if (!empty($value)
                || $key == 'entities_id' && $value >= 0) {

               $tablename = getTableNameForForeignKeyField($key);
               $itemtype = getItemTypeForTable($tablename);

               // get hateoas
               if ($params['get_hateoas'] && is_integer($value)) {
                  $fields['links'][] = ['rel'  => $itemtype,
                                             'href' => self::$api_url."/$itemtype/".$value];
               }

               // expand dropdown
               if ($params['expand_dropdowns']) {
                  $value = Dropdown::getDropdownName($tablename, $value);
                  // fix value for inexistent items
                  if ($value == "&nbsp;") {
                     $value = "";
                  }
               }
            }
         }
      }
      return $fields;
   }


   /**
    * Retrieve all child class for itemtype parameter
    *
    * @param string $itemtype Item type
    *
    * @return array child classes
    */
   static function getHatoasClasses($itemtype) {
      global $CFG_GLPI;

      $hclasses = [];
      if (in_array($itemtype, $CFG_GLPI["reservation_types"])) {
         $hclasses[] = "ReservationItem";
      }
      if (in_array($itemtype, $CFG_GLPI["document_types"])) {
         $hclasses[] = "Document_Item";
      }
      if (in_array($itemtype, $CFG_GLPI["contract_types"])) {
         $hclasses[] = "Contract_Item";
      }
      if (in_array($itemtype, $CFG_GLPI["infocom_types"])) {
         $hclasses[] = "Infocom";
      }
      if (in_array($itemtype, $CFG_GLPI["ticket_types"])) {
         $hclasses[] = "Item_Ticket";
      }if (in_array($itemtype, $CFG_GLPI["project_asset_types"])) {
         $hclasses[] = "Item_Project";
      }
      if (in_array($itemtype, $CFG_GLPI["networkport_types"])) {
         $hclasses[] = "NetworkPort";
      }
      if (in_array($itemtype, $CFG_GLPI["itemdevices_types"])) {
         //$hclasses[] = "Item_Devices";
         foreach ($CFG_GLPI['device_types'] as $device_type) {
            if ((($device_type =="DeviceMemory")
                 && !in_array($itemtype, $CFG_GLPI["itemdevicememory_types"]))
                || (($device_type =="DevicePowerSupply")
                    && !in_array($itemtype, $CFG_GLPI["itemdevicepowersupply_types"]))
                || (($device_type =="DeviceNetworkCard")
                    && !in_array($itemtype, $CFG_GLPI["itemdevicenetworkcard_types"]))) {
               continue;
            }
            $hclasses[] = "Item_".$device_type;
         }
      }

      //specific case
      switch ($itemtype) {
         case 'Ticket' :
            $hclasses[] = "TicketTask";
            $hclasses[] = "TicketValidation";
            $hclasses[] = "TicketCost";
            $hclasses[] = "Problem_Ticket";
            $hclasses[] = "Change_Ticket";
            $hclasses[] = "Item_Ticket";
            $hclasses[] = "ITILSolution";
            $hclasses[] = "ITILFollowup";
            break;

         case 'Problem' :
            $hclasses[] = "ProblemTask";
            $hclasses[] = "ProblemCost";
            $hclasses[] = "Change_Problem";
            $hclasses[] = "Problem_Ticket";
            $hclasses[] = "Item_Problem";
            $hclasses[] = "ITILSolution";
            $hclasses[] = "ITILFollowup";
            break;

         case 'Change' :
            $hclasses[] = "ChangeTask";
            $hclasses[] = "ChangeCost";
            $hclasses[] = "Itil_Project";
            $hclasses[] = "Change_Problem";
            $hclasses[] = "Change_Ticket";
            $hclasses[] = "Change_Item";
            $hclasses[] = "ITILSolution";
            $hclasses[] = "ITILFollowup";
            break;

         case 'Project' :
            $hclasses[] = "ProjectTask";
            $hclasses[] = "ProjectCost";
            $hclasses[] = "Itil_Project";
            $hclasses[] = "Item_Project";
            break;
      }

      return $hclasses;
   }


   /**
    * Send 404 error to client
    *
    * @param boolean $return_error (default true)
    *
    * @return void
    */
   public function messageNotfoundError($return_error = true) {

      $this->returnError(__("Item not found"),
                         404,
                         "ERROR_ITEM_NOT_FOUND",
                         false,
                         $return_error);
   }


   /**
    * Send 400 error to client
    *
    * @param boolean $return_error (default true)
    *
    * @return void
    */
   public function messageBadArrayError($return_error = true) {

      $this->returnError(__("input parameter must be an array of objects"),
                         400,
                         "ERROR_BAD_ARRAY",
                         true,
                         $return_error);
   }


   /**
    * Send 405 error to client
    *
    * @param boolean $return_error (default true)
    *
    * @return void
    */
   public function messageLostError($return_error = true) {

      $this->returnError(__("Method Not Allowed"),
                         405,
                         "ERROR_METHOD_NOT_ALLOWED",
                         true,
                         $return_error);
   }


   /**
    * Send 401 error to client
    *
    * @param boolean $return_error (default true)
    *
    * @return void
    */
   public function messageRightError($return_error = true) {

      $this->returnError(__("You don't have permission to perform this action."),
                         401,
                         "ERROR_RIGHT_MISSING",
                         false,
                         $return_error);
   }


   /**
    * Session Token KO
    *
    * @param boolean $return_error (default true)
    *
    * @return void
    */
   public function messageSessionError($return_error = true) {
      $this->returnError(__("session_token seems invalid"),
                         401,
                         "ERROR_SESSION_TOKEN_INVALID",
                         false,
                         $return_error);
   }


   /**
    * Session Token missing
    *
    * @param boolean $return_error (default true)
    *
    * @return void
    */
   public function messageSessionTokenMissing($return_error = true) {

      $this->returnError(__("parameter session_token is missing or empty"),
                         400,
                         "ERROR_SESSION_TOKEN_MISSING",
                         true,
                         $return_error);
   }


   /**
    * Generic function to send a error message and an error code to client
    *
    * @param string  $message         message to send (human readable)(default 'Bad Request')
    * @param integer $httpcode        http code (see : https://en.wikipedia.org/wiki/List_of_HTTP_status_codes)
    *                                      (default 400)
    * @param string  $statuscode      API status (to represent more precisely the current error)
    *                                      (default ERROR)
    * @param boolean $docmessage      if true, add a link to inline document in message
    *                                      (default true)
    * @param boolean $return_response if true, the error will be send to returnResponse function
    *                                      (who may exit after sending data), otherwise,
    *                                      we will return an array with the error
    *                                      (default true)
    *
    * @return array
    */
   public function returnError($message = "Bad Request", $httpcode = 400, $statuscode = "ERROR",
                               $docmessage = true, $return_response = true) {

      if (empty($httpcode)) {
         $httpcode = 400;
      }
      if (empty($statuscode)) {
         $statuscode = "ERROR";
      }

      if ($docmessage) {
         $message .= "; ".sprintf(__("view documentation in your browser at %s"),
                                  self::$api_url."/#$statuscode");
      }
      if ($return_response) {
         return $this->returnResponse([$statuscode, $message], $httpcode);
      }
      return [$statuscode, $message];
   }


   /**
    * Get the raw HTTP request body
    *
    * @return string
    */
   protected function getHttpBody() {
      return file_get_contents('php://input');
   }

   /**
    * Get raw names
    *
    * @since 9.5
    *
    * @param array  $data           A raw from the database
    * @param array  $params         API parameters
    * @param string $self_itemtype  Itemtype the API was called on
    *
    * @return array
    */
   protected function getFriendlyNames(
      array $data,
      array $params,
      string $self_itemtype
   ) {
      $_names = [];

      foreach ($params['add_keys_names'] as $kn_fkey) {
         if ($kn_fkey == "id") {
            // Get friendlyname for current item
            $kn_itemtype = $self_itemtype;
            $kn_id = $data[$kn_itemtype::getIndexName()];
         } else {

            if (!isset($data[$kn_fkey])) {
               Toolbox::logWarning(
                  "Invalid value: \"$kn_fkey\" doesn't exist.
               ");
               continue;
            }

            // Get friendlyname for given fkey
            $kn_itemtype = getItemtypeForForeignKeyField($kn_fkey);
            $kn_id = $data[$kn_fkey];
         }

         // Check itemtype is valid
         $kn_item = getItemForItemtype($kn_itemtype);
         if (!$kn_item) {
            Toolbox::logWarning(
               "Invalid itemtype \"$kn_itemtype\" for fkey  \"$kn_fkey\""
            );
            continue;
         }

         $kn_name = $kn_item::getFriendlyNameById($kn_id);
         $_names[$kn_fkey] = $kn_name;
      }

      return $_names;
   }

   /**
    * Get the profile picture of the given user
    *
    * @since 9.5
    *
    * @param int|boolean $user_id
    */
   public function userPicture($user_id) {
      $this->initEndpoint();

      // Try to load target user
      $user = new User();
      if (!$user->getFromDB($user_id)) {
         $this->returnError("Bad request: user with id '$user_id' not found");
      }

      if (!empty($user->fields['picture'])) {
         // Send file
         $file = GLPI_PICTURE_DIR . '/' . $user->fields['picture'];
         Toolbox::sendFile($file, $user->fields['picture']);
      } else {
         // No content
         http_response_code(204);
      }
      die;
   }

   /**
    * If the given itemtype is deprecated, replace it by it's current
    * equivalent and keep a reference to the deprecation logic so we can convert
    * the API input and/or output to the exêcted format.
    *
    * @param string  $itemtype
    * @return string The corrected itemtype.
    */
   public function handleDepreciation(string $itemtype): string {
      $deprecated = Toolbox::isAPIDeprecated($itemtype);

      if ($deprecated) {
         // Keep a reference to deprecated item
         $class = "Glpi\Api\Deprecated\\$itemtype";
         $this->deprecated_item = new $class();

         // Get correct itemtype
         $itemtype = $this->deprecated_item->getType();
      }

      return $itemtype;
   }

   /**
    * Check if the current call is using a deprecated item
    *
    * @return bool
    */
   public function isDeprecated(): bool {
      return $this->deprecated_item !== null;
   }
}
