. * --------------------------------------------------------------------- */ /** * @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; } protected function getProducts($params = []){ global $CFG_GLPI, $DB; $this->checkAppToken(); $this->logEndpointUsage(__FUNCTION__); $arr = []; foreach ($DB->request(['FROM'=>'Produit', 'WHERE'=> ['art_code' =>$params['art_code']]]) as $id => $row) { array_push($arr,$row); } if(count($arr) > 0){ return $arr[0]; } return []; } protected function getTrackingNumber($params = []){ global $CFG_GLPI, $DB; $this->checkAppToken(); $this->logEndpointUsage(__FUNCTION__); $tracking_numbers = []; foreach ($DB->request(["SELECT" => 'STK_NUMLOT', 'FROM'=>'Stock2', 'WHERE'=> ['art_code' =>$params['art_code']]]) as $id => $row) { array_push($tracking_numbers,$row); } if(count($tracking_numbers) > 0){ return $tracking_numbers; } return []; } protected function getDepots() { global $DB; $query = "SELECT * FROM `Depot` Where immobilisation = 1"; $result = $DB->query($query); if ($result->num_rows > 0) { $rows = array(); while($row = $result->fetch_assoc()) { $rows[] = $row; } return $rows; } return null; } protected function getZones($params = []) { global $DB; $arr = []; foreach ($DB->request(['SELECT' =>'STK_ZONE','FROM'=>'Stock2', 'WHERE'=> ['art_code' =>$params['art_code']]]) as $id => $row) { array_push($arr,$row); } if(count($arr) > 0){ return $arr; } return null; } protected function saveInventaire($params = []) { global $DB; $query = "SELECT `id` FROM `Inventaire` WHERE `DEP_CODE` = '".$params['codedepot']."' and `ART_CODE` = '".$params['codearticle']."' and `STK_NUMLOT` = '".$params['numlot']."' and `Emplacement` = '".$params['emplacement']."'"; $result = $DB->query($query); $rows = array(); if ($result->num_rows > 0) { while($row = $result->fetch_assoc()) { $rows[] = $row; } } $inventaire = $rows[0]; if($params['qtcompt1'] != '' && count($rows) < 1) $query = "INSERT INTO `Inventaire`(`DEP_CODE`, `ART_CODE`, `STK_NUMLOT`, `Qt_reel`, `Qt_compt1`, `usercomp1`, `datecomp1`, `Emplacement`,`Emplacement2`) VALUES ('".$params['codedepot']."','".$params['codearticle']."','".$params['numlot']."','".$params['qtreel']."','".$params['qtcompt1']."','".$params['usercomp1']."','".$params['datecomp1']."','".$params['emplacement']."','".$params['emplacement2']."')"; else { if(count($rows) > 0 && $inventaire['id'] != "") { $query = ""; if($params['qtcompt2'] != '') $query = "UPDATE `Inventaire` SET `Qt_compt2`=`Qt_compt2`+'".$params['qtcompt2']."',`usercomp2`='".$params['usercomp2']."',`datecomp2`='".$params['datecomp2']."', Emplacement2 = '".$params['emplacement2']. "' where `id`=".$inventaire['id']; else if ($params['qtcontrole'] != '') $query = "UPDATE `Inventaire` SET `Qt_controle`=`Qt_controle`+'".$params['qtcontrole']."',`usercontrole`='".$params['usercontrole']."',`datecontrole`='".$params['datecontrole']."' where `id`=".$inventaire['id']; else if($params['qtcompt1'] != '') $query = "UPDATE `Inventaire` SET `Qt_compt1`=`Qt_compt1`+'".$params['qtcompt1']."' where `id`=".$inventaire['id']; } } // echo $query; $result = $DB->query($query); } /** * 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->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 "
"; echo "