# glpi.js

    const request = require('request-promise-native');
    const { URL } = require('url');
    const _ = require('lodash');
    const debug = require('debug');
    const path = require('path');
    const fs = require('fs');

    const ServerError = require('./errors/ServerError');
    const InvalidItemTypeError = require('./errors/InvalidItemTypeError');
    const InvalidParameterError = require('./errors/InvalidParameterError');
    const MissingAuthorizationError = require('./errors/MissingAuthorizationError');
    const MissingAppTokenError = require('./errors/MissingAppTokenError');
    const MissingAPIURLError = require('./errors/MissingAPIURLError');
    const MissingHATEOASError = require('./errors/MissingHATEOASError');
    const MissingItemTypeError = require('./errors/MissingItemTypeError');
    const InvalidAPIURLError = require('./errors/InvalidAPIURLError');
    const InvalidHTTPMethodError = require('./errors/InvalidHTTPMethodError');
    const FileNotReadableError = require('./errors/FileNotReadableError');

    const log = debug('glpi-api');

    const itemTypes = require('./itemTypes.json');

    const HTTP_GET = 'get';
    const HTTP_POST = 'post';
    const HTTP_PUT = 'put';
    const HTTP_DELETE = 'delete';

    const userAgent = 'glpi-api/1.4.0';

    /** Class to manage access to GLPI via REST API */
    class Glpi {
      /**
       * Create a Glpi object
       *
       * Usage :
       *
       * ```
       * const Glpi = require('glpi-api');
       * const glpi = new Glpi({
       *   apiurl     : 'http://glpi.myserver.com/apirest.php',
       *   user_token : 'q56hqkniwot8wntb3z1qarka5atf365taaa2uyjrn',
       *   app_token  : 'f7g3csp8mgatg5ebc5elnazakw20i9fyev1qopya7',
       * });
       *
       * // or
       *
       * const glpi = new Glpi({
       *   apiurl     : 'http://glpi.myserver.com/apirest.php',
       *   app_token  : 'f7g3csp8mgatg5ebc5elnazakw20i9fyev1qopya7',
       *   auth       : {
       *     username : 'glpi',
       *     password : 'glpi',
       *   }
       * });
       * ```
       *
       * @param {Object} settings
       * @param {Object} settings.user_token Token used for user token authentication
       * @param {Object} settings.auth 2 parameters to login with user authentication
       * @param {Object} settings.auth.username username parameter used for user authentication
       * @param {Object} settings.auth.password password parameter used for user authentication
       * @param {Object} settings.app_token Authorization string provided by the GLPI api configuration
       * @param {Object} settings.apiurl URL of the apirest.php file
       */
      constructor(settings = {}) {
        if (!settings.user_token &&
           (!settings.auth || (!settings.auth.username || !settings.auth.password))) {
          throw new MissingAuthorizationError('Missing Authorization header');
        }

        if (!settings.app_token) {
          throw new MissingAppTokenError('Missing App-Token header');
        }

        if (!settings.apiurl) {
          throw new MissingAPIURLError('Missing API URL header');
        }

        let apiurl;
        try {
          apiurl = new URL(settings.apiurl);
        } catch (errApiurl) {
          throw new InvalidAPIURLError(`Invalid API URL ${settings.apiurl}`);
        }

        this._settings = {
          user_token : settings.user_token,
          auth       : this._getAuth(settings.auth),
          app_token  : settings.app_token,
          port       : settings.port,
          apiurl,
        };
        this._session = '';

        log('> SETTINGS :', this._settings);
      }

      /**
       * Add one or more custom itemTypes to the valid itemTypes list
       *
       * @param {string|array} customItemTypes String or array of string of itemType to add to the valid itemTypes list
       * @returns {undefined}
       */
      addCustomItemTypes(customItemTypes) {
        if (!_.isArray(customItemTypes)) {
          customItemTypes = [customItemTypes];
        }
        customItemTypes.forEach((customItemType) => {
          itemTypes.push(customItemType);
        });
      }

      /**
       * Return the appropriate string for authentication
       *
       * @param {Object} settings.auth 2 parameters to login with user authentication
       * @param {Object} settings.auth.username username parameter used for user authentication
       * @param {Object} settings.auth.password password parameter used for user authentication
       * @returns {object}
       */
      _getAuth(auth) {
        if (auth && auth.username) {
          const username = auth.username;
          const password = auth.password;
          const base64 = Buffer.from(`${username}:${password}`).toString('base64');
          return base64;
        }
        return auth;
      }

      /**
       * Send HTTP requests
       *
       * @param {string} method HTTP method to use for the request. Only GET, POST, PUT and DELETE are accepted,
       *                        throws an InvalidHTTPMethodError otherwise.
       * @param {string} endpoint API endpoint
       * @param {object} options
       * @param {object} options.headers  HTTP headers to add to the request
       * @param {object} options.query  URL query string to add to the request
       * @returns {object}
       */
      _request(method, endpoint, options) {
        if (![HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_DELETE].includes(method)) {
          throw new InvalidHTTPMethodError(`Invalid method: ${method}`);
        }

        log('> OPTIONS IN :', options);

        let headers = {
          'User-Agent'    : userAgent,
          'Cache-Control' : 'no-cache',
          'App-Token'     : this._settings.app_token,
        };

        if (this._session) {
          headers['Session-Token'] = this._session;
        }

        if (options && options.headers) {
          headers = { ...headers, ...options.headers };
          delete options.headers;
        }

        let req = {
          resolveWithFullResponse : true,
          json : true,
          baseUrl : this._settings.apiurl.href,
          url : endpoint,
          headers,
          method,
        };

        if (options) {
          if (options.query) {
            req.qs = options.query;
            delete options.query;
          }

          req = { ...req, ...options };
        }

        log('> REQUEST OPTIONS :', req);

        return request(req)
        .then((incomingMessage) => {
          const range = this._parseContentRange(incomingMessage.headers);
          let response = {
            code: incomingMessage.statusCode,
            data: incomingMessage.body,
            range,
          };
          return response;
        });
      }

      /**
       * Validate if itemType is accepted by GLPI
       *
       * @param {string} itemType itemType requested
       * @returns {boolean}
       */
      _validateItemType(itemType) {
        if (!itemType) {
          throw new MissingItemTypeError('Missing item type');
        }

        if (itemTypes.indexOf(itemType)===-1) {
          throw new InvalidItemTypeError('Invalid item type');
        }
        return true;
      }

      /**
       * Extract min, max and total from Content-Range header
       *
       * @param {object} headers key/value object of HTTP headers
       * @returns {object}
       */
      _parseContentRange(headers) {
        const pattern = /(\d+)-(\d+)\/(\d+)/;
        if (!headers || !headers['content-range'] || !pattern.test(headers['content-range'])) {
          return {};
        }
        const [ , min, max, total ] = pattern.exec(headers['content-range']);
        return {
          min   : parseInt(min, 10),
          max   : parseInt(max, 10),
          total : parseInt(total, 10),
        };
      }

      /**
       * Request a session token to use other api endpoints.
       * @returns {Promise}
       */
      initSession() {
        log('Calling initSession()');

        const headers = {
          'App-Token' : this._settings.app_token,
        };

        if (this._settings.user_token) {
          headers.Authorization = `user_token ${this._settings.user_token}`;
        } else {
          headers.Authorization = `Basic ${this._settings.auth}`;
        }

        return this._request(HTTP_GET, '/initSession', { headers })
        .then((response) => {
          this._session = response.data.session_token;
          return response;
        })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Destroy a session identified by a session token.
       * @returns {Promise}
       */
      killSession() {
        log('Calling killSession()');

        return this._request(HTTP_GET, '/killSession')
        .then((response) => {
          this._session = '';
          return response;
        })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Sends a notification to the user to reset his password.
       * Reset the password if `password_forget_token` and `password` are provided
       *
       * @param {string} email email address of the user to recover. Mandatory.
       * @param {string} password_forget_token reset token. Mandatory to reset password
       * @param {string} password the new password for the user. Mandatory to reset password
       * @returns {Promise}
       */
      lostPassword(email, password_forget_token, password) {
        log('Calling lostPassword()');

        const body = {
          email,
          password_forget_token,
          password,
        };

        return this._request(HTTP_PUT, '/lostPassword', { body })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Return all the profiles associated to logged user.
       * @returns {Promise}
       */
      getMyProfiles() {
        return this._request(HTTP_GET, '/getMyProfiles')
        .then((response) => ({ code : response.code, data : response.data.myprofiles }))
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Return the current active profile.
       * @returns {Promise}
       */
      getActiveProfile() {
        return this._request(HTTP_GET, '/getActiveProfile')
        .then((response) => ({ code : response.code, data : response.data.active_profile }))
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Change active profile to the profiles_id one.
       * See ${getMyProfiles} endpoint for possible profiles.
       *
       * @param {integer} profiles_id  ID of the new active profile.
       * @returns {Promise}
       */
      changeActiveProfile(profiles_id) {
        const body = { profiles_id };
        return this._request(HTTP_POST, '/changeActiveProfile', { body })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Return all the possible entities of the current logged user (and for current active profile).
       *
       * @returns {Promise}
       */
      getMyEntities() {
        return this._request(HTTP_GET, '/getMyEntities')
        .then((response) => ({ code : response.code, data : response.data.myentities }))
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Return active entities of current logged user.
       *
       * @returns {Promise}
       */
      getActiveEntities() {
        return this._request(HTTP_GET, '/getActiveEntities')
        .then((response) => ({ code : response.code, data : response.data.active_entity }))
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Change active profile to the profiles_id one.
       * See getMyProfiles endpoint for possible profiles.
       *
       * @param {integer} profiles_id  ID of the new active profile.
       * @returns {Promise}
       */
      changeActiveEntities(entities_id, is_recursive = 'false') {
        const body = { entities_id, is_recursive };
        return this._request(HTTP_POST, '/changeActiveEntities', { body })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Return the current php $_SESSION.
       *
       * @returns {Promise}
       */
      getFullSession() {
        return this._request(HTTP_GET, '/getFullSession')
        .then((response) => ({ code : response.code, data : response.data.session }))
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Return the instance fields of itemtype identified by id.
       *
       * @param {string} itemType itemtype requested
       * @param {string} id unique identifier of the itemtype. Mandatory.
       * @param {object} [opts]
       * @param {boolean} [opts.expand_dropdowns=false] show dropdown name instead of id.
       * @param {boolean} [opts.get_hateoas=true] Show relations of the item in a links attribute.
       * @param {boolean} [opts.get_sha1=false] Get a sha1 signature instead of the full answer.
       * @param {boolean} [opts.with_devices=false] Only for [Computer, NetworkEquipment, Peripheral, Phone, Printer], retrieve the associated components.
       * @param {boolean} [opts.with_disks=false] Only for Computer, retrieve the associated file-systems.
       * @param {boolean} [opts.with_softwares=false] Only for Computer, retrieve the associated software's installations.
       * @param {boolean} [opts.with_connections=false] Only for Computer, retrieve the associated direct connections (like peripherals and printers) .Optional.
       * @param {boolean} [opts.with_networkports=false] Retrieve all network's connections and advanced network's informations.
       * @param {boolean} [opts.with_infocoms=false] Retrieve financial and administrative informations.
       * @param {boolean} [opts.with_contracts=false] Retrieve associated contracts.
       * @param {boolean} [opts.with_documents=false] Retrieve associated external documents.
       * @param {boolean} [opts.with_tickets=false] Retrieve associated itil tickets.
       * @param {boolean} [opts.with_problems=false] Retrieve associated itil problems.
       * @param {boolean} [opts.with_changes=false] Retrieve associated itil changes.
       * @param {boolean} [opts.with_notes=false] Retrieve Notes.
       * @param {boolean} [opts.with_logs=false] Retrieve historical.
       * @returns {Promise}
       */
      getItem(itemType, id, opts = {}) {
        this._validateItemType(itemType);

        const options = {
          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,
        };

        const query = Object.assign({}, options, opts);
        const endpoint = `/${itemType}/${id}`;

        return this._request(HTTP_GET, endpoint, { query })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Return a collection of rows of the itemtype.
       *
       * @param {string} itemType itemtype requested
       * @param {object} [opts]
       * @param {boolean} [opts.expand_dropdowns=false] show dropdown name instead of id.
       * @param {boolean} [opts.get_hateoas=true] Show relation of item in a links attribute.
       * @param {boolean} [opts.only_id=false] keep only id keys in returned data.
       * @param {string} [opts.range=0-50] a string with a couple of number for start and end of pagination separated by a '-'. Ex: 150-200.
       * @param {string} [opts.sort=id] id of the searchoption to sort by.
       * @param {string} [opts.order=DESC] ASC - Ascending sort / DESC Descending sort.
       * @param {string} [opts.searchText] array of filters to pass on the query (with key = field and value the text to search)
       * @param {boolean} [opts.is_deleted=false] Return deleted element.
       * @returns {Promise}
       */
      getItems(itemType, opts = {}) {
        this._validateItemType(itemType);

        const options = {
          expand_dropdowns  : false,
          get_hateoas       : true,
          only_id           : false,
          range             : '0-50',
          sort              : 'id',
          order             : 'DESC',
          searchText        : '',
          is_deleted        : false,
        };

        const query = Object.assign({}, options, opts);
        const endpoint = `/${itemType}`;

        return this._request(HTTP_GET, endpoint, { query })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       *
       * @param {string|object} itemType parent itemtype provided. If Object, id must not be provided
       * @param {string} id unique identifier of the itemtype. Mandatory if itemType is a string.
       * @param {string} subItemType subItemType requested
       * @param {object} [opts]
       * @param {boolean} [opts.expand_dropdowns=false] show dropdown name instead of id. Optional.
       * @param {boolean} [opts.get_hateoas=true] Show relation of item in a links attribute. Optional.
       * @param {boolean} [opts.only_id=false] keep only id keys in returned data. Optional.
       * @param {string} [opts.range=0-50] a string with a couple of number for start and end of pagination separated by a '-'. Ex: 150-200. Optional.
       * @param {string} [opts.sort=id] id of the searchoption to sort by. Optional.
       * @param {string} [opts.order=DESC] ASC - Ascending sort / DESC Descending sort. Optional.
       * @returns {Promise}
       */
      getSubItems(itemType, id, subItemType, opts = {}) {

        const options = {
          expand_dropdowns  : false,
          get_hateoas       : true,
          only_id           : false,
          range             : '0-50',
          sort              : 'id',
          order             : 'DESC',
        };

        let endpoint;

        if (_.isPlainObject(itemType)) {
          const item = itemType;
          opts = subItemType;
          subItemType = id;

          if (!item.links || !_.isArray(item.links)) {
            throw new MissingHATEOASError('Missing HATEOAS on provided object');
          }
          this._validateItemType(subItemType);

          const link = item.links.find((e) => e.rel === subItemType);

          if (!link) {
            throw new MissingHATEOASError(`Missing link for '${subItemType}' on provided object`);
          }

          const url = new URL(link.href);

          endpoint = url.href.replace(this._settings.apiurl, '');
          if (endpoint[endpoint.length - 1] === '/') {
            endpoint = endpoint.slice(0, -1);
          }

        }
        else {
          this._validateItemType(itemType);
          this._validateItemType(subItemType);

          endpoint = `/${itemType}/${id}/${subItemType}`;
        }

        const query = Object.assign({}, options, opts);

        return this._request(HTTP_GET, endpoint, { query })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Virtually call Get an item for each line in input. So, you can have a ticket and a user in the same query.
       *
       * @param {object} [opts]
       * @param {object[]} opts.items items to retrieve. Mandatory. Each line of this array should contains two keys: `itemtype`and `items_id`
       * @param {string} opts.items[].itemtype itemtype requested
       * @param {string} opts.items[].items_id unique identifier of the itemtype
       * @param {boolean} [opts.expand_dropdowns=false] show dropdown name instead of id. Optional.
       * @param {boolean} [opts.get_hateoas=true] Show relations of the item in a links attribute. Optional.
       * @param {boolean} [opts.get_sha1=false] Get a sha1 signature instead of the full answer. Optional.
       * @param {boolean} [opts.with_devices=false] Only for [Computer, NetworkEquipment, Peripheral, Phone, Printer], retrieve the associated components. Optional.
       * @param {boolean} [opts.with_disks=false] Only for Computer, retrieve the associated file-systems. Optional.
       * @param {boolean} [opts.with_softwares=false] Only for Computer, retrieve the associated software's installations. Optional.
       * @param {boolean} [opts.with_connections=false] Only for Computer, retrieve the associated direct connections (like peripherals and printers) .Optional.
       * @param {boolean} [opts.with_networkports=false] Retrieve all network's connections and advanced network's informations. Optional.
       * @param {boolean} [opts.with_infocoms=false] Retrieve financial and administrative informations. Optional.
       * @param {boolean} [opts.with_contracts=false] Retrieve associated contracts. Optional.
       * @param {boolean} [opts.with_documents=false] Retrieve associated external documents. Optional.
       * @param {boolean} [opts.with_tickets=false] Retrieve associated itil tickets. Optional.
       * @param {boolean} [opts.with_problems=false] Retrieve associated itil problems. Optional.
       * @param {boolean} [opts.with_changes=false] Retrieve associated itil changes. Optional.
       * @param {boolean} [opts.with_notes=false] Retrieve Notes. Optional.
       * @param {boolean} [opts.with_logs=false] Retrieve historical. Optional.
       * @returns {Promise}
       */
      getMultipleItems(opts) {
        const options = {
          items             : [],
          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,
        };

        const query = Object.assign({}, options, opts);

        if(opts.items) {
          query.items = JSON.parse(JSON.stringify(opts.items));
        } else {
          throw new InvalidParameterError('Invalid parameter');
        }
        return this._request(HTTP_GET, '/getMultipleItems', { query })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * List the searchoptions of provided itemtype. To use with Search items.
       *
       * @param {string} itemType itemType requested
       * @param {boolean} [raw=false] return searchoption uncleaned if true (as provided by core)
       * @returns {Promise}
       */
      listSearchOptions(itemType, raw = false) {
        this._validateItemType(itemType);

        const query = (raw) ? { raw : true } : undefined;
        const endpoint = `/listSearchOptions/${itemType}`;

        return this._request(HTTP_GET, endpoint, { query })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Expose the GLPI searchEngine and combine criteria to retrieve a list of elements of specified itemtype.
       *
       * Note: you can use 'AllAssets' itemtype to retrieve a combination of all asset's types.
       *
       * @param {string} itemType itemType requested
       * @param {object} [opts]
       * @param {object[]} opts.criteria array of criterion objects to filter search. Optional.
       * Each criterion object must provide :
       * + _link_: (optional for 1st element) logical operator in [AND, OR, AND NOT, AND NOT].
       * + _field_: id of the searchoption.
       * + _searchtype_: type of search in [contains, equals, notequals, lessthan, morethan, under, notunder].
       * + _value_: the value to search.
       * @param {string} opts.criteria[].link (optional for 1st element) logical operator in [AND, OR, AND NOT, AND NOT].
       * @param {string} opts.criteria[].field id of the searchoption.
       * @param {string} opts.criteria[].searchtype type of search in [contains, equals, notequals, lessthan, morethan, under, notunder].
       * @param {string} opts.criteria[].value the value to search.
       * @param {object[]} opts.metacriteria array of meta-criterion objects to filter search. Optional.
       * A meta search is a link with another itemtype (ex: Computer with softwares). Each meta-criterion object must provide:
       * + _link_: logical operator in [AND, OR, AND NOT, AND NOT]. Mandatory.
       * + _itemtype_ : second itemtype to link.
       * + _field_: id of the searchoption.
       * + _searchtype_: type of search in [contains, equals, notequals, lessthan, morethan, under, notunder].
       * + _value_: the value to search.
       * @param {string} opts.metacriteria[].link logical operator in [AND, OR, AND NOT, AND NOT]. Mandatory.
       * @param {string} opts.metacriteria[].itemtype second itemtype to link.
       * @param {string} opts.metacriteria[].field id of the searchoption.
       * @param {string} opts.metacriteria[].searchtype type of search in [contains, equals, notequals, lessthan, morethan, under, notunder].
       * @param {string} opts.metacriteria[].value the value to search.
       * @param {string} [opts.sort=id] id of the searchoption to sort by. Optional.
       * @param {object} [opts.order=DESC] ASC - Ascending sort / DESC Descending sort. Optional.
       * @param {object} [opts.range=0-50] a string with a couple of number for start and end of pagination separated by a '-'. Ex: 150-200. Optional.
       * @param {array} opts.forcedisplay array of columns to display (default empty = use display preferences and searched criteria).
       * Some columns will be always presents (1: id, 2: name, 80: Entity). Optional.
       * @param {object} [opts.rawdata=false] a boolean for displaying raws data of the Search engine of glpi (like SQL request, full searchoptions, etc)
       * @param {object} [opts.withindexes=false] a boolean to retrieve rows indexed by items id. By default this option is set to false,
       * because order of json objects (which are identified by index) cannot be garrantued
       * (from http://json.org/ : An object is an unordered set of name/value pairs). So, we provide arrays to guarantying sorted rows.
       * @param {object} [opts.uid_cols=false] a boolean to identify cols by the 'uniqid' of the searchoptions instead of a numeric value (see List searchOptions and 'uid' field)
       * @param {object} [opts.giveItems=false] a boolean to retrieve the data with the html parsed from core, new data are provided in data_html key.
       * @returns {Promise}
       */
      search(itemType, opts = {}) {
        this._validateItemType(itemType);

        const options = {
          criteria     : [],
          metacriteria : [],
          sort         : 'id',
          order        : 'DESC',
          forcedisplay : [],
          rawdata      : false,
          withindexes  : false,
          uid_cols     : false,
          giveItems    : false,
        };

        const query = Object.assign({}, options, opts);
        const endpoint = `/search/${itemType}`;

        return this._request(HTTP_GET, endpoint, { query })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Add an object (or multiple objects) into GLPI.
       *
       * @param {string} itemType itemType requested
       * @param {object|object[]} input an object with fields of itemtype to be inserted.
       * You can add several items in one action by passing an array of objects. Mandatory.
       * @returns {Promise}
       */
      addItems(itemType, input = {}) {
        this._validateItemType(itemType);

        if (!input || _.isEmpty(input)) {
          throw new InvalidParameterError('Invalid parameter');
        }

        const body = { input };

        return this._request(HTTP_POST, `/${itemType}`, { body })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Update an object (or multiple objects) existing in GLPI.
       *
       * @param {string} itemType itemType requested
       * @param {string} [id] the unique identifier of the itemtype passed in URL.
       * You could skip this parameter by passing it in the input payload.
       * @param {object|object[]} input an object with fields of itemtype to be inserted.
       * You can add several items in one action by passing an array of objects. Mandatory.
       * @returns {Promise}
       */
      updateItems(itemType, id, input = {}) {
        this._validateItemType(itemType);
        if ((!input || _.isEmpty(input)) && (_.isPlainObject(id) || _.isArray(id))) {
          input = id;
          id = undefined;
        }

        if (!input || _.isEmpty(input) || (!id && !input.id && !input.length) || (id && input.length)) {
          throw new InvalidParameterError('Invalid parameter');
        }

        if (input.length) {
          const invalidArray = input.some(item => !item.id);
          if (invalidArray) {
            throw new InvalidParameterError('Invalid parameter');
          }
        }

        let endpoint = `/${itemType}`;
        if (id) {
          endpoint += `/${id}`;
        }

        const body = { input };

        return this._request(HTTP_PUT, endpoint, { body })
        .catch((err) => {
          throw new ServerError(err);
        });

      }

      /**
       * Delete an object existing in GLPI.
       *
       * @param {string} itemType itemType requested
       * @param {string} [id] the unique identifier of the itemtype passed in URL.
       * You could skip this parameter by passing it in the input payload.
       * @param {object|object[]} input Array of id who need to be deleted.
       * @param {object} [opts]
       * @param {object} [opts.force_purge=false] boolean, if the itemtype have a dustbin, you can force purge (delete finally). Optional.
       * @param {object} [opts.history=true] boolean, set to false to disable saving of deletion in global history. Optional.
       * @returns {Promise}
       */
      deleteItems(itemType, id, input = {}, opts = {}) {
        this._validateItemType(itemType);
        if (_.isPlainObject(id) || _.isArray(id)) {
          opts = input;
          input = id;
          id = undefined;
        }

        if ((!id && !input.id && !input.length) || (id && input.length)) {
          throw new InvalidParameterError('Invalid parameter');
        }

        if (input.length) {
          const invalidArray = input.some(item => !item.id);
          if (invalidArray) {
            throw new InvalidParameterError('Invalid parameter');
          }
        }

        const options = {
          force_purge : false,
          history     : true,
        };

        const query = Object.assign({}, options, opts);
        const body = { input };

        let endpoint = `/${itemType}`;
        if (id) endpoint += `/${id}`;

        return this._request(HTTP_DELETE, endpoint, { body, query })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Upload a document to GLPI
       *
       * @param {string} filePath Absolute path to the file to upload
       * @param {string} [description] Description to add to document
       * @returns {Promise}
       */
      upload(filePath, description = '') {
        try {
          fs.accessSync(filePath, fs.constants.R_OK);
        } catch (err) {
          throw new FileNotReadableError();
        }

        const file = path.parse(filePath);
        const fileName = file.name + file.ext;

        const readStream  = fs.createReadStream(filePath);
        const uploadManifest = JSON.stringify({
          input : {
            name : description,
            _filename: [fileName],
          },
        });

        const formData = {
          uploadManifest,
          'filename[0]' : {
            value : readStream,
            options : {
              filename : filePath,
              contentType : null,
            }
          },
        };

        log('> formData :', formData);

        return this._request(HTTP_POST, '/Document', { formData })
        .catch((err) => {
          throw new ServerError(err);
        });
      }

      /**
       * Download a document from GLPI
       *
       * @param {string|number} documentId unique identifier of the itemtype passed in the URL.
       * @returns {Promise}
       */
      download(documentId) {
        if (!documentId || isNaN(documentId)) {
          throw new InvalidParameterError('Invalid parameter');
        }

        const headers = {
          Accept : 'application/octet-stream',
        };

        return this._request(HTTP_GET, `/Document/${documentId}`, { headers })
        .catch((err) => {
          throw new ServerError(err);
        });
      }
    }

    module.exports = Glpi;