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

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

/**
 * Search Class
 *
 * Generic class for Search Engine
**/
class Search {

   // Default number of items displayed in global search
   const GLOBAL_DISPLAY_COUNT = 10;
   // EXPORT TYPE
   const GLOBAL_SEARCH        = -1;
   const HTML_OUTPUT          = 0;
   const SYLK_OUTPUT          = 1;
   const PDF_OUTPUT_LANDSCAPE = 2;
   const CSV_OUTPUT           = 3;
   const PDF_OUTPUT_PORTRAIT  = 4;

   const LBBR = '#LBBR#';
   const LBHR = '#LBHR#';

   const SHORTSEP = '$#$';
   const LONGSEP  = '$$##$$';

   const NULLVALUE = '__NULL__';

   static $output_type = self::HTML_OUTPUT;
   static $search = [];

   /**
    * Display search engine for an type
    *
    * @param string  $itemtype Item type to manage
    *
    * @return void
   **/
   static function show($itemtype) {

      $params = self::manageParams($itemtype, $_GET);
      echo "<div class='search_page'>";
      self::showGenericSearch($itemtype, $params);
      if ($params['as_map'] == 1) {
         self::showMap($itemtype, $params);
      } else {
         self::showList($itemtype, $params);
      }
      echo "</div>";
   }


   /**
    * Display result table for search engine for an type
    *
    * @param string $itemtype Item type to manage
    * @param array  $params   Search params passed to prepareDatasForSearch function
    *
    * @return void
   **/
   static function showList($itemtype, $params) {
      self::displayData(self::getDatas($itemtype, $params));
   }

   /**
    * Display result table for search engine for an type as a map
    *
    * @param string $itemtype Item type to manage
    * @param array  $params   Search params passed to prepareDatasForSearch function
    *
    * @return void
   **/
   static function showMap($itemtype, $params) {
      global $CFG_GLPI;

      if ($itemtype == 'Location') {
         $latitude = 21;
         $longitude = 20;
      } else if ($itemtype == 'Entity') {
         $latitude = 67;
         $longitude = 68;
      } else {
         $latitude = 998;
         $longitude = 999;
      }

      $params['criteria'][] = [
         'link'         => 'AND NOT',
         'field'        => $latitude,
         'searchtype'   => 'contains',
         'value'        => 'NULL'
      ];
      $params['criteria'][] = [
         'link'         => 'AND NOT',
         'field'        => $longitude,
         'searchtype'   => 'contains',
         'value'        => 'NULL'
      ];

      $data = self::getDatas($itemtype, $params);
      self::displayData($data);

      if ($data['data']['totalcount'] > 0) {
         $target = $data['search']['target'];
         $criteria = $data['search']['criteria'];
         array_pop($criteria);
         array_pop($criteria);
         $criteria[] = [
            'link'         => 'AND',
            'field'        => ($itemtype == 'Location' || $itemtype == 'Entity') ? 1 : (($itemtype == 'Ticket') ? 83 : 3),
            'searchtype'   => 'equals',
            'value'        => 'CURLOCATION'
         ];
         $globallinkto = Toolbox::append_params(
            [
               'criteria'     => Toolbox::stripslashes_deep($criteria),
               'metacriteria' => Toolbox::stripslashes_deep($data['search']['metacriteria'])
            ],
            '&amp;'
         );
         $parameters = "as_map=0&amp;sort=".$data['search']['sort']."&amp;order=".$data['search']['order'].'&amp;'.
                        $globallinkto;

         if (strpos($target, '?') == false) {
            $fulltarget = $target."?".$parameters;
         } else {
            $fulltarget = $target."&".$parameters;
         }
         $typename = class_exists($itemtype) ? $itemtype::getTypeName($data['data']['totalcount']) :
                        ($itemtype == 'AllAssets' ? __('assets') : $itemtype);

         echo "<div class='center'><p>".__('Search results for localized items only')."</p>";
         $js = "$(function() {
               var map = initMap($('#page'), 'map', 'full');
               _loadMap(map, '$itemtype');
            });

         var _loadMap = function(map_elt, itemtype) {
            L.AwesomeMarkers.Icon.prototype.options.prefix = 'far';
            var _micon = 'circle';

            var stdMarker = L.AwesomeMarkers.icon({
               icon: _micon,
               markerColor: 'blue'
            });

            var aMarker = L.AwesomeMarkers.icon({
               icon: _micon,
               markerColor: 'cadetblue'
            });

            var bMarker = L.AwesomeMarkers.icon({
               icon: _micon,
               markerColor: 'purple'
            });

            var cMarker = L.AwesomeMarkers.icon({
               icon: _micon,
               markerColor: 'darkpurple'
            });

            var dMarker = L.AwesomeMarkers.icon({
               icon: _micon,
               markerColor: 'red'
            });

            var eMarker = L.AwesomeMarkers.icon({
               icon: _micon,
               markerColor: 'darkred'
            });


            //retrieve geojson data
            map_elt.spin(true);
            $.ajax({
               dataType: 'json',
               method: 'POST',
               url: '{$CFG_GLPI['root_doc']}/ajax/map.php',
               data: {
                  itemtype: itemtype,
                  params: ".json_encode($params)."
               }
            }).done(function(data) {
               var _points = data.points;
               var _markers = L.markerClusterGroup({
                  iconCreateFunction: function(cluster) {
                     var childCount = cluster.getChildCount();

                     var markers = cluster.getAllChildMarkers();
                     var n = 0;
                     for (var i = 0; i < markers.length; i++) {
                        n += markers[i].count;
                     }

                     var c = ' marker-cluster-';
                     if (n < 10) {
                        c += 'small';
                     } else if (n < 100) {
                        c += 'medium';
                     } else {
                        c += 'large';
                     }

                     return new L.DivIcon({ html: '<div><span>' + n + '</span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });
                  }
               });

               $.each(_points, function(index, point) {
                  var _title = '<strong>' + point.title + '</strong><br/><a href=\''+'$fulltarget'.replace(/CURLOCATION/, point.loc_id)+'\'>".sprintf(__('%1$s %2$s'), 'COUNT', $typename)."'.replace(/COUNT/, point.count)+'</a>';
                  if (point.types) {
                     $.each(point.types, function(tindex, type) {
                        _title += '<br/>".sprintf(__('%1$s %2$s'), 'COUNT', 'TYPE')."'.replace(/COUNT/, type.count).replace(/TYPE/, type.name);
                     });
                  }
                  var _icon = stdMarker;
                  if (point.count < 10) {
                     _icon = stdMarker;
                  } else if (point.count < 100) {
                     _icon = aMarker;
                  } else if (point.count < 1000) {
                     _icon = bMarker;
                  } else if (point.count < 5000) {
                     _icon = cMarker;
                  } else if (point.count < 10000) {
                     _icon = dMarker;
                  } else {
                     _icon = eMarker;
                  }
                  var _marker = L.marker([point.lat, point.lng], { icon: _icon, title: point.title });
                  _marker.count = point.count;
                  _marker.bindPopup(_title);
                  _markers.addLayer(_marker);
               });

               map_elt.addLayer(_markers);
               map_elt.fitBounds(
                  _markers.getBounds(), {
                     padding: [50, 50],
                     maxZoom: 12
                  }
               );
            }).fail(function (response) {
               var _data = response.responseJSON;
               var _message = '".__s('An error occured loading data :(')."';
               if (_data.message) {
                  _message = _data.message;
               }
               var fail_info = L.control();
               fail_info.onAdd = function (map) {
                  this._div = L.DomUtil.create('div', 'fail_info');
                  this._div.innerHTML = _message + '<br/><span id=\'reload_data\'><i class=\'fa fa-sync\'></i> ".__s('Reload')."</span>';
                  return this._div;
               };
               fail_info.addTo(map_elt);
               $('#reload_data').on('click', function() {
                  $('.fail_info').remove();
                  _loadMap(map_elt);
               });
            }).always(function() {
               //hide spinner
               map_elt.spin(false);
            });
         }

         ";
         echo Html::scriptBlock($js);
         echo "</div>";
      }
   }


   /**
    * Get data based on search parameters
    *
    * @since 0.85
    *
    * @param string $itemtype      Item type to manage
    * @param array  $params        Search params passed to prepareDatasForSearch function
    * @param array  $forcedisplay  Array of columns to display (default empty = empty use display pref and search criteria)
    *
    * @return array The data
    **/
   static function getDatas($itemtype, $params, array $forcedisplay = []) {

      $data = self::prepareDatasForSearch($itemtype, $params, $forcedisplay);
      self::constructSQL($data);
      self::constructData($data);

      return $data;
   }


   /**
    * Prepare search criteria to be used for a search
    *
    * @since 0.85
    *
    * @param string $itemtype      Item type
    * @param array  $params        Array of parameters
    *                               may include sort, order, start, list_limit, deleted, criteria, metacriteria
    * @param array  $forcedisplay  Array of columns to display (default empty = empty use display pref and search criterias)
    *
    * @return array prepare to be used for a search (include criteria and others needed information)
   **/
   static function prepareDatasForSearch($itemtype, array $params, array $forcedisplay = []) {
      global $CFG_GLPI;

      // Default values of parameters
      $p['criteria']            = [];
      $p['metacriteria']        = [];
      $p['sort']                = '1'; //
      $p['order']               = 'ASC';//
      $p['start']               = 0;//
      $p['is_deleted']          = 0;
      $p['export_all']          = 0;
      if (class_exists($itemtype)) {
         $p['target']       = $itemtype::getSearchURL();
      } else {
         $p['target']       = Toolbox::getItemTypeSearchURL($itemtype);
      }
      $p['display_type']        = self::HTML_OUTPUT;
      $p['showmassiveactions']  = true;
      $p['dont_flush']          = false;
      $p['show_pager']          = true;
      $p['show_footer']         = true;
      $p['no_sort']             = false;
      $p['list_limit']          = $_SESSION['glpilist_limit'];
      $p['massiveactionparams'] = [];

      foreach ($params as $key => $val) {
         switch ($key) {
            case 'order':
               if (in_array($val, ['ASC', 'DESC'])) {
                  $p[$key] = $val;
               }
               break;
            case 'sort':
               $p[$key] = intval($val);
               if ($p[$key] < 0) {
                  $p[$key] = 1;
               }
               break;
            case 'is_deleted':
               if ($val == 1) {
                  $p[$key] = '1';
               }
               break;
            default:
               $p[$key] = $val;
               break;
         }
      }

      // Set display type for export if define
      if (isset($p['display_type'])) {
         // Limit to 10 element
         if ($p['display_type'] == self::GLOBAL_SEARCH) {
            $p['list_limit'] = self::GLOBAL_DISPLAY_COUNT;
         }
      }

      if ($p['export_all']) {
         $p['start'] = 0;
      }

      $data             = [];
      $data['search']   = $p;
      $data['itemtype'] = $itemtype;

      // Instanciate an object to access method
      $data['item'] = null;

      if ($itemtype != 'AllAssets') {
         $data['item'] = getItemForItemtype($itemtype);
      }

      $data['display_type'] = $data['search']['display_type'];

      if (!$CFG_GLPI['allow_search_all']) {
         foreach ($p['criteria'] as $val) {
            if (isset($val['field']) && $val['field'] == 'all') {
               Html::displayRightError();
            }
         }
      }
      if (!$CFG_GLPI['allow_search_view']) {
         foreach ($p['criteria'] as $val) {
            if (isset($val['field']) && $val['field'] == 'view') {
               Html::displayRightError();
            }
         }
      }

      /// Get the items to display
      // Add searched items

      $forcetoview = false;
      if (is_array($forcedisplay) && count($forcedisplay)) {
         $forcetoview = true;
      }
      $data['search']['all_search']  = false;
      $data['search']['view_search'] = false;
      // If no research limit research to display item and compute number of item using simple request
      $data['search']['no_search']   = true;

      $data['toview'] = self::addDefaultToView($itemtype, $params);
      $data['meta_toview'] = [];
      if (!$forcetoview) {
         // Add items to display depending of personal prefs
         $displaypref = DisplayPreference::getForTypeUser($itemtype, Session::getLoginUserID());
         if (count($displaypref)) {
            foreach ($displaypref as $val) {
               array_push($data['toview'], $val);
            }
         }
      } else {
         $data['toview'] = array_merge($data['toview'], $forcedisplay);
      }

      if (count($p['criteria']) > 0) {
         // use a recursive closure to push searchoption when using nested criteria
         $parse_criteria = function($criteria) use (&$parse_criteria, &$data) {
            foreach ($criteria as $criterion) {
               // recursive call
               if (isset($criterion['criteria'])) {
                  $parse_criteria($criterion['criteria']);
               } else {
                  // normal behavior
                  if (isset($criterion['field'])
                     && !in_array($criterion['field'], $data['toview'])) {
                     if ($criterion['field'] != 'all'
                        && $criterion['field'] != 'view'
                        && (!isset($criterion['meta'])
                           || !$criterion['meta'])) {
                        array_push($data['toview'], $criterion['field']);
                     } else if ($criterion['field'] == 'all') {
                        $data['search']['all_search'] = true;
                     } else if ($criterion['field'] == 'view') {
                        $data['search']['view_search'] = true;
                     }
                  }

                  if (isset($criterion['value'])
                     && (strlen($criterion['value']) > 0)) {
                     $data['search']['no_search'] = false;
                  }
               }
            }
         };

         // call the closure
         $parse_criteria($p['criteria']);
      }

      if (count($p['metacriteria'])) {
         $data['search']['no_search'] = false;
      }

      // Add order item
      if (!in_array($p['sort'], $data['toview'])) {
         array_push($data['toview'], $p['sort']);
      }

      // Special case for Ticket : put ID in front
      if ($itemtype == 'Ticket') {
         array_unshift($data['toview'], 2);
      }

      $limitsearchopt   = self::getCleanedOptions($itemtype);
      // Clean and reorder toview
      $tmpview = [];
      foreach ($data['toview'] as $val) {
         if (isset($limitsearchopt[$val]) && !in_array($val, $tmpview)) {
            $tmpview[] = $val;
         }
      }
      $data['toview']    = $tmpview;
      $data['tocompute'] = $data['toview'];

      // Force item to display
      if ($forcetoview) {
         foreach ($data['toview'] as $val) {
            if (!in_array($val, $data['tocompute'])) {
                array_push($data['tocompute'], $val);
            }
         }
      }

      return $data;
   }


   /**
    * Construct SQL request depending of search parameters
    *
    * Add to data array a field sql containing an array of requests :
    *      search : request to get items limited to wanted ones
    *      count : to count all items based on search criterias
    *                    may be an array a request : need to add counts
    *                    maybe empty : use search one to count
    *
    * @since 0.85
    *
    * @param array $data  Array of search datas prepared to generate SQL
    *
    * @return void
   **/
   static function constructSQL(array &$data) {
      global $CFG_GLPI, $DB;

      if (!isset($data['itemtype'])) {
         return false;
      }

      $data['sql']['count']  = [];
      $data['sql']['search'] = '';

      $searchopt        = &self::getOptions($data['itemtype']);

      $blacklist_tables = [];
      $orig_table = self::getOrigTableName($data['itemtype']);
      if (isset($CFG_GLPI['union_search_type'][$data['itemtype']])) {
         $itemtable          = $CFG_GLPI['union_search_type'][$data['itemtype']];
         $blacklist_tables[] = $orig_table;
      } else {
         $itemtable = $orig_table;
      }

      // hack for AllAssets
      if (isset($CFG_GLPI['union_search_type'][$data['itemtype']])) {
         $entity_restrict = true;
      } else {
         $entity_restrict = $data['item']->isEntityAssign() && $data['item']->isField('entities_id');
      }

      // Construct the request

      //// 1 - SELECT
      // request currentuser for SQL supervision, not displayed
      $SELECT = "SELECT DISTINCT `$itemtable`.`id` AS id, '".Toolbox::addslashes_deep($_SESSION['glpiname'])."' AS currentuser,
                        ".self::addDefaultSelect($data['itemtype']);

      // Add select for all toview item
      foreach ($data['toview'] as $val) {
         $SELECT .= self::addSelect($data['itemtype'], $val);
      }

      if (isset($data['search']['as_map']) && $data['search']['as_map'] == 1 && $data['itemtype'] != 'Entity') {
         $SELECT .= ' `glpi_locations`.`id` AS loc_id, ';
      }

      //// 2 - FROM AND LEFT JOIN
      // Set reference table
      $FROM = " FROM `$itemtable`";

      // Init already linked tables array in order not to link a table several times
      $already_link_tables = [];
      // Put reference table
      array_push($already_link_tables, $itemtable);

      // Add default join
      $COMMONLEFTJOIN = self::addDefaultJoin($data['itemtype'], $itemtable, $already_link_tables);
      $FROM          .= $COMMONLEFTJOIN;

      // Add all table for toview items
      foreach ($data['tocompute'] as $val) {
         if (!in_array($searchopt[$val]["table"], $blacklist_tables)) {
            $FROM .= self::addLeftJoin($data['itemtype'], $itemtable, $already_link_tables,
                                       $searchopt[$val]["table"],
                                       $searchopt[$val]["linkfield"], 0, 0,
                                       $searchopt[$val]["joinparams"],
                                       $searchopt[$val]["field"]);
         }
      }

      // Search all case :
      if ($data['search']['all_search']) {
         foreach ($searchopt as $key => $val) {
            // Do not search on Group Name
            if (is_array($val) && isset($val['table'])) {
               if (!in_array($searchopt[$key]["table"], $blacklist_tables)) {
                  $FROM .= self::addLeftJoin($data['itemtype'], $itemtable, $already_link_tables,
                                             $searchopt[$key]["table"],
                                             $searchopt[$key]["linkfield"], 0, 0,
                                             $searchopt[$key]["joinparams"],
                                             $searchopt[$key]["field"]);
               }
            }
         }
      }

      //// 3 - WHERE

      // default string
      $COMMONWHERE = self::addDefaultWhere($data['itemtype']);
      $first       = empty($COMMONWHERE);

      // Add deleted if item have it
      if ($data['item'] && $data['item']->maybeDeleted()) {
         $LINK = " AND ";
         if ($first) {
            $LINK  = " ";
            $first = false;
         }
         $COMMONWHERE .= $LINK."`$itemtable`.`is_deleted` = ".(int)$data['search']['is_deleted']." ";
      }

      // Remove template items
      if ($data['item'] && $data['item']->maybeTemplate()) {
         $LINK = " AND ";
         if ($first) {
            $LINK  = " ";
            $first = false;
         }
         $COMMONWHERE .= $LINK."`$itemtable`.`is_template` = 0 ";
      }

      // Add Restrict to current entities
      if ($entity_restrict) {
         $LINK = " AND ";
         if ($first) {
            $LINK  = " ";
            $first = false;
         }

         if ($data['itemtype'] == 'Entity') {
            $COMMONWHERE .= getEntitiesRestrictRequest($LINK, $itemtable);

         } else if (isset($CFG_GLPI["union_search_type"][$data['itemtype']])) {
            // Will be replace below in Union/Recursivity Hack
            $COMMONWHERE .= $LINK." ENTITYRESTRICT ";
         } else {
            $COMMONWHERE .= getEntitiesRestrictRequest($LINK, $itemtable, '', '',
                                                       $data['item']->maybeRecursive() && $data['item']->isField('is_recursive'));
         }
      }
      $WHERE  = "";
      $HAVING = "";

      // Add search conditions
      // If there is search items
      if (count($data['search']['criteria'])) {
         $WHERE  = self::constructCriteriaSQL($data['search']['criteria'], $data, $searchopt);
         $HAVING = self::constructCriteriaSQL($data['search']['criteria'], $data, $searchopt, true);

         // if criteria (with meta flag) need additional join/from sql
         self::constructAdditionalSqlForMetacriteria($data['search']['criteria'], $SELECT, $FROM, $already_link_tables, $data);
      }

      //// 4 - ORDER
      $ORDER = " ORDER BY `id` ";
      foreach ($data['tocompute'] as $val) {
         if ($data['search']['sort'] == $val) {
            $ORDER = self::addOrderBy(
               $data['itemtype'],
               $data['search']['sort'],
               $data['search']['order']
            );
         }
      }

      $SELECT = rtrim(trim($SELECT), ',');

      //// 7 - Manage GROUP BY
      $GROUPBY = "";
      // Meta Search / Search All / Count tickets
      $criteria_with_meta = array_filter($data['search']['criteria'], function($criterion) {
         return isset($criterion['meta'])
                && $criterion['meta'];
      });
      if ((count($data['search']['metacriteria']))
          || count($criteria_with_meta)
          || !empty($HAVING)
          || $data['search']['all_search']) {
         $GROUPBY = " GROUP BY `$itemtable`.`id`";
      }

      if (empty($GROUPBY)) {
         foreach ($data['toview'] as $val2) {
            if (!empty($GROUPBY)) {
               break;
            }
            if (isset($searchopt[$val2]["forcegroupby"])) {
               $GROUPBY = " GROUP BY `$itemtable`.`id`";
            }
         }
      }

      $LIMIT   = "";
      $numrows = 0;
      //No search : count number of items using a simple count(ID) request and LIMIT search
      if ($data['search']['no_search']) {
         $LIMIT = " LIMIT ".(int)$data['search']['start'].", ".(int)$data['search']['list_limit'];

         // Force group by for all the type -> need to count only on table ID
         if (!isset($searchopt[1]['forcegroupby'])) {
            $count = "count(*)";
         } else {
            $count = "count(DISTINCT `$itemtable`.`id`)";
         }
         // request currentuser for SQL supervision, not displayed
         $query_num = "SELECT $count,
                              '".Toolbox::addslashes_deep($_SESSION['glpiname'])."' AS currentuser
                       FROM `$itemtable`".
                       $COMMONLEFTJOIN;

         $first     = true;

         if (!empty($COMMONWHERE)) {
            $LINK = " AND ";
            if ($first) {
               $LINK  = " WHERE ";
               $first = false;
            }
            $query_num .= $LINK.$COMMONWHERE;
         }
         // Union Search :
         if (isset($CFG_GLPI["union_search_type"][$data['itemtype']])) {
            $tmpquery = $query_num;

            foreach ($CFG_GLPI[$CFG_GLPI["union_search_type"][$data['itemtype']]] as $ctype) {
               $ctable = $ctype::getTable();
               if (($citem = getItemForItemtype($ctype))
                   && $citem->canView()) {
                  // State case
                  if ($data['itemtype'] == 'AllAssets') {
                     $query_num  = str_replace($CFG_GLPI["union_search_type"][$data['itemtype']],
                                               $ctable, $tmpquery);
                     $query_num  = str_replace($data['itemtype'], $ctype, $query_num);
                     $query_num .= " AND `$ctable`.`id` IS NOT NULL ";

                     // Add deleted if item have it
                     if ($citem && $citem->maybeDeleted()) {
                        $query_num .= " AND `$ctable`.`is_deleted` = 0 ";
                     }

                     // Remove template items
                     if ($citem && $citem->maybeTemplate()) {
                        $query_num .= " AND `$ctable`.`is_template` = 0 ";
                     }

                  } else {// Ref table case
                     $reftable = $data['itemtype']::getTable();
                     if ($data['item'] && $data['item']->maybeDeleted()) {
                        $tmpquery = str_replace("`".$CFG_GLPI["union_search_type"][$data['itemtype']]."`.
                                                   `is_deleted`",
                                                "`$reftable`.`is_deleted`", $tmpquery);
                     }
                     $replace  = "FROM `$reftable`
                                  INNER JOIN `$ctable`
                                       ON (`$reftable`.`items_id` =`$ctable`.`id`
                                           AND `$reftable`.`itemtype` = '$ctype')";

                     $query_num = str_replace("FROM `".
                                                $CFG_GLPI["union_search_type"][$data['itemtype']]."`",
                                              $replace, $tmpquery);
                     $query_num = str_replace($CFG_GLPI["union_search_type"][$data['itemtype']],
                                              $ctable, $query_num);

                  }
                  $query_num = str_replace("ENTITYRESTRICT",
                                           getEntitiesRestrictRequest('', $ctable, '', '',
                                                                      $citem->maybeRecursive()),
                                           $query_num);
                  $data['sql']['count'][] = $query_num;
               }
            }

         } else {
            $data['sql']['count'][] = $query_num;
         }
      }

      // If export_all reset LIMIT condition
      if ($data['search']['export_all']) {
         $LIMIT = "";
      }

      if (!empty($WHERE) || !empty($COMMONWHERE)) {
         if (!empty($COMMONWHERE)) {
            $WHERE = ' WHERE '.$COMMONWHERE.(!empty($WHERE)?' AND ( '.$WHERE.' )':'');
         } else {
            $WHERE = ' WHERE '.$WHERE.' ';
         }
         $first = false;
      }

      if (!empty($HAVING)) {
         $HAVING = ' HAVING '.$HAVING;
      }

      // Create QUERY
      if (isset($CFG_GLPI["union_search_type"][$data['itemtype']])) {
         $first = true;
         $QUERY = "";
         foreach ($CFG_GLPI[$CFG_GLPI["union_search_type"][$data['itemtype']]] as $ctype) {
            $ctable = $ctype::getTable();
            if (($citem = getItemForItemtype($ctype))
                && $citem->canView()) {
               if ($first) {
                  $first = false;
               } else {
                  $QUERY .= " UNION ";
               }
               $tmpquery = "";
               // AllAssets case
               if ($data['itemtype'] == 'AllAssets') {
                  $tmpquery = $SELECT.", '$ctype' AS TYPE ".
                              $FROM.
                              $WHERE;

                  $tmpquery .= " AND `$ctable`.`id` IS NOT NULL ";

                  // Add deleted if item have it
                  if ($citem && $citem->maybeDeleted()) {
                     $tmpquery .= " AND `$ctable`.`is_deleted` = 0 ";
                  }

                  // Remove template items
                  if ($citem && $citem->maybeTemplate()) {
                     $tmpquery .= " AND `$ctable`.`is_template` = 0 ";
                  }

                  $tmpquery.= $GROUPBY.
                              $HAVING;

                  // Replace 'asset_types' by itemtype table name
                  $tmpquery = str_replace(
                     $CFG_GLPI["union_search_type"][$data['itemtype']],
                     $ctable,
                     $tmpquery
                  );
                  // Replace 'AllAssets' by itemtype
                  // Use quoted value to prevent replacement of AllAssets in column identifiers
                  $tmpquery = str_replace(
                     $DB->quoteValue('AllAssets'),
                     $DB->quoteValue($ctype),
                     $tmpquery
                  );
               } else {// Ref table case
                  $reftable = $data['itemtype']::getTable();

                  $tmpquery = $SELECT.", '$ctype' AS TYPE,
                                      `$reftable`.`id` AS refID, "."
                                      `$ctable`.`entities_id` AS ENTITY ".
                              $FROM.
                              $WHERE;
                  if ($data['item']->maybeDeleted()) {
                     $tmpquery = str_replace("`".$CFG_GLPI["union_search_type"][$data['itemtype']]."`.
                                                `is_deleted`",
                                             "`$reftable`.`is_deleted`", $tmpquery);
                  }

                  $replace = "FROM `$reftable`"."
                              INNER JOIN `$ctable`"."
                                 ON (`$reftable`.`items_id`=`$ctable`.`id`"."
                                     AND `$reftable`.`itemtype` = '$ctype')";
                  $tmpquery = str_replace("FROM `".
                                             $CFG_GLPI["union_search_type"][$data['itemtype']]."`",
                                          $replace, $tmpquery);
                  $tmpquery = str_replace($CFG_GLPI["union_search_type"][$data['itemtype']],
                                          $ctable, $tmpquery);
                  $name_field = $ctype::getNameField();
                  $tmpquery = str_replace("`$ctable`.`name`", "`$ctable`.`$name_field`", $tmpquery);
               }
               $tmpquery = str_replace("ENTITYRESTRICT",
                                       getEntitiesRestrictRequest('', $ctable, '', '',
                                                                  $citem->maybeRecursive()),
                                       $tmpquery);

               // SOFTWARE HACK
               if ($ctype == 'Software') {
                  $tmpquery = str_replace("`glpi_softwares`.`serial`", "''", $tmpquery);
                  $tmpquery = str_replace("`glpi_softwares`.`otherserial`", "''", $tmpquery);
               }
               $QUERY .= $tmpquery;
            }
         }
         if (empty($QUERY)) {
            echo self::showError($data['display_type']);
            return;
         }
         $QUERY .= str_replace($CFG_GLPI["union_search_type"][$data['itemtype']].".", "", $ORDER) .
                   $LIMIT;
      } else {
         $QUERY = $SELECT.
                  $FROM.
                  $WHERE.
                  $GROUPBY.
                  $HAVING.
                  $ORDER.
                  $LIMIT;
      }
      $data['sql']['search'] = $QUERY;
   }

   /**
    * Construct WHERE (or HAVING) part of the sql based on passed criteria
    *
    * @since 9.4
    *
    * @param  array   $criteria  list of search criterion, we should have these keys:
    *                               - link (optionnal): AND, OR, NOT AND, NOT OR
    *                               - field: id of the searchoption
    *                               - searchtype: how to match value (contains, equals, etc)
    *                               - value
    * @param  array   $data      common array used by search engine,
    *                            contains all the search part (sql, criteria, params, itemtype etc)
    *                            TODO: should be a property of the class
    * @param  array   $searchopt Search options for the current itemtype
    * @param  boolean $is_having Do we construct sql WHERE or HAVING part
    *
    * @return string             the sql sub string
    */
   static function constructCriteriaSQL($criteria = [], $data = [], $searchopt = [], $is_having = false) {
      $sql = "";

      foreach ($criteria as $criterion) {
         if (!isset($criterion['criteria'])
             && (!isset($criterion['value'])
                 || strlen($criterion['value']) <= 0)) {
            continue;
         }

         $itemtype = $data['itemtype'];
         $meta = false;
         if (isset($criterion['meta'])
             && $criterion['meta']
             && isset($criterion['itemtype'])) {
            $itemtype = $criterion['itemtype'];
            $meta = true;
            $searchopt = &self::getOptions($itemtype);
         }

         // common search
         if (!isset($criterion['field'])
             || ($criterion['field'] != "all"
                 && $criterion['field'] != "view")) {
            $LINK    = " ";
            $NOT     = 0;
            $tmplink = "";

            if (isset($criterion['link'])
                  && in_array($criterion['link'], array_keys(self::getLogicalOperators()))) {
               if (strstr($criterion['link'], "NOT")) {
                  $tmplink = " ".str_replace(" NOT", "", $criterion['link']);
                  $NOT     = 1;
               } else {
                  $tmplink = " ".$criterion['link'];
               }
            } else {
               $tmplink = " AND ";
            }

            // Manage Link if not first item
            if (!empty($sql)) {
               $LINK = $tmplink;
            }

            if (isset($criterion['criteria']) && count($criterion['criteria'])) {
               $sub_sql = self::constructCriteriaSQL($criterion['criteria'], $data, $searchopt, $is_having);
               if (strlen($sub_sql)) {
                  if ($NOT) {
                     $sql .= "$LINK NOT($sub_sql)";
                  } else {
                     $sql .= "$LINK ($sub_sql)";
                  }
               }
            } else if (isset($searchopt[$criterion['field']]["usehaving"])
                       || ($meta && "AND NOT" === $criterion['link'])) {
               if (!$is_having) {
                  // the having part will be managed in a second pass
                  continue;
               }

               $new_having = self::addHaving($LINK, $NOT, $itemtype,
                                             $criterion['field'], $criterion['searchtype'],
                                             $criterion['value']);
               if ($new_having !== false) {
                  $sql .= $new_having;
               }
            } else {
               if ($is_having) {
                  // the having part has been already managed in the first pass
                  continue;
               }

               $new_where = self::addWhere($LINK, $NOT, $itemtype, $criterion['field'],
                                           $criterion['searchtype'], $criterion['value'], $meta);
               if ($new_where !== false) {
                  $sql .= $new_where;
               }
            }
         } else if (isset($criterion['value'])
                    && strlen($criterion['value']) > 0) { // view and all search
            $LINK       = " OR ";
            $NOT        = 0;
            $globallink = " AND ";
            if (isset($criterion['link'])) {
               switch ($criterion['link']) {
                  case "AND" :
                     $LINK       = " OR ";
                     $globallink = " AND ";
                     break;
                  case "AND NOT" :
                     $LINK       = " AND ";
                     $NOT        = 1;
                     $globallink = " AND ";
                     break;
                  case "OR" :
                     $LINK       = " OR ";
                     $globallink = " OR ";
                     break;
                  case "OR NOT" :
                     $LINK       = " AND ";
                     $NOT        = 1;
                     $globallink = " OR ";
                     break;
               }
            } else {
               $tmplink =" AND ";
            }
            // Manage Link if not first item
            if (!empty($sql)) {
               $sql .= $globallink;
            }
            $first2 = true;
            $items = [];
            if (isset($criterion['field']) && $criterion['field'] == "all") {
               $items = $searchopt;
            } else { // toview case : populate toview
               foreach ($data['toview'] as $key2 => $val2) {
                  $items[$val2] = $searchopt[$val2];
               }
            }
            $view_sql = "";
            foreach ($items as $key2 => $val2) {
               if (isset($val2['nosearch']) && $val2['nosearch']) {
                  continue;
               }
               if (is_array($val2)) {
                  // Add Where clause if not to be done in HAVING CLAUSE
                  if (!$is_having && !isset($val2["usehaving"])) {
                     $tmplink = $LINK;
                     if ($first2) {
                        $tmplink = " ";
                     }

                     $new_where = self::addWhere($tmplink, $NOT, $itemtype, $key2,
                                                 $criterion['searchtype'], $criterion['value'], $meta);
                     if ($new_where !== false) {
                        $first2  = false;
                        $view_sql .=  $new_where;
                     }
                  }
               }
            }
            if (strlen($view_sql)) {
               $sql.= " ($view_sql) ";
            }
         }
      }
      return $sql;
   }

   /**
    * Construct aditionnal SQL (select, joins, etc) for meta-criteria
    *
    * @since 9.4
    *
    * @param  array  $criteria             list of search criterion
    * @param  string &$SELECT              TODO: should be a class property (output parameter)
    * @param  string &$FROM                TODO: should be a class property (output parameter)
    * @param  array  &$already_link_tables TODO: should be a class property (output parameter)
    * @param  array  &$data                TODO: should be a class property (output parameter)
    *
    * @return void
    */
   static function constructAdditionalSqlForMetacriteria($criteria = [],
                                                         &$SELECT = "",
                                                         &$FROM = "",
                                                         &$already_link_tables = [],
                                                         &$data = []) {
      $data['meta_toview'] = [];
      foreach ($criteria as $criterion) {
         // manage sub criteria
         if (isset($criterion['criteria'])) {
            self::constructAdditionalSqlForMetacriteria(
               $criterion['criteria'],
               $SELECT,
               $FROM,
               $already_link_tables,
               $data
            );
            continue;
         }

         // parse only criterion with meta flag
         if (!isset($criterion['itemtype'])
             || empty($criterion['itemtype'])
             || !isset($criterion['meta'])
             || !$criterion['meta']
             || !isset($criterion['value'])
             || strlen($criterion['value']) <= 0) {
            continue;
         }

         $m_itemtype = $criterion['itemtype'];
         $metaopt = &self::getOptions($m_itemtype);
         $sopt    = $metaopt[$criterion['field']];

         //add toview for meta criterion
         $data['meta_toview'][$m_itemtype][] = $criterion['field'];

         $SELECT .= self::addSelect(
            $m_itemtype,
            $criterion['field'],
            true, // meta-criterion
            $m_itemtype
         );

         $FROM .= self::addMetaLeftJoin($data['itemtype'], $m_itemtype,
                                        $already_link_tables,
                                        $sopt["joinparams"]);

         $FROM .= self::addLeftJoin($m_itemtype,
                                    $m_itemtype::getTable(),
                                    $already_link_tables, $sopt["table"],
                                    $sopt["linkfield"], 1, $m_itemtype,
                                    $sopt["joinparams"], $sopt["field"]);
      }
   }


   /**
    * Retrieve datas from DB : construct data array containing columns definitions and rows datas
    *
    * add to data array a field data containing :
    *      cols : columns definition
    *      rows : rows data
    *
    * @since 0.85
    *
    * @param array   $data      array of search data prepared to get data
    * @param boolean $onlycount If we just want to count results
    *
    * @return void
   **/
   static function constructData(array &$data, $onlycount = false) {
      if (!isset($data['sql']) || !isset($data['sql']['search'])) {
         return false;
      }
      $data['data'] = [];

      // Use a ReadOnly connection if available and configured to be used
      $DBread = DBConnection::getReadConnection();
      $DBread->query("SET SESSION group_concat_max_len = 16384;");

      // directly increase group_concat_max_len to avoid double query
      if (count($data['search']['metacriteria'])) {
         foreach ($data['search']['metacriteria'] as $metacriterion) {
            if ($metacriterion['link'] == 'AND NOT'
                || $metacriterion['link'] == 'OR NOT') {
               $DBread->query("SET SESSION group_concat_max_len = 4194304;");
               break;
            }
         }
      }

      $DBread->execution_time = true;
      $result = $DBread->query($data['sql']['search']);
      /// Check group concat limit : if warning : increase limit
      if ($result2 = $DBread->query('SHOW WARNINGS')) {
         if ($DBread->numrows($result2) > 0) {
            $res = $DBread->fetchAssoc($result2);
            if ($res['Code'] == 1260) {
               $DBread->query("SET SESSION group_concat_max_len = 8194304;");
               $DBread->execution_time = true;
               $result = $DBread->query($data['sql']['search']);
            }

            if ($res['Code'] == 1116) { // too many tables
               echo self::showError($data['search']['display_type'],
                                    __("'All' criterion is not usable with this object list, ".
                                       "sql query fails (too many tables). ".
                                       "Please use 'Items seen' criterion instead"));
               return false;
            }
         }
      }

      if ($result) {
         $data['data']['execution_time'] = $DBread->execution_time;
         if (isset($data['search']['savedsearches_id'])) {
            SavedSearch::updateExecutionTime(
               (int)$data['search']['savedsearches_id'],
               $DBread->execution_time
            );
         }

         $data['data']['totalcount'] = 0;
         // if real search or complete export : get numrows from request
         if (!$data['search']['no_search']
             || $data['search']['export_all']) {
            $data['data']['totalcount'] = $DBread->numrows($result);
         } else {
            if (!isset($data['sql']['count'])
               || (count($data['sql']['count']) == 0)) {
               $data['data']['totalcount'] = $DBread->numrows($result);
            } else {
               foreach ($data['sql']['count'] as $sqlcount) {
                  $result_num = $DBread->query($sqlcount);
                  $data['data']['totalcount'] += $DBread->result($result_num, 0, 0);
               }
            }
         }

         if ($onlycount) {
            //we just want to coutn results; no need to continue process
            return;
         }

         // Search case
         $data['data']['begin'] = $data['search']['start'];
         $data['data']['end']   = min($data['data']['totalcount'],
                                      $data['search']['start']+$data['search']['list_limit'])-1;
         //map case
         if (isset($data['search']['as_map'])  && $data['search']['as_map'] == 1) {
            $data['data']['end'] = $data['data']['totalcount']-1;
         }

         // No search Case
         if ($data['search']['no_search']) {
            $data['data']['begin'] = 0;
            $data['data']['end']   = min($data['data']['totalcount']-$data['search']['start'],
                                         $data['search']['list_limit'])-1;
         }
         // Export All case
         if ($data['search']['export_all']) {
            $data['data']['begin'] = 0;
            $data['data']['end']   = $data['data']['totalcount']-1;
         }

         // Get columns
         $data['data']['cols'] = [];

         $searchopt = &self::getOptions($data['itemtype']);

         foreach ($data['toview'] as $opt_id) {
            $data['data']['cols'][] = [
               'itemtype'  => $data['itemtype'],
               'id'        => $opt_id,
               'name'      => $searchopt[$opt_id]["name"],
               'meta'      => 0,
               'searchopt' => $searchopt[$opt_id],
            ];
         }

         // manage toview column for criteria with meta flag
         foreach ($data['meta_toview'] as $m_itemtype => $toview) {
            $searchopt = &self::getOptions($m_itemtype);
            foreach ($toview as $opt_id) {
               $data['data']['cols'][] = [
                  'itemtype'  => $m_itemtype,
                  'id'        => $opt_id,
                  'name'      => $searchopt[$opt_id]["name"],
                  'meta'      => 1,
                  'searchopt' => $searchopt[$opt_id],
               ];
            }
         }

         // Display columns Headers for meta items
         $already_printed = [];

         if (count($data['search']['metacriteria'])) {
            foreach ($data['search']['metacriteria'] as $metacriteria) {
               if (isset($metacriteria['itemtype']) && !empty($metacriteria['itemtype'])
                     && isset($metacriteria['value']) && (strlen($metacriteria['value']) > 0)) {

                  if (!isset($already_printed[$metacriteria['itemtype'].$metacriteria['field']])) {
                     $searchopt = &self::getOptions($metacriteria['itemtype']);

                     $data['data']['cols'][] = [
                        'itemtype'  => $metacriteria['itemtype'],
                        'id'        => $metacriteria['field'],
                        'name'      => $searchopt[$metacriteria['field']]["name"],
                        'meta'      => 1,
                        'searchopt' =>$searchopt[$metacriteria['field']]
                     ];

                     $already_printed[$metacriteria['itemtype'].$metacriteria['field']] = 1;
                  }
               }
            }
         }

         // search group (corresponding of dropdown optgroup) of current col
         foreach ($data['data']['cols'] as $num => $col) {
            // search current col in searchoptions ()
            while (key($searchopt) !== null
                   && key($searchopt) != $col['id']) {
               next($searchopt);
            }
            if (key($searchopt) !== null) {
               //search optgroup (non array option)
               while (key($searchopt) !== null
                      && is_numeric(key($searchopt))
                      && is_array(current($searchopt))) {
                  prev($searchopt);
               }
               if (key($searchopt) !== null
                   && key($searchopt) !== "common") {
                  $data['data']['cols'][$num]['groupname'] = current($searchopt);
               }

            }
            //reset
            reset($searchopt);
         }

         // Get rows

         // if real search seek to begin of items to display (because of complete search)
         if (!$data['search']['no_search']) {
            $DBread->dataSeek($result, $data['search']['start']);
         }

         $i = $data['data']['begin'];
         $data['data']['warning']
            = "For compatibility keep raw data  (ITEM_X, META_X) at the top for the moment. Will be drop in next version";

         $data['data']['rows']  = [];
         $data['data']['items'] = [];

         self::$output_type = $data['display_type'];

         while (($i < $data['data']['totalcount']) && ($i <= $data['data']['end'])) {
            $row = $DBread->fetchAssoc($result);
            $newrow        = [];
            $newrow['raw'] = $row;

            // Parse datas
            foreach ($newrow['raw'] as $key => $val) {
               if (preg_match('/ITEM(_(\w[^\d]+))?_(\d+)(_(.+))?/', $key, $matches)) {
                  $j = $matches[3];
                  if (isset($matches[2]) && !empty($matches[2])) {
                     $j = $matches[2] . '_' . $matches[3];
                  }
                  $fieldname = 'name';
                  if (isset($matches[5])) {
                     $fieldname = $matches[5];
                  }

                  // No Group_concat case
                  if ($fieldname == 'content' || strpos($val, self::LONGSEP) === false) {
                     $newrow[$j]['count'] = 1;

                     $handled = false;
                     if ($fieldname != 'content' && strpos($val, self::SHORTSEP) !== false) {
                        $split2                    = self::explodeWithID(self::SHORTSEP, $val);
                        if (is_numeric($split2[1])) {
                           $newrow[$j][0][$fieldname] = $split2[0];
                           $newrow[$j][0]['id']       = $split2[1];
                           $handled = true;
                        }
                     }

                     if (!$handled) {
                        if ($val === self::NULLVALUE) {
                           $newrow[$j][0][$fieldname] = null;
                        } else {
                           $newrow[$j][0][$fieldname] = $val;
                        }
                     }
                  } else {
                     if (!isset($newrow[$j])) {
                        $newrow[$j] = [];
                     }
                     $split               = explode(self::LONGSEP, $val);
                     $newrow[$j]['count'] = count($split);
                     foreach ($split as $key2 => $val2) {
                        $handled = false;
                        if (strpos($val2, self::SHORTSEP) !== false) {
                           $split2                  = self::explodeWithID(self::SHORTSEP, $val2);
                           if (is_numeric($split2[1])) {
                              $newrow[$j][$key2]['id'] = $split2[1];
                              if ($split2[0] == self::NULLVALUE) {
                                 $newrow[$j][$key2][$fieldname] = null;
                              } else {
                                 $newrow[$j][$key2][$fieldname] = $split2[0];
                              }
                              $handled = true;
                           }
                        }

                        if (!$handled) {
                           $newrow[$j][$key2][$fieldname] = $val2;
                        }
                     }
                  }
               } else {
                  if ($key == 'currentuser') {
                     if (!isset($data['data']['currentuser'])) {
                        $data['data']['currentuser'] = $val;
                     }
                  } else {
                     $newrow[$key] = $val;
                     // Add id to items list
                     if ($key == 'id') {
                        $data['data']['items'][$val] = $i;
                     }
                  }
               }
            }
            foreach ($data['data']['cols'] as $val) {
               $newrow[$val['itemtype'] . '_' . $val['id']]['displayname'] = self::giveItem(
                  $val['itemtype'],
                  $val['id'],
                  $newrow
               );
            }

            $data['data']['rows'][$i] = $newrow;
            $i++;
         }

         $data['data']['count'] = count($data['data']['rows']);
      } else {
         echo $DBread->error();
      }
   }


   /**
    * Display datas extracted from DB
    *
    * @param array $data Array of search datas prepared to get datas
    *
    * @return void
   **/
   static function displayData(array $data) {
      global $CFG_GLPI;

      $item = null;
      if (class_exists($data['itemtype'])) {
         $item = new $data['itemtype']();
      }

      if (!isset($data['data']) || !isset($data['data']['totalcount'])) {
         return false;
      }
      // Contruct Pager parameters
      $globallinkto
         = Toolbox::append_params(['criteria'
                                          => Toolbox::stripslashes_deep($data['search']['criteria']),
                                        'metacriteria'
                                          => Toolbox::stripslashes_deep($data['search']['metacriteria'])],
                                  '&amp;');
      $parameters = "sort=".$data['search']['sort']."&amp;order=".$data['search']['order'].'&amp;'.
                     $globallinkto;

      if (isset($_GET['_in_modal'])) {
         $parameters .= "&amp;_in_modal=1";
      }

      // Global search header
      if ($data['display_type'] == self::GLOBAL_SEARCH) {
         if ($data['item']) {
            echo "<div class='center'><h2>".$data['item']->getTypeName();
            // More items
            if ($data['data']['totalcount'] > ($data['search']['start'] + self::GLOBAL_DISPLAY_COUNT)) {
               echo " <a href='".$data['search']['target']."?$parameters'>".__('All')."</a>";
            }
            echo "</h2></div>\n";
         } else {
            return false;
         }
      }

      // If the begin of the view is before the number of items
      if ($data['data']['count'] > 0) {
         // Display pager only for HTML
         if ($data['display_type'] == self::HTML_OUTPUT) {
            // For plugin add new parameter if available
            if ($plug = isPluginItemType($data['itemtype'])) {
               $out = Plugin::doOneHook($plug['plugin'], 'addParamFordynamicReport', $data['itemtype']);
               if (is_array($out) && count($out)) {
                  $parameters .= Toolbox::append_params($out, '&amp;');
               }
            }
            $search_config_top    = "";
            $search_config_bottom = "";
            if (!isset($_GET['_in_modal'])) {

               $search_config_top = $search_config_bottom
                  = "<div class='pager_controls'>";

               $map_link = '';
               if (null == $item || $item->maybeLocated()) {
                  $map_link = "<input type='checkbox' name='as_map' id='as_map' value='1'";
                  if ($data['search']['as_map'] == 1) {
                     $map_link .= " checked='checked'";
                  }
                  $map_link .= "/>";
                  $map_link .= "<label for='as_map'><span title='".__s('Show as map')."' class='pointer fa fa-globe-americas'
                     onClick=\"toogle('as_map','','','');
                                 document.forms['searchform".$data["itemtype"]."'].submit();\"></span></label>";
               }
               $search_config_top .= $map_link;

               if (Session::haveRightsOr('search_config', [
                  DisplayPreference::PERSONAL,
                  DisplayPreference::GENERAL
               ])) {
                  $options_link = "<span class='fa fa-wrench pointer' title='".
                     __s('Select default items to show')."' onClick=\"$('#%id').dialog('open');\">
                     <span class='sr-only'>" .  __s('Select default items to show') . "</span></span>";

                  $search_config_top .= str_replace('%id', 'search_config_top', $options_link);
                  $search_config_bottom .= str_replace('%id', 'search_config_bottom', $options_link);

                  $pref_url = $CFG_GLPI["root_doc"]."/front/displaypreference.form.php?itemtype=".
                              $data['itemtype'];
                  $search_config_top .= Ajax::createIframeModalWindow(
                     'search_config_top',
                     $pref_url,
                     [
                        'title'         => __('Select default items to show'),
                        'reloadonclose' => true,
                        'display'       => false
                     ]
                  );
                  $search_config_bottom .= Ajax::createIframeModalWindow(
                     'search_config_bottom',
                     $pref_url,
                     [
                        'title'         => __('Select default items to show'),
                        'reloadonclose' => true,
                        'display'       => false
                     ]
                  );
               }
            }

            if ($item !== null && $item->maybeDeleted()) {
               $delete_ctrl        = self::isDeletedSwitch($data['search']['is_deleted'], $data['itemtype']);
               $search_config_top .= $delete_ctrl;
            }

            if ($data['search']['show_pager']) {
               Html::printPager($data['search']['start'], $data['data']['totalcount'],
                              $data['search']['target'], $parameters, $data['itemtype'], 0,
                                 $search_config_top);
            }

            $search_config_top    .= "</div>";
            $search_config_bottom .= "</div>";
         }

         // Define begin and end var for loop
         // Search case
         $begin_display = $data['data']['begin'];
         $end_display   = $data['data']['end'];

         // Form to massive actions
         $isadmin = ($data['item'] && $data['item']->canUpdate());
         if (!$isadmin
               && Infocom::canApplyOn($data['itemtype'])) {
            $isadmin = (Infocom::canUpdate() || Infocom::canCreate());
         }
         if ($data['itemtype'] != 'AllAssets') {
            $showmassiveactions = ($data['search']['showmassiveactions'] ?? true)
               && count(MassiveAction::getAllMassiveActions($data['item'],
                                                            $data['search']['is_deleted']));
         } else {
            $showmassiveactions = $data['search']['showmassiveactions'] ?? true;
         }

         if ($data['search']['as_map'] == 0) {
            $massformid = 'massform'.$data['itemtype'];
            if ($showmassiveactions
               && ($data['display_type'] == self::HTML_OUTPUT)) {

               Html::openMassiveActionsForm($massformid);
               $massiveactionparams                  = $data['search']['massiveactionparams'];
               $massiveactionparams['num_displayed'] = $end_display-$begin_display;
               $massiveactionparams['fixed']         = false;
               $massiveactionparams['is_deleted']    = $data['search']['is_deleted'];
               $massiveactionparams['container']     = $massformid;

               Html::showMassiveActions($massiveactionparams);
            }

            // Compute number of columns to display
            // Add toview elements
            $nbcols          = count($data['data']['cols']);

            if (($data['display_type'] == self::HTML_OUTPUT)
               && $showmassiveactions) { // HTML display - massive modif
               $nbcols++;
            }

            // Display List Header
            echo self::showHeader($data['display_type'], $end_display-$begin_display+1, $nbcols);

            // New Line for Header Items Line
            $headers_line        = '';
            $headers_line_top    = '';
            $headers_line_bottom = '';

            $headers_line_top .= self::showBeginHeader($data['display_type']);
            $headers_line_top .= self::showNewLine($data['display_type']);

            if ($data['display_type'] == self::HTML_OUTPUT) {
               // $headers_line_bottom .= self::showBeginHeader($data['display_type']);
               $headers_line_bottom .= self::showNewLine($data['display_type']);
            }

            $header_num = 1;

            if (($data['display_type'] == self::HTML_OUTPUT)
                  && $showmassiveactions) { // HTML display - massive modif
               $headers_line_top
                  .= self::showHeaderItem($data['display_type'],
                                          Html::getCheckAllAsCheckbox($massformid),
                                          $header_num, "", 0, $data['search']['order']);
               if ($data['display_type'] == self::HTML_OUTPUT) {
                  $headers_line_bottom
                     .= self::showHeaderItem($data['display_type'],
                                             Html::getCheckAllAsCheckbox($massformid),
                                             $header_num, "", 0, $data['search']['order']);
               }
            }

            // Display column Headers for toview items
            $metanames = [];
            foreach ($data['data']['cols'] as $val) {
               $linkto = '';
               if (!$val['meta']
                   && !$data['search']['no_sort']
                   && (!isset($val['searchopt']['nosort'])
                     || !$val['searchopt']['nosort'])) {

                  $linkto = $data['search']['target'].(strpos($data['search']['target'], '?') ? '&amp;' : '?').
                              "itemtype=".$data['itemtype']."&amp;sort=".
                              $val['id']."&amp;order=".
                              (($data['search']['order'] == "ASC") ?"DESC":"ASC").
                              "&amp;start=".$data['search']['start']."&amp;".$globallinkto;
               }

               $name = $val["name"];

               // prefix by group name (corresponding to optgroup in dropdown) if exists
               if (isset($val['groupname'])) {
                  $groupname = $val['groupname'];
                  if (is_array($groupname)) {
                     //since 9.2, getSearchOptions has been changed
                     $groupname = $groupname['name'];
                  }
                  $name  = "$groupname - $name";
               }

               // Not main itemtype add itemtype to display
               if ($data['itemtype'] != $val['itemtype']) {
                  if (!isset($metanames[$val['itemtype']])) {
                     if ($metaitem = getItemForItemtype($val['itemtype'])) {
                        $metanames[$val['itemtype']] = $metaitem->getTypeName();
                     }
                  }
                  $name = sprintf(__('%1$s - %2$s'), $metanames[$val['itemtype']],
                                 $val["name"]);
               }

               $headers_line .= self::showHeaderItem($data['display_type'],
                                                      $name,
                                                      $header_num, $linkto,
                                                      (!$val['meta']
                                                      && ($data['search']['sort'] == $val['id'])),
                                                      $data['search']['order']);
            }

            // Add specific column Header
            if (isset($CFG_GLPI["union_search_type"][$data['itemtype']])) {
               $headers_line .= self::showHeaderItem($data['display_type'], __('Item type'),
                                                      $header_num);
            }
            // End Line for column headers
            $headers_line        .= self::showEndLine($data['display_type']);

            $headers_line_top    .= $headers_line;
            if ($data['display_type'] == self::HTML_OUTPUT) {
               $headers_line_bottom .= $headers_line;
            }

            $headers_line_top    .= self::showEndHeader($data['display_type']);
            // $headers_line_bottom .= self::showEndHeader($data['display_type']);

            echo $headers_line_top;

            // Init list of items displayed
            if ($data['display_type'] == self::HTML_OUTPUT) {
               Session::initNavigateListItems($data['itemtype']);
            }

            // Num of the row (1=header_line)
            $row_num = 1;

            $massiveaction_field = 'id';
            if (($data['itemtype'] != 'AllAssets')
                  && isset($CFG_GLPI["union_search_type"][$data['itemtype']])) {
               $massiveaction_field = 'refID';
            }

            $typenames = [];
            // Display Loop
            foreach ($data['data']['rows'] as $rowkey => $row) {
               // Column num
               $item_num = 1;
               $row_num++;
               // New line
               echo self::showNewLine($data['display_type'], ($row_num%2),
                                    $data['search']['is_deleted']);

               $current_type       = (isset($row['TYPE']) ? $row['TYPE'] : $data['itemtype']);
               $massiveaction_type = $current_type;

               if (($data['itemtype'] != 'AllAssets')
                  && isset($CFG_GLPI["union_search_type"][$data['itemtype']])) {
                  $massiveaction_type = $data['itemtype'];
               }

               // Add item in item list
               Session::addToNavigateListItems($current_type, $row["id"]);

               if (($data['display_type'] == self::HTML_OUTPUT)
                     && $showmassiveactions) { // HTML display - massive modif
                  $tmpcheck = "";

                  if (($data['itemtype'] == 'Entity')
                        && !in_array($row["id"], $_SESSION["glpiactiveentities"])) {
                     $tmpcheck = "&nbsp;";

                  } else if ($data['itemtype'] == 'User'
                           && !Session::canViewAllEntities()
                           && !Session::haveAccessToOneOfEntities(Profile_User::getUserEntities($row["id"], false))) {
                     $tmpcheck = "&nbsp;";

                  } else if (($data['item'] instanceof CommonDBTM)
                              && $data['item']->maybeRecursive()
                              && !in_array($row["entities_id"], $_SESSION["glpiactiveentities"])) {
                     $tmpcheck = "&nbsp;";

                  } else {
                     $tmpcheck = Html::getMassiveActionCheckBox($massiveaction_type,
                                                               $row[$massiveaction_field]);
                  }
                  echo self::showItem($data['display_type'], $tmpcheck, $item_num, $row_num,
                                       "width='10'");
               }

               // Print other toview items
               foreach ($data['data']['cols'] as $col) {
                  $colkey = "{$col['itemtype']}_{$col['id']}";
                  if (!$col['meta']) {
                     echo self::showItem($data['display_type'], $row[$colkey]['displayname'],
                                          $item_num, $row_num,
                                          self::displayConfigItem($data['itemtype'], $col['id'],
                                                                  $row, $colkey));
                  } else { // META case
                     echo self::showItem($data['display_type'], $row[$colkey]['displayname'],
                                       $item_num, $row_num);
                  }
               }

               if (isset($CFG_GLPI["union_search_type"][$data['itemtype']])) {
                  if (!isset($typenames[$row["TYPE"]])) {
                     if ($itemtmp = getItemForItemtype($row["TYPE"])) {
                        $typenames[$row["TYPE"]] = $itemtmp->getTypeName();
                     }
                  }
                  echo self::showItem($data['display_type'], $typenames[$row["TYPE"]],
                                    $item_num, $row_num);
               }
               // End Line
               echo self::showEndLine($data['display_type']);
               // Flush ONLY for an HTML display (issue #3348)
               if ($data['display_type'] == self::HTML_OUTPUT
                   && !$data['search']['dont_flush']) {
                  Html::glpi_flush();
               }
            }

            // Create title
            $title = '';
            if (($data['display_type'] == self::PDF_OUTPUT_LANDSCAPE)
                  || ($data['display_type'] == self::PDF_OUTPUT_PORTRAIT)) {
               $title = self::computeTitle($data);
            }

            if ($data['search']['show_footer']) {
               if ($data['display_type'] == self::HTML_OUTPUT) {
                  echo $headers_line_bottom;
               }
            }

            // Display footer (close table)
            echo self::showFooter($data['display_type'], $title, $data['data']['count']);

            if ($data['search']['show_footer']) {
               // Delete selected item
               if ($data['display_type'] == self::HTML_OUTPUT) {
                  if ($showmassiveactions) {
                     $massiveactionparams['ontop'] = false;
                     Html::showMassiveActions($massiveactionparams);
                     // End form for delete item
                     Html::closeForm();
                  } else {
                     echo "<br>";
                  }
               }
               if ($data['display_type'] == self::HTML_OUTPUT
                  && $data['search']['show_pager']) { // In case of HTML display
                  Html::printPager($data['search']['start'], $data['data']['totalcount'],
                                 $data['search']['target'], $parameters, '', 0,
                                    $search_config_bottom);

               }
            }
         }
      } else {
         if (!isset($_GET['_in_modal'])) {
            echo "<div class='center pager_controls'>";
            if (null == $item || $item->maybeLocated()) {
               $map_link = "<input type='checkbox' name='as_map' id='as_map' value='1'";
               if ($data['search']['as_map'] == 1) {
                  $map_link .= " checked='checked'";
               }
               $map_link .= "/>";
               $map_link .= "<label for='as_map'><span title='".__s('Show as map')."' class='pointer fa fa-globe-americas'
                  onClick=\"toogle('as_map','','','');
                              document.forms['searchform".$data["itemtype"]."'].submit();\"></span></label>";
               echo $map_link;
            }

            if ($item !== null && $item->maybeDeleted()) {
               echo self::isDeletedSwitch($data['search']['is_deleted'], $data['itemtype']);
            }
            echo "</div>";
         }
         echo self::showError($data['display_type']);
      }
   }


   /**
    * @since 0.90
    *
    * @param boolean $is_deleted
    * @param string  $itemtype
    *
    * @return string
   */
   static function isDeletedSwitch($is_deleted, $itemtype = "") {
      $rand = mt_rand();
      return "<div class='switch grey_border pager_controls'>".
             "<label for='is_deletedswitch$rand' title='".__s('Show the trashbin')."' >".
                "<span class='sr-only'>" . __s('Show the trashbin') . "</span>" .
                "<input type='hidden' name='is_deleted' value='0' /> ".
                "<input type='checkbox' id='is_deletedswitch$rand' name='is_deleted' value='1' ".
                  ($is_deleted?"checked='checked'":"").
                  " onClick = \"toogle('is_deleted','','','');
                              document.forms['searchform$itemtype'].submit();\" />".
                "<span class='fa fa-trash-alt pointer'></span>".
                "<span class='lever'></span>" .
                "</label>".
             "</div>";
   }


   /**
    * Compute title (use case of PDF OUTPUT)
    *
    * @param array $data Array data of search
    *
    * @return string Title
   **/
   static function computeTitle($data) {
      $title = "";

      if (count($data['search']['criteria'])) {
         //Drop the first link as it is not needed, or convert to clean link (AND NOT -> NOT)
         if (isset($data['search']['criteria']['0']['link'])) {
            $notpos = strpos($data['search']['criteria']['0']['link'], 'NOT');
            //If link was like '%NOT%' just use NOT. Otherwise remove the link
            if ($notpos > 0) {
               $data['search']['criteria']['0']['link'] = 'NOT';
            } else if (!$notpos) {
               unset($data['search']['criteria']['0']['link']);
            }
         }

         foreach ($data['search']['criteria'] as $criteria) {
            if (isset($criteria['itemtype'])) {
               $searchopt = &self::getOptions($criteria['itemtype']);
            } else {
               $searchopt = &self::getOptions($data['itemtype']);
            }
            $titlecontain = '';

            if (isset($criteria['criteria'])) {
               //This is a group criteria, call computeTitle again and concat
               $newdata = $data;
               $oldlink = $criteria['link'];
               $newdata['search'] = $criteria;
               $titlecontain = sprintf(__('%1$s %2$s (%3$s)'), $titlecontain, $oldlink,
                  Search::computeTitle($newdata));
            } else {
               if (strlen($criteria['value']) > 0) {
                  if (isset($criteria['link'])) {
                     $titlecontain = " ".$criteria['link']." ";
                  }
                  $gdname    = '';
                  $valuename = '';

                  switch ($criteria['field']) {
                     case "all" :
                        $titlecontain = sprintf(__('%1$s %2$s'), $titlecontain, __('All'));
                        break;

                     case "view" :
                        $titlecontain = sprintf(__('%1$s %2$s'), $titlecontain, __('Items seen'));
                        break;

                     default :
                        if (isset($criteria['meta']) && $criteria['meta']) {
                           $searchoptname = sprintf(__('%1$s / %2$s'),
                                          $criteria['itemtype'],
                                          $searchopt[$criteria['field']]["name"]);
                        } else {
                           $searchoptname = $searchopt[$criteria['field']]["name"];
                        }

                        $titlecontain = sprintf(__('%1$s %2$s'), $titlecontain, $searchoptname);
                        $itemtype     = getItemTypeForTable($searchopt[$criteria['field']]["table"]);
                        $valuename    = '';
                        if ($item = getItemForItemtype($itemtype)) {
                           $valuename = $item->getValueToDisplay($searchopt[$criteria['field']],
                                                                 $criteria['value']);
                        }

                        $gdname = Dropdown::getDropdownName($searchopt[$criteria['field']]["table"],
                                                            $criteria['value']);
                  }

                  if (empty($valuename)) {
                     $valuename = $criteria['value'];
                  }
                  switch ($criteria['searchtype']) {
                     case "equals" :
                        if (in_array($searchopt[$criteria['field']]["field"],
                                     ['name', 'completename'])) {
                           $titlecontain = sprintf(__('%1$s = %2$s'), $titlecontain, $gdname);
                        } else {
                           $titlecontain = sprintf(__('%1$s = %2$s'), $titlecontain, $valuename);
                        }
                        break;

                     case "notequals" :
                        if (in_array($searchopt[$criteria['field']]["field"],
                                       ['name', 'completename'])) {
                           $titlecontain = sprintf(__('%1$s <> %2$s'), $titlecontain, $gdname);
                        } else {
                           $titlecontain = sprintf(__('%1$s <> %2$s'), $titlecontain, $valuename);
                        }
                        break;

                     case "lessthan" :
                        $titlecontain = sprintf(__('%1$s < %2$s'), $titlecontain, $valuename);
                        break;

                     case "morethan" :
                        $titlecontain = sprintf(__('%1$s > %2$s'), $titlecontain, $valuename);
                        break;

                     case "contains" :
                        $titlecontain = sprintf(__('%1$s = %2$s'), $titlecontain,
                                                '%'.$valuename.'%');
                        break;

                     case "notcontains" :
                        $titlecontain = sprintf(__('%1$s <> %2$s'), $titlecontain,
                                                '%'.$valuename.'%');
                        break;

                     case "under" :
                        $titlecontain = sprintf(__('%1$s %2$s'), $titlecontain,
                                                sprintf(__('%1$s %2$s'), __('under'), $gdname));
                        break;

                     case "notunder" :
                        $titlecontain = sprintf(__('%1$s %2$s'), $titlecontain,
                                                sprintf(__('%1$s %2$s'), __('not under'), $gdname));
                        break;

                     default :
                        $titlecontain = sprintf(__('%1$s = %2$s'), $titlecontain, $valuename);
                        break;
                  }
               }
            }
            $title .= $titlecontain;
         }
      }
      if (isset($data['search']['metacriteria']) &&
         count($data['search']['metacriteria'])) {
         $metanames = [];
         foreach ($data['search']['metacriteria'] as $metacriteria) {
            $searchopt = &self::getOptions($metacriteria['itemtype']);
            if (!isset($metanames[$metacriteria['itemtype']])) {
               if ($metaitem = getItemForItemtype($metacriteria['itemtype'])) {
                  $metanames[$metacriteria['itemtype']] = $metaitem->getTypeName();
               }
            }

            $titlecontain2 = '';
            if (strlen($metacriteria['value']) > 0) {
               if (isset($metacriteria['link'])) {
                  $titlecontain2 = sprintf(__('%1$s %2$s'), $titlecontain2,
                                             $metacriteria['link']);
               }
               $titlecontain2
                  = sprintf(__('%1$s %2$s'), $titlecontain2,
                              sprintf(__('%1$s / %2$s'),
                                    $metanames[$metacriteria['itemtype']],
                                    $searchopt[$metacriteria['field']]["name"]));

               $gdname2 = Dropdown::getDropdownName($searchopt[$metacriteria['field']]["table"],
                                                      $metacriteria['value']);
               switch ($metacriteria['searchtype']) {
                  case "equals" :
                     if (in_array($searchopt[$metacriteria['link']]
                                             ["field"],
                                    ['name', 'completename'])) {
                        $titlecontain2 = sprintf(__('%1$s = %2$s'), $titlecontain2,
                                                   $gdname2);
                     } else {
                        $titlecontain2 = sprintf(__('%1$s = %2$s'), $titlecontain2,
                                                   $metacriteria['value']);
                     }
                     break;

                  case "notequals" :
                     if (in_array($searchopt[$metacriteria['link']]["field"],
                                    ['name', 'completename'])) {
                        $titlecontain2 = sprintf(__('%1$s <> %2$s'), $titlecontain2,
                                                   $gdname2);
                     } else {
                        $titlecontain2 = sprintf(__('%1$s <> %2$s'), $titlecontain2,
                                                   $metacriteria['value']);
                     }
                     break;

                  case "lessthan" :
                     $titlecontain2 = sprintf(__('%1$s < %2$s'), $titlecontain2,
                                                $metacriteria['value']);
                     break;

                  case "morethan" :
                     $titlecontain2 = sprintf(__('%1$s > %2$s'), $titlecontain2,
                                                $metacriteria['value']);
                     break;

                  case "contains" :
                     $titlecontain2 = sprintf(__('%1$s = %2$s'), $titlecontain2,
                                                '%'.$metacriteria['value'].'%');
                     break;

                  case "notcontains" :
                     $titlecontain2 = sprintf(__('%1$s <> %2$s'), $titlecontain2,
                                                '%'.$metacriteria['value'].'%');
                     break;

                  case "under" :
                     $titlecontain2 = sprintf(__('%1$s %2$s'), $titlecontain2,
                                                sprintf(__('%1$s %2$s'), __('under'),
                                                      $gdname2));
                     break;

                  case "notunder" :
                     $titlecontain2 = sprintf(__('%1$s %2$s'), $titlecontain2,
                                                sprintf(__('%1$s %2$s'), __('not under'),
                                                      $gdname2));
                     break;

                  default :
                     $titlecontain2 = sprintf(__('%1$s = %2$s'), $titlecontain2,
                                                $metacriteria['value']);
                     break;
               }
            }
            $title .= $titlecontain2;
         }
      }
      return $title;
   }

   /**
    * Get meta types available for search engine
    *
    * @param string $itemtype Type to display the form
    *
    * @return array Array of available itemtype
   **/
   static function getMetaItemtypeAvailable($itemtype) {
      global $CFG_GLPI;

      $itemtype = self::getMetaReferenceItemtype($itemtype);

      if (!(($item = getItemForItemtype($itemtype)) instanceof CommonDBTM)) {
         return [];
      }

      $key_to_itemtypes = [
         'directconnect_types'  => ['Computer'],
         'infocom_types'        => ['Budget', 'Infocom'],
         'linkgroup_types'      => ['Group'],
         // 'linkgroup_tech_types' => 'Group', // Cannot handle ambiguity with 'Group' from 'linkgroup_types'
         'linkuser_types'       => ['User'],
         // 'linkuser_tech_types'  => 'User', // Cannot handle ambiguity with 'User' from 'linkuser_types'
         'project_asset_types'  => ['Project'],
         'rackable_types'       => ['Enclosure', 'Rack'],
         'ticket_types'         => ['Change', 'Problem', 'Ticket'],
      ];

      $linked = [];
      foreach ($CFG_GLPI as $key => $values) {
         if ($key === 'link_types') {
            // Links are associated to all items of a type, it does not make any sense to use them in meta search
            continue;
         }
         if ($key === 'ticket_types' && $item instanceof CommonITILObject) {
            // Linked are filtered by CommonITILObject::getAllTypesForHelpdesk()
            $linked = array_merge($linked, array_keys($item::getAllTypesForHelpdesk()));
            continue;
         }

         $matches = [];
         if (preg_match('/^(.+)_types$/', $key, $matches)) {
            $config_itemtypes = array_key_exists($key, $key_to_itemtypes)
               ? $key_to_itemtypes[$key]
               : [ucwords($matches[1])];
            foreach ($config_itemtypes as $config_itemtype) {
               if (!is_a($config_itemtype, CommonDBTM::class, true)) {
                  continue;
               }
               if ($itemtype === $config_itemtype::getType()) {
                  // List is related to source itemtype, all types of list are so linked
                  $linked = array_merge($linked, $values);
               } else if (in_array($itemtype, $values)) {
                  // Source itemtype is inside list, type corresponding to list is so linked
                  $linked[] = $config_itemtype::getType();
               }
            }
         }
      }

      return array_unique($linked);
   }



   /**
    * @since 0.85
    *
    * @param $itemtype
   **/
   static function getMetaReferenceItemtype($itemtype) {

      if (!isPluginItemType($itemtype)) {
         return $itemtype;
      }

      // Use reference type if given itemtype extends a reference type.
      $types = [
         'Computer',
         'Problem',
         'Change',
         'Ticket',
         'Printer',
         'Monitor',
         'Peripheral',
         'Software',
         'Phone'
      ];
      foreach ($types as $type) {
         if (is_a($itemtype, $type, true)) {
            return $type;
         }
      }

      return false;
   }


   /**
    * @since 0.85
   **/
   static function getLogicalOperators($only_not = false) {
      if ($only_not) {
         return [
            'AND'     => Dropdown::EMPTY_VALUE,
            'AND NOT' => __("NOT")
         ];
      }

      return [
         'AND'     => __('AND'),
         'OR'      => __('OR'),
         'AND NOT' => __('AND NOT'),
         'OR NOT'  => __('OR NOT')
      ];
   }


   /**
    * Print generic search form
    *
    * Params need to parsed before using Search::manageParams function
    *
    * @param string $itemtype  Type to display the form
    * @param array  $params    Array of parameters may include sort, is_deleted, criteria, metacriteria
    *
    * @return void
   **/
   static function showGenericSearch($itemtype, array $params) {
      global $CFG_GLPI;

      // Default values of parameters
      $p['sort']         = '';
      $p['is_deleted']   = 0;
      $p['as_map']       = 0;
      $p['criteria']     = [];
      $p['metacriteria'] = [];
      if (class_exists($itemtype)) {
         $p['target']       = $itemtype::getSearchURL();
      } else {
         $p['target']       = Toolbox::getItemTypeSearchURL($itemtype);
      }
      $p['showreset']    = true;
      $p['showbookmark'] = true;
      $p['showfolding']  = true;
      $p['mainform']     = true;
      $p['prefix_crit']  = '';
      $p['addhidden']    = [];
      $p['actionname']   = 'search';
      $p['actionvalue']  = _sx('button', 'Search');

      foreach ($params as $key => $val) {
         $p[$key] = $val;
      }

      $main_block_class = '';
      if ($p['mainform']) {
         echo "<form name='searchform$itemtype' method='get' action='".$p['target']."'>";
      } else {
         $main_block_class = "sub_criteria";
      }
      echo "<div id='searchcriteria' class='$main_block_class'>";
      $nbsearchcountvar      = 'nbcriteria'.strtolower($itemtype).mt_rand();
      $searchcriteriatableid = 'criteriatable'.strtolower($itemtype).mt_rand();
      // init criteria count
      echo Html::scriptBlock("
         var $nbsearchcountvar = ".count($p['criteria']).";
      ");

      echo "<ul id='$searchcriteriatableid'>";

      // Display normal search parameters
      $i = 0;
      foreach (array_keys($p['criteria']) as $i) {
         self::displayCriteria([
            'itemtype' => $itemtype,
            'num'      => $i,
            'p'        => $p
         ]);
      }

      $rand_criteria = mt_rand();
      echo "<li id='more-criteria$rand_criteria'
            class='normalcriteria headerRow'
            style='display: none;'>...</li>";

      echo "</ul>";
      echo "<div class='search_actions'>";
      $linked = self::getMetaItemtypeAvailable($itemtype);
      echo "<span id='addsearchcriteria$rand_criteria' class='secondary'>
               <i class='fas fa-plus-square'></i>
               ".__s('rule')."
            </span>";
      if (count($linked)) {
         echo "<span id='addmetasearchcriteria$rand_criteria' class='secondary'>
                  <i class='far fa-plus-square'></i>
                  ".__s('global rule')."
               </span>";
      }
      echo "<span id='addcriteriagroup$rand_criteria' class='secondary'>
               <i class='fas fa-plus-circle'></i>
               ".__s('group')."
            </span>";
      $json_p = json_encode($p);

      if ($p['mainform']) {
         // Display submit button
         echo "<input type='submit' name='".$p['actionname']."' value=\"".$p['actionvalue']."\" class='submit' >";
         if ($p['showbookmark'] || $p['showreset']) {
            if ($p['showbookmark']) {
               //TODO: change that!
               Ajax::createIframeModalWindow('loadbookmark',
                                       SavedSearch::getSearchURL() . "?action=load&type=" . SavedSearch::SEARCH,
                                       ['title'         => __('Load a saved search')]);
               SavedSearch::showSaveButton(SavedSearch::SEARCH, $itemtype);
            }

            if ($p['showreset']) {
               echo "<a class='fa fa-undo reset-search' href='"
                  .$p['target']
                  .(strpos($p['target'], '?') ? '&amp;' : '?')
                  ."reset=reset' title=\"".__s('Blank')."\"
                  ><span class='sr-only'>" . __s('Blank')  ."</span></a>";
            }

            if ($p['showfolding']) {
               echo "<a class='fa fa-angle-double-up fa-fw fold-search'
                        href='#'
                        title=\"".__("Fold search")."\"></a>";
            }
         }
      }
      echo "</div>"; //.search_actions

      // idor checks
      $idor_display_criteria       = Session::getNewIDORToken($itemtype);
      $idor_display_meta_criteria  = Session::getNewIDORToken($itemtype);
      $idor_display_criteria_group = Session::getNewIDORToken($itemtype);

      $JS = <<<JAVASCRIPT
         $('#addsearchcriteria$rand_criteria').on('click', function(event) {
            event.preventDefault();
            $.post('{$CFG_GLPI['root_doc']}/ajax/search.php', {
               'action': 'display_criteria',
               'itemtype': '$itemtype',
               'num': $nbsearchcountvar,
               'p': $json_p,
               '_idor_token': '$idor_display_criteria'
            })
            .done(function(data) {
               $(data).insertBefore('#more-criteria$rand_criteria');
               $nbsearchcountvar++;
            });
         });

         $('#addmetasearchcriteria$rand_criteria').on('click', function(event) {
            event.preventDefault();
            $.post('{$CFG_GLPI['root_doc']}/ajax/search.php', {
               'action': 'display_meta_criteria',
               'itemtype': '$itemtype',
               'meta': true,
               'num': $nbsearchcountvar,
               'p': $json_p,
               '_idor_token': '$idor_display_meta_criteria'
            })
            .done(function(data) {
               $(data).insertBefore('#more-criteria$rand_criteria');
               $nbsearchcountvar++;
            });
         });

         $('#addcriteriagroup$rand_criteria').on('click', function(event) {
            event.preventDefault();
            $.post('{$CFG_GLPI['root_doc']}/ajax/search.php', {
               'action': 'display_criteria_group',
               'itemtype': '$itemtype',
               'meta': true,
               'num': $nbsearchcountvar,
               'p': $json_p,
               '_idor_token': '$idor_display_criteria_group'
            })
            .done(function(data) {
               $(data).insertBefore('#more-criteria$rand_criteria');
               $nbsearchcountvar++;
            });
         });
JAVASCRIPT;

      if ($p['mainform']) {
         $JS .= <<<JAVASCRIPT
         $('.fold-search').on('click', function(event) {
            var search_criteria =  $('#searchcriteria ul li:not(:first-child)');
            event.preventDefault();
            $(this)
               .toggleClass('fa-angle-double-up')
               .toggleClass('fa-angle-double-down');
            search_criteria.toggle();
            window.localStorage.setItem(
               'show_full_searchcriteria',
               search_criteria.first().is(':visible')
            );
         });

         // Init search_criteria state
         var search_criteria_visibility = window.localStorage.getItem('show_full_searchcriteria');
         if (search_criteria_visibility !== undefined && search_criteria_visibility == 'false') {
            $('.fold-search').click();
         }

         $(document).on("click", ".remove-search-criteria", function() {
            var rowID = $(this).data('rowid');
            $('#' + rowID).remove();
            $('#searchcriteria ul li:first-child').addClass('headerRow').show();
         });
JAVASCRIPT;
      }
      echo Html::scriptBlock($JS);

      if (count($p['addhidden'])) {
         foreach ($p['addhidden'] as $key => $val) {
            echo Html::hidden($key, ['value' => $val]);
         }
      }

      if ($p['mainform']) {
         // For dropdown
         echo Html::hidden('itemtype', ['value' => $itemtype]);
         // Reset to start when submit new search
         echo Html::hidden('start', ['value'    => 0]);
      }

      echo "</div>";
      if ($p['mainform']) {
         Html::closeForm();
      }
   }

   /**
    * Display a criteria field set, this function should be called by ajax/search.php
    *
    * @since 9.4
    *
    * @param  array  $request we should have these keys of parameters:
    *                            - itemtype: main itemtype for criteria, sub one for metacriteria
    *                            - num: index of the criteria
    *                            - p: params of showGenericSearch method
    *
    * @return void
    */
   static function displayCriteria($request = []) {
      global $CFG_GLPI;

      if (!isset($request["itemtype"])
          || !isset($request["num"])) {
         return "";
      }

      $num         = (int) $request['num'];
      $p           = $request['p'];
      $options     = self::getCleanedOptions($request["itemtype"]);
      $randrow     = mt_rand();
      $rowid       = 'searchrow'.$request['itemtype'].$randrow;
      $addclass    = $num == 0 ? ' headerRow' : '';
      $prefix      = isset($p['prefix_crit']) ? $p['prefix_crit'] :'';
      $parents_num = isset($p['parents_num']) ? $p['parents_num'] : [];
      $criteria    = [];
      $from_meta   = isset($request['from_meta']) && $request['from_meta'];

      $sess_itemtype = $request["itemtype"];
      if ($from_meta) {
         $sess_itemtype = $request["parent_itemtype"];
      }

      if (!$criteria = self::findCriteriaInSession($sess_itemtype, $num, $parents_num)) {
         $criteria = self::getDefaultCriteria($request["itemtype"]);
      }

      if (isset($criteria['meta'])
          && $criteria['meta']
          && !$from_meta) {
         return self::displayMetaCriteria($request);
      }

      if (isset($criteria['criteria'])
          && is_array($criteria['criteria'])) {
         return self::displayCriteriaGroup($request);
      }

      echo "<li class='normalcriteria$addclass' id='$rowid'>";

      if (!$from_meta) {
         // First line display add / delete images for normal and meta search items
         if ($num == 0
             && isset($p['mainform'])
             && $p['mainform']) {
            // Instanciate an object to access method
            $item = null;
            if ($request["itemtype"] != 'AllAssets') {
               $item = getItemForItemtype($request["itemtype"]);
            }
            if ($item && $item->maybeDeleted()) {
               echo Html::hidden('is_deleted', [
                  'value' => $p['is_deleted'],
                  'id'    => 'is_deleted'
               ]);
            }
            echo Html::hidden('as_map', [
               'value' => $p['as_map'],
               'id'    => 'as_map'
            ]);
         }
         echo "<i class='far fa-minus-square remove-search-criteria' alt='-' title=\"".
                  __s('Delete a rule')."\" data-rowid='$rowid'></i>&nbsp;";

      }

      // Display link item
      $value = '';
      if (!$from_meta) {
         if (isset($criteria["link"])) {
            $value = $criteria["link"];
         }
         $operators = Search::getLogicalOperators(($num == 0));
         Dropdown::showFromArray("criteria{$prefix}[$num][link]", $operators, [
            'value' => $value,
            'width' => '80px'
         ]);
      }

      $values   = [];
      // display select box to define search item
      if ($CFG_GLPI['allow_search_view'] == 2 && !isset($request['from_meta'])) {
         $values['view'] = __('Items seen');
      }

      reset($options);
      $group = '';

      foreach ($options as $key => $val) {
         // print groups
         if (!is_array($val)) {
            $group = $val;
         } else if (count($val) == 1) {
            $group = $val['name'];
         } else {
            if ((!isset($val['nosearch']) || ($val['nosearch'] == false))
                && (!$from_meta || !array_key_exists('nometa', $val) || $val['nometa'] !== true)) {
               $values[$group][$key] = $val["name"];
            }
         }
      }
      if ($CFG_GLPI['allow_search_view'] == 1 && !isset($request['from_meta'])) {
         $values['view'] = __('Items seen');
      }
      if ($CFG_GLPI['allow_search_all'] && !isset($request['from_meta'])) {
         $values['all'] = __('All');
      }
      $value = '';

      if (isset($criteria['field'])) {
         $value = $criteria['field'];
      }

      $rand = Dropdown::showFromArray("criteria{$prefix}[$num][field]", $values, [
         'value' => $value,
         'width' => '170px'
      ]);
      $field_id = Html::cleanId("dropdown_criteria{$prefix}[$num][field]$rand");
      $spanid   = Html::cleanId('SearchSpan'.$request["itemtype"].$prefix.$num);
      echo "<span id='$spanid'>";

      $used_itemtype = $request["itemtype"];
      // Force Computer itemtype for AllAssets to permit to show specific items
      if ($request["itemtype"] == 'AllAssets') {
         $used_itemtype = 'Computer';
      }

      $searchtype = isset($criteria['searchtype'])
                     ? $criteria['searchtype']
                     : "";
      $p_value    = isset($criteria['value'])
                     ? stripslashes($criteria['value'])
                     : "";

      $params = [
         'itemtype'    => $used_itemtype,
         '_idor_token' => Session::getNewIDORToken($used_itemtype),
         'field'       => $value,
         'searchtype'  => $searchtype,
         'value'       => $p_value,
         'num'         => $num,
         'p'           => $p,
      ];
      Search::displaySearchoption($params);
      echo "</span>";

      Ajax::updateItemOnSelectEvent(
         $field_id,
         $spanid,
         $CFG_GLPI["root_doc"]."/ajax/search.php",
         [
            'action'     => 'display_searchoption',
            'field'      => '__VALUE__',
         ] + $params
      );

      echo "</li>";
   }

   /**
    * Display a meta-criteria field set, this function should be called by ajax/search.php
    * Call displayCriteria method after displaying its itemtype field
    *
    * @since 9.4
    *
    * @param  array  $request @see displayCriteria method
    *
    * @return void
    */
   static function displayMetaCriteria($request = []) {
      global $CFG_GLPI;

      if (!isset($request["itemtype"])
          || !isset($request["num"])) {
         return "";
      }

      $p            = $request['p'];
      $num          = (int) $request['num'];
      $prefix       = isset($p['prefix_crit']) ? $p['prefix_crit'] : '';
      $parents_num  = isset($p['parents_num']) ? $p['parents_num'] : [];
      $itemtype     = $request["itemtype"];
      $metacriteria = [];

      if (!$metacriteria = self::findCriteriaInSession($itemtype, $num, $parents_num)) {
         // Set default field
         $options  = Search::getCleanedOptions($itemtype);

         foreach ($options as $key => $val) {
            if (is_array($val) && isset($val['table'])) {
               $metacriteria['field'] = $key;
               break;
            }
         }
      }

      $linked =  Search::getMetaItemtypeAvailable($itemtype);
      $rand   = mt_rand();

      $rowid  = 'metasearchrow'.$request['itemtype'].$rand;

      echo "<li class='metacriteria' id='$rowid'>";
      echo "<i class='far fa-minus-square remove-search-criteria' alt='-' title=\"".
               __s('Delete a global rule')."\" data-rowid='$rowid'></i>&nbsp;";

      // Display link item (not for the first item)
      Dropdown::showFromArray(
         "criteria{$prefix}[$num][link]",
         Search::getLogicalOperators(),
         [
            'value' => isset($metacriteria["link"])
               ? $metacriteria["link"]
               : "",
            'width' => '80px'
         ]
      );

      // Display select of the linked item type available
      $rand = Dropdown::showItemTypes("criteria{$prefix}[$num][itemtype]", $linked, [
         'value' => isset($metacriteria['itemtype'])
                    && !empty($metacriteria['itemtype'])
                     ? $metacriteria['itemtype']
                     : "",
         'width' => '170px'
      ]);
      echo Html::hidden("criteria{$prefix}[$num][meta]", [
         'value' => true
      ]);
      $field_id = Html::cleanId("dropdown_criteria{$prefix}[$num][itemtype]$rand");
      $spanid   = Html::cleanId("show_".$request["itemtype"]."_".$prefix.$num."_$rand");
      // Ajax script for display search met& item
      echo "<blockquote>";

      $params = [
         'action'          => 'display_criteria',
         'itemtype'        => '__VALUE__',
         'parent_itemtype' => $request['itemtype'],
         'from_meta'       => true,
         'num'             => $num,
         'p'               => $request["p"],
         '_idor_token'     => Session::getNewIDORToken("", [
            'parent_itemtype' => $request['itemtype']
         ])
      ];
      Ajax::updateItemOnSelectEvent(
         $field_id,
         $spanid,
         $CFG_GLPI["root_doc"]."/ajax/search.php",
         $params
      );

      echo "<span id='$spanid'>";
      if (isset($metacriteria['itemtype'])
          && !empty($metacriteria['itemtype'])) {
         $params['itemtype'] = $metacriteria['itemtype'];
         self::displayCriteria($params);
      }
      echo "</span>";
      echo "</blockquote>";
      echo "</li>";
   }

   /**
    * Display a group of nested criteria.
    * A group (parent) criteria  can contains children criteria (who also cantains children, etc)
    *
    * @since 9.4
    *
    * @param  array  $request @see displayCriteria method
    *
    * @return void
    */
   static function displayCriteriaGroup($request = []) {
      $num         = (int) $request['num'];
      $p           = $request['p'];
      $randrow     = mt_rand();
      $rowid       = 'searchrow'.$request['itemtype'].$randrow;
      $addclass    = $num == 0 ? ' headerRow' : '';
      $prefix      = isset($p['prefix_crit']) ? $p['prefix_crit'] : '';
      $parents_num = isset($p['parents_num']) ? $p['parents_num'] : [];

      if (!$criteria = self::findCriteriaInSession($request['itemtype'], $num, $parents_num)) {
         $criteria = [
            'criteria' => self::getDefaultCriteria($request['itemtype']),
         ];
      }

      echo "<li class='normalcriteria$addclass' id='$rowid'>";
      echo "<i class='far fa-minus-square remove-search-criteria' alt='-' title=\"".
               __s('Delete a rule')."\" data-rowid='$rowid'></i>&nbsp;";
      Dropdown::showFromArray("criteria{$prefix}[$num][link]", Search::getLogicalOperators(), [
         'value' => isset($criteria["link"]) ? $criteria["link"] : '',
         'width' => '80px'
      ]);

      $parents_num = isset($p['parents_num']) ? $p['parents_num'] : [];
      array_push($parents_num, $num);
      $params = [
         'mainform'    => false,
         'prefix_crit' => "{$prefix}[$num][criteria]",
         'parents_num' => $parents_num,
         'criteria'    => $criteria['criteria'],
      ];

      echo self::showGenericSearch($request['itemtype'], $params);
      echo "</li>";
   }

   /**
    * Retrieve a single criteria in Session by its index
    *
    * @since 9.4
    *
    * @param  string  $itemtype    which glpi type we must search in session
    * @param  integer $num         index of the criteria
    * @param  array   $parents_num node indexes of the parents (@see displayCriteriaGroup)
    *
    * @return mixed   the found criteria array of false of nothing found
    */
   static function findCriteriaInSession($itemtype = '', $num = 0, $parents_num = []) {
      if (!isset($_SESSION['glpisearch'][$itemtype]['criteria'])) {
         return false;
      }
      $criteria = &$_SESSION['glpisearch'][$itemtype]['criteria'];

      if (count($parents_num)) {
         foreach ($parents_num as $parent) {
            if (!isset($criteria[$parent]['criteria'])) {
               return false;
            }
            $criteria = &$criteria[$parent]['criteria'];
         }
      }

      if (isset($criteria[$num])
          && is_array($criteria[$num])) {
         return $criteria[$num];
      }

      return false;
   }

   /**
    * construct the default criteria for an itemtype
    *
    * @since 9.4
    *
    * @param  string $itemtype
    *
    * @return array  criteria
    */
   static function getDefaultCriteria($itemtype = '') {
      global $CFG_GLPI;

      $field = '';

      if ($CFG_GLPI['allow_search_view'] == 2) {
         $field = 'view';
      } else {
         $options = self::getCleanedOptions($itemtype);
         foreach ($options as $key => $val) {
            if (is_array($val)
                && isset($val['table'])) {
               $field = $key;
               break;
            }
         }
      }

      return [
         [
            'field' => $field,
            'link'  => 'contains',
            'value' => ''
         ]
      ];
   }

   /**
    * Display first part of criteria (field + searchtype, just after link)
    * will call displaySearchoptionValue for the next part (value)
    *
    * @since 9.4
    *
    * @param  array  $request we should have these keys of parameters:
    *                            - itemtype: main itemtype for criteria, sub one for metacriteria
    *                            - num: index of the criteria
    *                            - field: field key of the criteria
    *                            - p: params of showGenericSearch method
    *
    * @return void
    */
   static function displaySearchoption($request = []) {
      global $CFG_GLPI;
      if (!isset($request["itemtype"])
          || !isset($request["field"])
          || !isset($request["num"])) {
         return "";
      }

      $p      = $request['p'];
      $num    = (int) $request['num'];
      $prefix = isset($p['prefix_crit']) ? $p['prefix_crit'] : '';

      if (!is_subclass_of($request['itemtype'], 'CommonDBTM')) {
         throw new \RuntimeException('Invalid itemtype provided!');
      }

      if (isset($request['meta']) && $request['meta']) {
         $fieldname = 'metacriteria';
      } else {
         $fieldname = 'criteria';
         $request['meta'] = 0;
      }

      $actions = Search::getActionsFor($request["itemtype"], $request["field"]);

      // is it a valid action for type ?
      if (count($actions)
          && (empty($request['searchtype']) || !isset($actions[$request['searchtype']]))) {
         $tmp = $actions;
         unset($tmp['searchopt']);
         $request['searchtype'] = key($tmp);
         unset($tmp);
      }

      $rands = -1;
      $dropdownname = Html::cleanId("spansearchtype$fieldname".
                                    $request["itemtype"].
                                    $prefix.
                                    $num);
      $searchopt = [];
      if (count($actions)>0) {
         // get already get search options
         if (isset($actions['searchopt'])) {
            $searchopt = $actions['searchopt'];
            // No name for clean array with quotes
            unset($searchopt['name']);
            unset($actions['searchopt']);
         }
         $searchtype_name = "{$fieldname}{$prefix}[$num][searchtype]";
         $rands = Dropdown::showFromArray($searchtype_name, $actions, [
            'value' => $request["searchtype"],
            'width' => '105px'
         ]);
         $fieldsearch_id = Html::cleanId("dropdown_$searchtype_name$rands");
      }

      echo "<span id='$dropdownname'>";
      $params = [
         'value'       => rawurlencode(stripslashes($request['value'])),
         'searchopt'   => $searchopt,
         'searchtype'  => $request["searchtype"],
         'num'         => $num,
         'itemtype'    => $request["itemtype"],
         '_idor_token' => Session::getNewIDORToken($request["itemtype"]),
         'from_meta'   => isset($request['from_meta'])
                           ? $request['from_meta']
                           : false,
         'field'       => $request["field"],
         'p'           => $p,
      ];
      self::displaySearchoptionValue($params);
      echo "</span>";

      Ajax::updateItemOnSelectEvent(
         $fieldsearch_id,
         $dropdownname,
         $CFG_GLPI["root_doc"]."/ajax/search.php",
         [
            'action'     => 'display_searchoption_value',
            'searchtype' => '__VALUE__',
         ] + $params
      );
   }

   /**
    * Display last part of criteria (value, just after searchtype)
    * called by displaySearchoptionValue
    *
    * @since 9.4
    *
    * @param  array  $request we should have these keys of parameters:
    *                            - searchtype: (contains, equals) passed by displaySearchoption
    *
    * @return void
    */
   static function displaySearchoptionValue($request = []) {
      if (!isset($request['searchtype'])) {
         return "";
      }

      $p                 = $request['p'];
      $prefix            = isset($p['prefix_crit']) ? $p['prefix_crit'] : '';
      $searchopt         = isset($request['searchopt']) ? $request['searchopt'] : [];
      $request['value']  = rawurldecode($request['value']);
      $fieldname         = isset($request['meta']) && $request['meta']
                              ? 'metacriteria'
                              : 'criteria';
      $inputname         = $fieldname.$prefix.'['.$request['num'].'][value]';
      $display           = false;
      $item              = getItemForItemtype($request['itemtype']);
      $options2          = [];
      $options2['value'] = $request['value'];
      $options2['width'] = '100%';
      // For tree dropdpowns
      $options2['permit_select_parent'] = true;

      switch ($request['searchtype']) {
         case "equals" :
         case "notequals" :
         case "morethan" :
         case "lessthan" :
         case "under" :
         case "notunder" :
            if (!$display && isset($searchopt['field'])) {
               // Specific cases
               switch ($searchopt['table'].".".$searchopt['field']) {
                  // Add mygroups choice to searchopt
                  case "glpi_groups.completename" :
                     $searchopt['toadd'] = ['mygroups' => __('My groups')];
                     break;

                  case "glpi_changes.status" :
                  case "glpi_changes.impact" :
                  case "glpi_changes.urgency" :
                  case "glpi_problems.status" :
                  case "glpi_problems.impact" :
                  case "glpi_problems.urgency" :
                  case "glpi_tickets.status" :
                  case "glpi_tickets.impact" :
                  case "glpi_tickets.urgency" :
                     $options2['showtype'] = 'search';
                     break;

                  case "glpi_changes.priority" :
                  case "glpi_problems.priority" :
                  case "glpi_tickets.priority" :
                     $options2['showtype']  = 'search';
                     $options2['withmajor'] = true;
                     break;

                  case "glpi_tickets.global_validation" :
                     $options2['all'] = true;
                     break;

                  case "glpi_ticketvalidations.status" :
                     $options2['all'] = true;
                     break;

                  case "glpi_users.name" :
                     $options2['right']            = (isset($searchopt['right']) ? $searchopt['right'] : 'all');
                     $options2['inactive_deleted'] = 1;
                     break;
               }

               // Standard datatype usage
               if (!$display && isset($searchopt['datatype'])) {
                  switch ($searchopt['datatype']) {

                     case "date" :
                     case "date_delay" :
                     case "datetime" :
                        $options2['relative_dates'] = true;
                        break;
                  }
               }

               $out = $item->getValueToSelect($searchopt, $inputname, $request['value'], $options2);
               if (strlen($out)) {
                  echo $out;
                  $display = true;
               }

               //Could display be handled by a plugin ?
               if (!$display
                   && $plug = isPluginItemType(getItemTypeForTable($searchopt['table']))) {
                  $display = Plugin::doOneHook(
                     $plug['plugin'],
                     'searchOptionsValues',
                     [
                        'name'           => $inputname,
                        'searchtype'     => $request['searchtype'],
                        'searchoption'   => $searchopt,
                        'value'          => $request['value']
                     ]
                  );
               }

            }
           break;
      }

      // Default case : text field
      if (!$display) {
           echo "<input type='text' size='13' name='$inputname' value=\"".
                  Html::cleanInputText($request['value'])."\">";
      }
   }


   /**
    * Generic Function to add GROUP BY to a request
    *
    * @since 9.4: $num param has been dropped
    *
    * @param string  $LINK           link to use
    * @param string  $NOT            is is a negative search ?
    * @param string  $itemtype       item type
    * @param integer $ID             ID of the item to search
    * @param string  $searchtype     search type ('contains' or 'equals')
    * @param string  $val            value search
    *
    * @return select string
   **/
   static function addHaving($LINK, $NOT, $itemtype, $ID, $searchtype, $val) {

      global $DB;

      $searchopt  = &self::getOptions($itemtype);
      if (!isset($searchopt[$ID]['table'])) {
         return false;
      }
      $table = $searchopt[$ID]["table"];
      $NAME = "ITEM_{$itemtype}_{$ID}";

      // Plugin can override core definition for its type
      if ($plug = isPluginItemType($itemtype)) {
         $out = Plugin::doOneHook(
            $plug['plugin'],
            'addHaving',
            $LINK, $NOT, $itemtype, $ID, $val, "{$itemtype}_{$ID}"
         );
         if (!empty($out)) {
            return $out;
         }
      }

      //// Default cases
      // Link with plugin tables
      if (preg_match("/^glpi_plugin_([a-z0-9]+)/", $table, $matches)) {
         if (count($matches) == 2) {
            $plug     = $matches[1];
            $out = Plugin::doOneHook(
               $plug,
               'addHaving',
               $LINK, $NOT, $itemtype, $ID, $val, "{$itemtype}_{$ID}"
            );
            if (!empty($out)) {
               return $out;
            }
         }
      }

      if (in_array($searchtype, ["notequals", "notcontains"])) {
         $NOT = !$NOT;
      }

      // Preformat items
      if (isset($searchopt[$ID]["datatype"])) {
         switch ($searchopt[$ID]["datatype"]) {
            case "datetime" :
               if (in_array($searchtype, ['contains', 'notcontains'])) {
                  break;
               }

               $force_day = false;
               if (strstr($val, 'BEGIN') || strstr($val, 'LAST')) {
                  $force_day = true;
               }

               $val = Html::computeGenericDateTimeSearch($val, $force_day);

               $operator = '';
               switch ($searchtype) {
                  case 'equals':
                     $operator = !$NOT ? '=' : '!=';
                     break;
                  case 'notequals':
                     $operator = !$NOT ? '!=' : '=';
                     break;
                  case 'lessthan':
                     $operator = !$NOT ? '<' : '>';
                     break;
                  case 'morethan':
                     $operator = !$NOT ? '>' : '<';
                     break;
               }

               return " {$LINK} ({$DB->quoteName($NAME)} $operator {$DB->quoteValue($val)}) ";
               break;
            case "count" :
            case "number" :
            case "decimal" :
            case "timestamp" :
               $search  = ["/\&lt;/","/\&gt;/"];
               $replace = ["<",">"];
               $val     = preg_replace($search, $replace, $val);
               if (preg_match("/([<>])([=]*)[[:space:]]*([0-9]+)/", $val, $regs)) {
                  if ($NOT) {
                     if ($regs[1] == '<') {
                        $regs[1] = '>';
                     } else {
                        $regs[1] = '<';
                     }
                  }
                  $regs[1] .= $regs[2];
                  return " $LINK (`$NAME` ".$regs[1]." ".$regs[3]." ) ";
               }

               if (is_numeric($val)) {
                  if (isset($searchopt[$ID]["width"])) {
                     if (!$NOT) {
                        return " $LINK (`$NAME` < ".(intval($val) + $searchopt[$ID]["width"])."
                                        AND `$NAME` > ".
                                           (intval($val) - $searchopt[$ID]["width"]).") ";
                     }
                     return " $LINK (`$NAME` > ".(intval($val) + $searchopt[$ID]["width"])."
                                     OR `$NAME` < ".
                                        (intval($val) - $searchopt[$ID]["width"])." ) ";
                  }
                  // Exact search
                  if (!$NOT) {
                     return " $LINK (`$NAME` = ".(intval($val)).") ";
                  }
                  return " $LINK (`$NAME` <> ".(intval($val)).") ";
               }
               break;
         }
      }

      return self::makeTextCriteria("`$NAME`", $val, $NOT, $LINK);
   }


   /**
    * Generic Function to add ORDER BY to a request
    *
    * @since 9.4: $key param has been dropped
    *
    * @param string  $itemtype  ID of the device type
    * @param integer $ID        field to add
    * @param string  $order     order define
    *
    * @return select string
    *
   **/
   static function addOrderBy($itemtype, $ID, $order) {
      global $CFG_GLPI;

      // Security test for order
      if ($order != "ASC") {
         $order = "DESC";
      }
      $searchopt = &self::getOptions($itemtype);

      $table     = $searchopt[$ID]["table"];
      $field     = $searchopt[$ID]["field"];

      $addtable = '';

      $is_fkey_composite_on_self = getTableNameForForeignKeyField($searchopt[$ID]["linkfield"]) == $table
         && $searchopt[$ID]["linkfield"] != getForeignKeyFieldForTable($table);
      $orig_table = self::getOrigTableName($itemtype);
      if (($is_fkey_composite_on_self || $table != $orig_table)
          && ($searchopt[$ID]["linkfield"] != getForeignKeyFieldForTable($table))) {
         $addtable .= "_".$searchopt[$ID]["linkfield"];
      }

      if (isset($searchopt[$ID]['joinparams'])) {
         $complexjoin = self::computeComplexJoinID($searchopt[$ID]['joinparams']);

         if (!empty($complexjoin)) {
            $addtable .= "_".$complexjoin;
         }
      }

      if (isset($CFG_GLPI["union_search_type"][$itemtype])) {
         return " ORDER BY `ITEM_{$itemtype}_{$ID}` $order ";
      }

      // Plugin can override core definition for its type
      if ($plug = isPluginItemType($itemtype)) {
         $out = Plugin::doOneHook(
            $plug['plugin'],
            'addOrderBy',
            $itemtype, $ID, $order, "{$itemtype}_{$ID}"
         );
         if (!empty($out)) {
            return $out;
         }
      }

      switch ($table.".".$field) {
         case "glpi_auth_tables.name" :
            $user_searchopt = self::getOptions('User');
            return " ORDER BY `glpi_users`.`authtype` $order,
                              `glpi_authldaps".$addtable."_".
                                 self::computeComplexJoinID($user_searchopt[30]['joinparams'])."`.
                                 `name` $order,
                              `glpi_authmails".$addtable."_".
                                 self::computeComplexJoinID($user_searchopt[31]['joinparams'])."`.
                                 `name` $order ";

         case "glpi_users.name" :
            if ($itemtype!='User') {
               if ($_SESSION["glpinames_format"] == User::FIRSTNAME_BEFORE) {
                  $name1 = 'firstname';
                  $name2 = 'realname';
               } else {
                  $name1 = 'realname';
                  $name2 = 'firstname';
               }
               return " ORDER BY `".$table.$addtable."`.`$name1` $order,
                                 `".$table.$addtable."`.`$name2` $order,
                                 `".$table.$addtable."`.`name` $order";
            }
            return " ORDER BY `".$table.$addtable."`.`name` $order";

         case "glpi_networkequipments.ip" :
         case "glpi_ipaddresses.name" :
            return " ORDER BY INET_ATON(`$table$addtable`.`$field`) $order ";
      }

      //// Default cases

      // Link with plugin tables
      if (preg_match("/^glpi_plugin_([a-z0-9]+)/", $table, $matches)) {
         if (count($matches) == 2) {
            $plug     = $matches[1];
            $out = Plugin::doOneHook(
               $plug,
               'addOrderBy',
               $itemtype, $ID, $order, "{$itemtype}_{$ID}"
            );
            if (!empty($out)) {
               return $out;
            }
         }
      }

      // Preformat items
      if (isset($searchopt[$ID]["datatype"])) {
         switch ($searchopt[$ID]["datatype"]) {
            case "date_delay" :
               $interval = "MONTH";
               if (isset($searchopt[$ID]['delayunit'])) {
                  $interval = $searchopt[$ID]['delayunit'];
               }

               $add_minus = '';
               if (isset($searchopt[$ID]["datafields"][3])) {
                  $add_minus = "- `$table$addtable`.`".$searchopt[$ID]["datafields"][3]."`";
               }
               return " ORDER BY ADDDATE(`$table$addtable`.`".$searchopt[$ID]["datafields"][1]."`,
                                         INTERVAL (`$table$addtable`.`".
                                                   $searchopt[$ID]["datafields"][2]."` $add_minus)
                                         $interval) $order ";
         }
      }

      return " ORDER BY `ITEM_{$itemtype}_{$ID}` $order ";
   }


   /**
    * Generic Function to add default columns to view
    *
    * @param string $itemtype device type
    * @param array  $params   array of parameters
    *
    * @return select string
   **/
   static function addDefaultToView($itemtype, $params) {
      global $CFG_GLPI;

      $toview = [];
      $item   = null;
      $entity_check = true;

      if ($itemtype != 'AllAssets') {
         $item = getItemForItemtype($itemtype);
         $entity_check = $item->isEntityAssign();
      }
      // Add first element (name)
      array_push($toview, 1);

      if (isset($params['as_map']) && $params['as_map'] == 1) {
         // Add location name when map mode
         array_push($toview, ($itemtype == 'Location' ? 1 : ($itemtype == 'Ticket' ? 83 : 3)));
      }

      // Add entity view :
      if (Session::isMultiEntitiesMode()
          && $entity_check
          && (isset($CFG_GLPI["union_search_type"][$itemtype])
              || ($item && $item->maybeRecursive())
              || isset($_SESSION['glpiactiveentities']) && (count($_SESSION["glpiactiveentities"]) > 1))) {
         array_push($toview, 80);
      }
      return $toview;
   }


   /**
    * Generic Function to add default select to a request
    *
    * @param string $itemtype device type
    *
    * @return string Select string
   **/
   static function addDefaultSelect($itemtype) {
      global $DB;

      $itemtable = self::getOrigTableName($itemtype);
      $item      = null;
      $mayberecursive = false;
      if ($itemtype != 'AllAssets') {
         $item           = getItemForItemtype($itemtype);
         $mayberecursive = $item->maybeRecursive();
      }
      $ret = "";
      switch ($itemtype) {

         case 'FieldUnicity' :
            $ret = "`glpi_fieldunicities`.`itemtype` AS ITEMTYPE,";
            break;

         default :
            // Plugin can override core definition for its type
            if ($plug = isPluginItemType($itemtype)) {
               $ret = Plugin::doOneHook(
                  $plug['plugin'],
                  'addDefaultSelect',
                  $itemtype
               );
            }
      }
      if ($itemtable == 'glpi_entities') {
         $ret .= "`$itemtable`.`id` AS entities_id, '1' AS is_recursive, ";
      } else if ($mayberecursive) {
         if ($item->isField('entities_id')) {
            $ret .= $DB->quoteName("$itemtable.entities_id").", ";
         }
         if ($item->isField('is_recursive')) {
            $ret .= $DB->quoteName("$itemtable.is_recursive").", ";
         }
      }
      return $ret;
   }


   /**
    * Generic Function to add select to a request
    *
    * @since 9.4: $num param has been dropped
    *
    * @param string  $itemtype     item type
    * @param integer $ID           ID of the item to add
    * @param boolean $meta         boolean is a meta
    * @param integer $meta_type    meta type table ID (default 0)
    *
    * @return string Select string
   **/
   static function addSelect($itemtype, $ID, $meta = 0, $meta_type = 0) {
      global $DB, $CFG_GLPI;

      $searchopt   = &self::getOptions($itemtype);
      $table       = $searchopt[$ID]["table"];
      $field       = $searchopt[$ID]["field"];
      $addtable    = "";
      $addtable2   = "";
      $NAME        = "ITEM_{$itemtype}_{$ID}";
      $complexjoin = '';

      if (isset($searchopt[$ID]['joinparams'])) {
         $complexjoin = self::computeComplexJoinID($searchopt[$ID]['joinparams']);
      }

      $is_fkey_composite_on_self = getTableNameForForeignKeyField($searchopt[$ID]["linkfield"]) == $table
         && $searchopt[$ID]["linkfield"] != getForeignKeyFieldForTable($table);

      $orig_table = self::getOrigTableName($itemtype);
      if (((($is_fkey_composite_on_self || $table != $orig_table)
            && (!isset($CFG_GLPI["union_search_type"][$itemtype])
                || ($CFG_GLPI["union_search_type"][$itemtype] != $table)))
           || !empty($complexjoin))
          && ($searchopt[$ID]["linkfield"] != getForeignKeyFieldForTable($table))) {
         $addtable .= "_".$searchopt[$ID]["linkfield"];
      }

      if (!empty($complexjoin)) {
         $addtable .= "_".$complexjoin;
         $addtable2 .= "_".$complexjoin;
      }

      $addmeta = "";
      if ($meta) {
         // $NAME = "META";
         if ($meta_type::getTable() != $table) {
            $addmeta = "_".$meta_type;
            $addtable  .= $addmeta;
            $addtable2 .= $addmeta;
         }
      }

      // Plugin can override core definition for its type
      if ($plug = isPluginItemType($itemtype)) {
         $out = Plugin::doOneHook(
            $plug['plugin'],
            'addSelect',
            $itemtype, $ID, "{$itemtype}_{$ID}"
         );
         if (!empty($out)) {
            return $out;
         }
      }

      $tocompute      = "`$table$addtable`.`$field`";
      $tocomputeid    = "`$table$addtable`.`id`";

      $tocomputetrans = "IFNULL(`$table".$addtable."_trans`.`value`,'".self::NULLVALUE."') ";

      $ADDITONALFIELDS = '';
      if (isset($searchopt[$ID]["additionalfields"])
          && count($searchopt[$ID]["additionalfields"])) {
         foreach ($searchopt[$ID]["additionalfields"] as $key) {
            if ($meta
                || (isset($searchopt[$ID]["forcegroupby"]) && $searchopt[$ID]["forcegroupby"])) {
               $ADDITONALFIELDS .= " IFNULL(GROUP_CONCAT(DISTINCT CONCAT(IFNULL(`$table$addtable`.`$key`,
                                                                         '".self::NULLVALUE."'),
                                                   '".self::SHORTSEP."', $tocomputeid)ORDER BY $tocomputeid SEPARATOR '".self::LONGSEP."'), '".self::NULLVALUE.self::SHORTSEP."')
                                    AS `".$NAME."_$key`, ";
            } else {
               $ADDITONALFIELDS .= "`$table$addtable`.`$key` AS `".$NAME."_$key`, ";
            }
         }
      }

      // Virtual display no select : only get additional fields
      if (strpos($field, '_virtual') === 0) {
         return $ADDITONALFIELDS;
      }

      switch ($table.".".$field) {

         case "glpi_users.name" :
            if ($itemtype != 'User') {
               if ((isset($searchopt[$ID]["forcegroupby"]) && $searchopt[$ID]["forcegroupby"])) {
                  $addaltemail = "";
                  if ((($itemtype == 'Ticket') || ($itemtype == 'Problem'))
                      && isset($searchopt[$ID]['joinparams']['beforejoin']['table'])
                      && (($searchopt[$ID]['joinparams']['beforejoin']['table']
                            == 'glpi_tickets_users')
                          || ($searchopt[$ID]['joinparams']['beforejoin']['table']
                                == 'glpi_problems_users')
                          || ($searchopt[$ID]['joinparams']['beforejoin']['table']
                                == 'glpi_changes_users'))) { // For tickets_users

                     $ticket_user_table
                        = $searchopt[$ID]['joinparams']['beforejoin']['table'].
                          "_".self::computeComplexJoinID($searchopt[$ID]['joinparams']['beforejoin']
                                                                   ['joinparams']).$addmeta;
                     $addaltemail
                        = "GROUP_CONCAT(DISTINCT CONCAT(`$ticket_user_table`.`users_id`, ' ',
                                                        `$ticket_user_table`.`alternative_email`)
                                                        SEPARATOR '".self::LONGSEP."') AS `".$NAME."_2`, ";
                  }
                  return " GROUP_CONCAT(DISTINCT `$table$addtable`.`id` SEPARATOR '".self::LONGSEP."')
                                       AS `".$NAME."`,
                           $addaltemail
                           $ADDITONALFIELDS";

               }
               return " `$table$addtable`.`$field` AS `".$NAME."`,
                        `$table$addtable`.`realname` AS `".$NAME."_realname`,
                        `$table$addtable`.`id`  AS `".$NAME."_id`,
                        `$table$addtable`.`firstname` AS `".$NAME."_firstname`,
                        $ADDITONALFIELDS";
            }
            break;

         case "glpi_softwarelicenses.number" :
            if ($meta) {
               return " FLOOR(SUM(`$table$addtable2`.`$field`)
                              * COUNT(DISTINCT `$table$addtable2`.`id`)
                              / COUNT(`$table$addtable2`.`id`)) AS `".$NAME."`,
                        MIN(`$table$addtable2`.`$field`) AS `".$NAME."_min`,
                         $ADDITONALFIELDS";
            } else {
               return " FLOOR(SUM(`$table$addtable`.`$field`)
                              * COUNT(DISTINCT `$table$addtable`.`id`)
                              / COUNT(`$table$addtable`.`id`)) AS `".$NAME."`,
                        MIN(`$table$addtable`.`$field`) AS `".$NAME."_min`,
                         $ADDITONALFIELDS";
            }

         case "glpi_profiles.name" :
            if (($itemtype == 'User')
                && ($ID == 20)) {

               $addtable2 = '';
               if ($meta) {
                  $addtable2 = "_".$meta_type;
               }
               return " GROUP_CONCAT(`$table$addtable`.`$field` SEPARATOR '".self::LONGSEP."') AS `".$NAME."`,
                        GROUP_CONCAT(`glpi_profiles_users$addtable2`.`entities_id` SEPARATOR '".self::LONGSEP."')
                                    AS `".$NAME."_entities_id`,
                        GROUP_CONCAT(`glpi_profiles_users$addtable2`.`is_recursive` SEPARATOR '".self::LONGSEP."')
                                    AS `".$NAME."_is_recursive`,
                        GROUP_CONCAT(`glpi_profiles_users$addtable2`.`is_dynamic` SEPARATOR '".self::LONGSEP."')
                                    AS `".$NAME."_is_dynamic`,
                        $ADDITONALFIELDS";
            }
            break;

         case "glpi_entities.completename" :
            if (($itemtype == 'User')
                && ($ID == 80)) {

               $addtable2 = '';
               if ($meta) {
                  $addtable2 = "_".$meta_type;
               }
               return " GROUP_CONCAT(`$table$addtable`.`completename` SEPARATOR '".self::LONGSEP."')
                                    AS `".$NAME."`,
                        GROUP_CONCAT(`glpi_profiles_users$addtable2`.`profiles_id` SEPARATOR '".self::LONGSEP."')
                                    AS `".$NAME."_profiles_id`,
                        GROUP_CONCAT(`glpi_profiles_users$addtable2`.`is_recursive` SEPARATOR '".self::LONGSEP."')
                                    AS `".$NAME."_is_recursive`,
                        GROUP_CONCAT(`glpi_profiles_users$addtable2`.`is_dynamic` SEPARATOR '".self::LONGSEP."')
                                    AS `".$NAME."_is_dynamic`,
                        $ADDITONALFIELDS";
            }
            break;

         case "glpi_auth_tables.name":
            $user_searchopt = self::getOptions('User');
            return " `glpi_users`.`authtype` AS `".$NAME."`,
                     `glpi_users`.`auths_id` AS `".$NAME."_auths_id`,
                     `glpi_authldaps".$addtable."_".
                           self::computeComplexJoinID($user_searchopt[30]['joinparams']).$addmeta."`.`$field`
                              AS `".$NAME."_".$ID."_ldapname`,
                     `glpi_authmails".$addtable."_".
                           self::computeComplexJoinID($user_searchopt[31]['joinparams']).$addmeta."`.`$field`
                              AS `".$NAME."_mailname`,
                     $ADDITONALFIELDS";

         case "glpi_softwareversions.name" :
            if ($meta && ($meta_type == 'Software')) {
               return " GROUP_CONCAT(DISTINCT CONCAT(`glpi_softwares`.`name`, ' - ',
                                                     `$table$addtable2`.`$field`, '".self::SHORTSEP."',
                                                     `$table$addtable2`.`id`) SEPARATOR '".self::LONGSEP."')
                                    AS `".$NAME."`,
                        $ADDITONALFIELDS";
            }
            break;

         case "glpi_softwareversions.comment" :
            if ($meta && ($meta_type == 'Software')) {
               return " GROUP_CONCAT(DISTINCT CONCAT(`glpi_softwares`.`name`, ' - ',
                                                     `$table$addtable2`.`$field`,'".self::SHORTSEP."',
                                                     `$table$addtable2`.`id`) SEPARATOR '".self::LONGSEP."')
                                    AS `".$NAME."`,
                        $ADDITONALFIELDS";
            }
            return " GROUP_CONCAT(DISTINCT CONCAT(`$table$addtable`.`name`, ' - ',
                                                  `$table$addtable`.`$field`, '".self::SHORTSEP."',
                                                  `$table$addtable`.`id`) SEPARATOR '".self::LONGSEP."')
                                 AS `".$NAME."`,
                     $ADDITONALFIELDS";

         case "glpi_states.name" :
            if ($meta && ($meta_type == 'Software')) {
               return " GROUP_CONCAT(DISTINCT CONCAT(`glpi_softwares`.`name`, ' - ',
                                                     `glpi_softwareversions$addtable`.`name`, ' - ',
                                                     `$table$addtable2`.`$field`, '".self::SHORTSEP."',
                                                     `$table$addtable2`.`id`) SEPARATOR '".self::LONGSEP."')
                                     AS `".$NAME."`,
                        $ADDITONALFIELDS";
            } else if ($itemtype == 'Software') {
               return " GROUP_CONCAT(DISTINCT CONCAT(`glpi_softwareversions`.`name`, ' - ',
                                                     `$table$addtable`.`$field`,'".self::SHORTSEP."',
                                                     `$table$addtable`.`id`) SEPARATOR '".self::LONGSEP."')
                                    AS `".$NAME."`,
                        $ADDITONALFIELDS";
            }
            break;

         case "glpi_itilfollowups.content":
         case "glpi_tickettasks.content":
         case "glpi_changetasks.content":
            if (is_subclass_of($itemtype, "CommonITILObject")) {
               // force ordering by date desc
               return " GROUP_CONCAT(
                  DISTINCT CONCAT(
                     IFNULL($tocompute, '".self::NULLVALUE."'),
                     '".self::SHORTSEP."',
                     $tocomputeid
                  )
                  ORDER BY `$table$addtable`.`date` DESC
                  SEPARATOR '".self::LONGSEP."'
               ) AS `".$NAME."`, $ADDITONALFIELDS";
            }
            break;

         default:
            break;
      }

      //// Default cases
      // Link with plugin tables
      if (preg_match("/^glpi_plugin_([a-z0-9]+)/", $table, $matches)) {
         if (count($matches) == 2) {
            $plug     = $matches[1];
            $out = Plugin::doOneHook(
               $plug,
               'addSelect',
               $itemtype, $ID, "{$itemtype}_{$ID}"
            );
            if (!empty($out)) {
               return $out;
            }
         }
      }

      if (isset($searchopt[$ID]["computation"])) {
         $tocompute = $searchopt[$ID]["computation"];
         $tocompute = str_replace($DB->quoteName('TABLE'), 'TABLE', $tocompute);
         $tocompute = str_replace("TABLE", $DB->quoteName("$table$addtable"), $tocompute);
      }
      // Preformat items
      if (isset($searchopt[$ID]["datatype"])) {
         switch ($searchopt[$ID]["datatype"]) {
            case "count" :
               return " COUNT(DISTINCT `$table$addtable`.`$field`) AS `".$NAME."`,
                     $ADDITONALFIELDS";

            case "date_delay" :
               $interval = "MONTH";
               if (isset($searchopt[$ID]['delayunit'])) {
                  $interval = $searchopt[$ID]['delayunit'];
               }

               $add_minus = '';
               if (isset($searchopt[$ID]["datafields"][3])) {
                  $add_minus = "-`$table$addtable`.`".$searchopt[$ID]["datafields"][3]."`";
               }
               if ($meta
                   || (isset($searchopt[$ID]["forcegroupby"]) && $searchopt[$ID]["forcegroupby"])) {
                  return " GROUP_CONCAT(DISTINCT ADDDATE(`$table$addtable`.`".
                                                            $searchopt[$ID]["datafields"][1]."`,
                                                         INTERVAL (`$table$addtable`.`".
                                                                    $searchopt[$ID]["datafields"][2].
                                                                    "` $add_minus) $interval)
                                         SEPARATOR '".self::LONGSEP."') AS `".$NAME."`,
                           $ADDITONALFIELDS";
               }
               return "ADDDATE(`$table$addtable`.`".$searchopt[$ID]["datafields"][1]."`,
                               INTERVAL (`$table$addtable`.`".$searchopt[$ID]["datafields"][2].
                                          "` $add_minus) $interval) AS `".$NAME."`,
                       $ADDITONALFIELDS";

            case "itemlink" :
               if ($meta
                  || (isset($searchopt[$ID]["forcegroupby"]) && $searchopt[$ID]["forcegroupby"])) {

                  $TRANS = '';
                  if (Session::haveTranslations(getItemTypeForTable($table), $field)) {
                      $TRANS = "GROUP_CONCAT(DISTINCT CONCAT(IFNULL($tocomputetrans, '".self::NULLVALUE."'),
                                                             '".self::SHORTSEP."',$tocomputeid) ORDER BY $tocomputeid
                                             SEPARATOR '".self::LONGSEP."')
                                     AS `".$NAME."_trans`, ";
                  }

                  return " GROUP_CONCAT(DISTINCT CONCAT($tocompute, '".self::SHORTSEP."' ,
                                                        `$table$addtable`.`id`) ORDER BY `$table$addtable`.`id`
                                        SEPARATOR '".self::LONGSEP."') AS `".$NAME."`,
                           $TRANS
                           $ADDITONALFIELDS";
               }
               return " $tocompute AS `".$NAME."`,
                        `$table$addtable`.`id` AS `".$NAME."_id`,
                        $ADDITONALFIELDS";
         }
      }

      // Default case
      if ($meta
          || (isset($searchopt[$ID]["forcegroupby"]) && $searchopt[$ID]["forcegroupby"]
              && (!isset($searchopt[$ID]["computation"])
                  || isset($searchopt[$ID]["computationgroupby"])
                     && $searchopt[$ID]["computationgroupby"]))) { // Not specific computation
         $TRANS = '';
         if (Session::haveTranslations(getItemTypeForTable($table), $field)) {
            $TRANS = "GROUP_CONCAT(DISTINCT CONCAT(IFNULL($tocomputetrans, '".self::NULLVALUE."'),
                                                   '".self::SHORTSEP."',$tocomputeid) ORDER BY $tocomputeid SEPARATOR '".self::LONGSEP."')
                                  AS `".$NAME."_trans`, ";

         }
         return " GROUP_CONCAT(DISTINCT CONCAT(IFNULL($tocompute, '".self::NULLVALUE."'),
                                               '".self::SHORTSEP."',$tocomputeid) ORDER BY $tocomputeid SEPARATOR '".self::LONGSEP."')
                              AS `".$NAME."`,
                  $TRANS
                  $ADDITONALFIELDS";
      }
      $TRANS = '';
      if (Session::haveTranslations(getItemTypeForTable($table), $field)) {
         $TRANS = $tocomputetrans." AS `".$NAME."_trans`, ";

      }
      return "$tocompute AS `".$NAME."`, $TRANS $ADDITONALFIELDS";
   }


   /**
    * Generic Function to add default where to a request
    *
    * @param string $itemtype device type
    *
    * @return string Where string
   **/
   static function addDefaultWhere($itemtype) {
      $condition = '';

      switch ($itemtype) {
         case 'Reminder' :
            $condition = Reminder::addVisibilityRestrict();
            break;

         case 'RSSFeed' :
            $condition = RSSFeed::addVisibilityRestrict();
            break;

         case 'Notification' :
            if (!Config::canView()) {
               $condition = " `glpi_notifications`.`itemtype` NOT IN ('CronTask', 'DBConnection') ";
            }
            break;

         // No link
         case 'User' :
            // View all entities
            if (!Session::canViewAllEntities()) {
               $condition = getEntitiesRestrictRequest("", "glpi_profiles_users", '', '', true);
            }
            break;

         case 'ProjectTask' :
            $condition  = '';
            $teamtable  = 'glpi_projecttaskteams';
            $condition .= "`glpi_projects`.`is_template` = 0";
            $condition .= " AND ((`$teamtable`.`itemtype` = 'User'
                             AND `$teamtable`.`items_id` = '".Session::getLoginUserID()."')";
            if (count($_SESSION['glpigroups'])) {
               $condition .= " OR (`$teamtable`.`itemtype` = 'Group'
                                    AND `$teamtable`.`items_id`
                                       IN (".implode(",", $_SESSION['glpigroups'])."))";
            }
            $condition .= ") ";
            break;

         case 'Project' :
            $condition = '';
            if (!Session::haveRight("project", Project::READALL)) {
               $teamtable  = 'glpi_projectteams';
               $condition .= "(`glpi_projects`.users_id = '".Session::getLoginUserID()."'
                               OR (`$teamtable`.`itemtype` = 'User'
                                   AND `$teamtable`.`items_id` = '".Session::getLoginUserID()."')";
               if (count($_SESSION['glpigroups'])) {
                  $condition .= " OR (`glpi_projects`.`groups_id`
                                       IN (".implode(",", $_SESSION['glpigroups'])."))";
                  $condition .= " OR (`$teamtable`.`itemtype` = 'Group'
                                      AND `$teamtable`.`items_id`
                                          IN (".implode(",", $_SESSION['glpigroups'])."))";
               }
               $condition .= ") ";
            }
            break;

         case 'Ticket' :
            // Same structure in addDefaultJoin
            $condition = '';
            if (!Session::haveRight("ticket", Ticket::READALL)) {

               $searchopt
                  = &self::getOptions($itemtype);
               $requester_table
                  = '`glpi_tickets_users_'.
                     self::computeComplexJoinID($searchopt[4]['joinparams']['beforejoin']
                                                          ['joinparams']).'`';
               $requestergroup_table
                  = '`glpi_groups_tickets_'.
                     self::computeComplexJoinID($searchopt[71]['joinparams']['beforejoin']
                                                          ['joinparams']).'`';

               $assign_table
                  = '`glpi_tickets_users_'.
                     self::computeComplexJoinID($searchopt[5]['joinparams']['beforejoin']
                                                          ['joinparams']).'`';
               $assigngroup_table
                  = '`glpi_groups_tickets_'.
                     self::computeComplexJoinID($searchopt[8]['joinparams']['beforejoin']
                                                          ['joinparams']).'`';

               $observer_table
                  = '`glpi_tickets_users_'.
                     self::computeComplexJoinID($searchopt[66]['joinparams']['beforejoin']
                                                          ['joinparams']).'`';
               $observergroup_table
                  = '`glpi_groups_tickets_'.
                     self::computeComplexJoinID($searchopt[65]['joinparams']['beforejoin']
                                                          ['joinparams']).'`';

               $condition = "(";

               if (Session::haveRight("ticket", Ticket::READMY)) {
                    $condition .= " $requester_table.users_id = '".Session::getLoginUserID()."'
                                    OR $observer_table.users_id = '".Session::getLoginUserID()."'
                                    OR `glpi_tickets`.`users_id_recipient` = '".Session::getLoginUserID()."'";
               } else {
                    $condition .= "0=1";
               }

               if (Session::haveRight("ticket", Ticket::READGROUP)) {
                  if (count($_SESSION['glpigroups'])) {
                     $condition .= " OR $requestergroup_table.`groups_id`
                                             IN (".implode(",", $_SESSION['glpigroups']).")";
                     $condition .= " OR $observergroup_table.`groups_id`
                                             IN (".implode(",", $_SESSION['glpigroups']).")";
                  }
               }

               if (Session::haveRight("ticket", Ticket::OWN)) {// Can own ticket : show assign to me
                  $condition .= " OR $assign_table.users_id = '".Session::getLoginUserID()."' ";
               }

               if (Session::haveRight("ticket", Ticket::READASSIGN)) { // assign to me

                  $condition .=" OR $assign_table.`users_id` = '".Session::getLoginUserID()."'";
                  if (count($_SESSION['glpigroups'])) {
                     $condition .= " OR $assigngroup_table.`groups_id`
                                             IN (".implode(",", $_SESSION['glpigroups']).")";
                  }
                  if (Session::haveRight('ticket', Ticket::ASSIGN)) {
                     $condition .= " OR `glpi_tickets`.`status`='".CommonITILObject::INCOMING."'";
                  }
               }

               if (Session::haveRightsOr('ticketvalidation',
                                         [TicketValidation::VALIDATEINCIDENT,
                                               TicketValidation::VALIDATEREQUEST])) {
                  $condition .= " OR `glpi_ticketvalidations`.`users_id_validate`
                                          = '".Session::getLoginUserID()."'";
               }
               $condition .= ") ";
            }
            break;

         case 'Change' :
         case 'Problem':
            if ($itemtype == 'Change') {
               $right       = 'change';
               $table       = 'changes';
               $groupetable = "`glpi_changes_groups_";
            } else if ($itemtype == 'Problem') {
               $right       = 'problem';
               $table       = 'problems';
               $groupetable = "`glpi_groups_problems_";
            }
            // Same structure in addDefaultJoin
            $condition = '';
            if (!Session::haveRight("$right", $itemtype::READALL)) {
               $searchopt       = &self::getOptions($itemtype);
               if (Session::haveRight("$right", $itemtype::READMY)) {
                  $requester_table      = '`glpi_'.$table.'_users_'.
                                          self::computeComplexJoinID($searchopt[4]['joinparams']
                                                                     ['beforejoin']['joinparams']).'`';
                  $requestergroup_table = $groupetable.
                                          self::computeComplexJoinID($searchopt[71]['joinparams']
                                                                     ['beforejoin']['joinparams']).'`';

                  $observer_table       = '`glpi_'.$table.'_users_'.
                                          self::computeComplexJoinID($searchopt[66]['joinparams']
                                                                     ['beforejoin']['joinparams']).'`';
                  $observergroup_table  = $groupetable.
                                          self::computeComplexJoinID($searchopt[65]['joinparams']
                                                                    ['beforejoin']['joinparams']).'`';

                  $assign_table         = '`glpi_'.$table.'_users_'.
                                          self::computeComplexJoinID($searchopt[5]['joinparams']
                                                                     ['beforejoin']['joinparams']).'`';
                  $assigngroup_table    = $groupetable.
                                          self::computeComplexJoinID($searchopt[8]['joinparams']
                                                                     ['beforejoin']['joinparams']).'`';
               }
               $condition = "(";

               if (Session::haveRight("$right", $itemtype::READMY)) {
                  $condition .= " $requester_table.users_id = '".Session::getLoginUserID()."'
                                 OR $observer_table.users_id = '".Session::getLoginUserID()."'
                                 OR $assign_table.users_id = '".Session::getLoginUserID()."'
                                 OR `glpi_".$table."`.`users_id_recipient` = '".Session::getLoginUserID()."'";
                  if (count($_SESSION['glpigroups'])) {
                     $my_groups_keys = "'" . implode("','", $_SESSION['glpigroups']) . "'";
                     $condition .= " OR $requestergroup_table.groups_id IN ($my_groups_keys)
                                 OR $observergroup_table.groups_id IN ($my_groups_keys)
                                 OR $assigngroup_table.groups_id IN ($my_groups_keys)";
                  }
               } else {
                  $condition .= "0=1";
               }

               $condition .= ") ";
            }
            break;

         case 'Config':
            $availableContexts = ['core'] + Plugin::getPlugins();
            $availableContexts = implode("', '", $availableContexts);
            $condition = "`context` IN ('$availableContexts')";
            break;

         case 'SavedSearch':
            $condition = SavedSearch::addVisibilityRestrict();
            break;

         case 'TicketTask':
            // Filter on is_private
            $allowed_is_private = [];
            if (Session::haveRight(TicketTask::$rightname, CommonITILTask::SEEPRIVATE)) {
               $allowed_is_private[] = 1;
            }
            if (Session::haveRight(TicketTask::$rightname, CommonITILTask::SEEPUBLIC)) {
               $allowed_is_private[] = 0;
            }

            // If the user can't see public and private
            if (!count($allowed_is_private)) {
               $condition = "0 = 1";
               break;
            }

            $in = "IN ('" . implode("','", $allowed_is_private) . "')";
            $condition = "(`glpi_tickettasks`.`is_private` $in ";

            // Check for assigned or created tasks
            $condition .= "OR `glpi_tickettasks`.`users_id` = " . Session::getLoginUserID() . " ";
            $condition .= "OR `glpi_tickettasks`.`users_id_tech` = " . Session::getLoginUserID() . " ";

            // Check for parent item visibility unless the user can see all the
            // possible parents
            if (!Session::haveRight('ticket', Ticket::READALL)) {
               $condition .= "AND " . TicketTask::buildParentCondition();
            }

            $condition .= ")";

            break;

         case 'ITILFollowup':
            // Filter on is_private
            $allowed_is_private = [];
            if (Session::haveRight(ITILFollowup::$rightname, ITILFollowup::SEEPRIVATE)) {
               $allowed_is_private[] = 1;
            }
            if (Session::haveRight(ITILFollowup::$rightname, ITILFollowup::SEEPUBLIC)) {
               $allowed_is_private[] = 0;
            }

            // If the user can't see public and private
            if (!count($allowed_is_private)) {
               $condition = "0 = 1";
               break;
            }

            $in = "IN ('" . implode("','", $allowed_is_private) . "')";
            $condition = "(`glpi_itilfollowups`.`is_private` $in ";

            // Now filter on parent item visiblity
            $condition .= "AND (";

            // Filter for "ticket" parents
            $condition .= ITILFollowup::buildParentCondition(\Ticket::getType());
            $condition .= "OR ";

            // Filter for "change" parents
            $condition .= ITILFollowup::buildParentCondition(
               \Change::getType(),
               'changes_id',
               "glpi_changes_users",
               "glpi_changes_groups"
            );
            $condition .= "OR ";

            // Fitler for "problem" parents
            $condition .= ITILFollowup::buildParentCondition(
               \Problem::getType(),
               'problems_id',
               "glpi_problems_users",
               "glpi_groups_problems"
            );
            $condition .= "))";

            break;

         default :
            // Plugin can override core definition for its type
            if ($plug = isPluginItemType($itemtype)) {
               $condition = Plugin::doOneHook($plug['plugin'], 'addDefaultWhere', $itemtype);
            }
            break;
      }

      /* Hook to restrict user right on current itemtype */
      list($itemtype, $condition) = Plugin::doHookFunction('add_default_where', [$itemtype, $condition]);
      return $condition;
   }

   /**
    * Generic Function to add where to a request
    *
    * @param string  $link         Link string
    * @param boolean $nott         Is it a negative search ?
    * @param string  $itemtype     Item type
    * @param integer $ID           ID of the item to search
    * @param string  $searchtype   Searchtype used (equals or contains)
    * @param string  $val          Item num in the request
    * @param integer $meta         Is a meta search (meta=2 in search.class.php) (default 0)
    *
    * @return string Where string
   **/
   static function addWhere($link, $nott, $itemtype, $ID, $searchtype, $val, $meta = 0) {

      global $DB;

      $searchopt = &self::getOptions($itemtype);
      if (!isset($searchopt[$ID]['table'])) {
         return false;
      }
      $table     = $searchopt[$ID]["table"];
      $field     = $searchopt[$ID]["field"];

      $inittable = $table;
      $addtable  = '';
      $is_fkey_composite_on_self = getTableNameForForeignKeyField($searchopt[$ID]["linkfield"]) == $table
         && $searchopt[$ID]["linkfield"] != getForeignKeyFieldForTable($table);
      $orig_table = self::getOrigTableName($itemtype);
      if (($table != 'asset_types')
          && ($is_fkey_composite_on_self || $table != $orig_table)
          && ($searchopt[$ID]["linkfield"] != getForeignKeyFieldForTable($table))) {
         $addtable = "_".$searchopt[$ID]["linkfield"];
         $table   .= $addtable;
      }

      if (isset($searchopt[$ID]['joinparams'])) {
         $complexjoin = self::computeComplexJoinID($searchopt[$ID]['joinparams']);

         if (!empty($complexjoin)) {
            $table .= "_".$complexjoin;
         }
      }

      $addmeta = "";
      if ($meta
          && ($itemtype::getTable() != $inittable)) {
         $addmeta = "_".$itemtype;
         $table .= $addmeta;
      }

      // Hack to allow search by ID on every sub-table
      if (preg_match('/^\$\$\$\$([0-9]+)$/', $val, $regs)) {
         return $link." (`$table`.`id` ".($nott?"<>":"=").$regs[1]." ".
                         (($regs[1] == 0)?" OR `$table`.`id` IS NULL":'').") ";
      }

      // Preparse value
      if (isset($searchopt[$ID]["datatype"])) {
         switch ($searchopt[$ID]["datatype"]) {
            case "datetime" :
            case "date" :
            case "date_delay" :
               $force_day = true;
               if ($searchopt[$ID]["datatype"] == 'datetime'
                  && !(strstr($val, 'BEGIN') || strstr($val, 'LAST') || strstr($val, 'DAY'))
               ) {
                  $force_day = false;
               }

               $val = Html::computeGenericDateTimeSearch($val, $force_day);

               break;
         }
      }
      switch ($searchtype) {
         case "notcontains" :
            $nott = !$nott;
         case "contains" :
            $SEARCH = self::makeTextSearch($val, $nott);
            break;

         case "equals" :
            if ($nott) {
               $SEARCH = " <> '$val'";
            } else {
               $SEARCH = " = '$val'";
            }
            break;

         case "notequals" :
            if ($nott) {
               $SEARCH = " = '$val'";
            } else {
               $SEARCH = " <> '$val'";
            }
            break;

         case "under" :
            if ($nott) {
               $SEARCH = " NOT IN ('".implode("','", getSonsOf($inittable, $val))."')";
            } else {
               $SEARCH = " IN ('".implode("','", getSonsOf($inittable, $val))."')";
            }
            break;

         case "notunder" :
            if ($nott) {
               $SEARCH = " IN ('".implode("','", getSonsOf($inittable, $val))."')";
            } else {
               $SEARCH = " NOT IN ('".implode("','", getSonsOf($inittable, $val))."')";
            }
            break;

      }

      //Check in current item if a specific where is defined
      if (method_exists($itemtype, 'addWhere')) {
         $out = $itemtype::addWhere($link, $nott, $itemtype, $ID, $searchtype, $val);
         if (!empty($out)) {
            return $out;
         }
      }

      // Plugin can override core definition for its type
      if ($plug = isPluginItemType($itemtype)) {
         $out = Plugin::doOneHook(
            $plug['plugin'],
            'addWhere',
            $link, $nott, $itemtype, $ID, $val, $searchtype
         );
         if (!empty($out)) {
            return $out;
         }
      }

      switch ($inittable.".".$field) {
         // case "glpi_users_validation.name" :

         case "glpi_users.name" :
            if ($itemtype == 'User') { // glpi_users case / not link table
               if (in_array($searchtype, ['equals', 'notequals'])) {
                  $search_str = "`$table`.`id`" . $SEARCH;

                  if ($searchtype == 'notequals') {
                     $nott = !$nott;
                  }

                  // Add NULL if $val = 0 and not negative search
                  // Or negative search on real value
                  if ((!$nott && ($val == 0)) || ($nott && ($val != 0))) {
                     $search_str .= " OR `$table`.`id` IS NULL";
                  }

                  return " $link ($search_str)";
               }
               return self::makeTextCriteria("`$table`.`$field`", $val, $nott, $link);
            }
            if ($_SESSION["glpinames_format"] == User::FIRSTNAME_BEFORE) {
               $name1 = 'firstname';
               $name2 = 'realname';
            } else {
               $name1 = 'realname';
               $name2 = 'firstname';
            }

            if (in_array($searchtype, ['equals', 'notequals'])) {
               return " $link (`$table`.`id`".$SEARCH.
                               (($val == 0)?" OR `$table`.`id` IS".
                                   (($searchtype == "notequals")?" NOT":"")." NULL":'').') ';
            }
            $toadd   = '';

            $tmplink = 'OR';
            if ($nott) {
               $tmplink = 'AND';
            }

            if (is_a($itemtype, CommonITILObject::class, true)) {
               if (isset($searchopt[$ID]["joinparams"]["beforejoin"]["table"])
                   && isset($searchopt[$ID]["joinparams"]["beforejoin"]["joinparams"])
                   && (($searchopt[$ID]["joinparams"]["beforejoin"]["table"]
                         == 'glpi_tickets_users')
                       || ($searchopt[$ID]["joinparams"]["beforejoin"]["table"]
                             == 'glpi_problems_users')
                       || ($searchopt[$ID]["joinparams"]["beforejoin"]["table"]
                             == 'glpi_changes_users'))) {

                  $bj        = $searchopt[$ID]["joinparams"]["beforejoin"];
                  $linktable = $bj['table'].'_'.self::computeComplexJoinID($bj['joinparams']).$addmeta;
                  //$toadd     = "`$linktable`.`alternative_email` $SEARCH $tmplink ";
                  $toadd     = self::makeTextCriteria("`$linktable`.`alternative_email`", $val,
                                                      $nott, $tmplink);
                  if ($val == '^$') {
                     return $link." ((`$linktable`.`users_id` IS NULL)
                            OR `$linktable`.`alternative_email` IS NULL)";
                  }
               }
            }
            $toadd2 = '';
            if ($nott
                && ($val != 'NULL') && ($val != 'null')) {
               $toadd2 = " OR `$table`.`$field` IS NULL";
            }
            return $link." (((`$table`.`$name1` $SEARCH
                            $tmplink `$table`.`$name2` $SEARCH
                            $tmplink `$table`.`$field` $SEARCH
                            $tmplink CONCAT(`$table`.`$name1`, ' ', `$table`.`$name2`) $SEARCH )
                            $toadd2) $toadd)";

         case "glpi_groups.completename" :
            if ($val == 'mygroups') {
               switch ($searchtype) {
                  case 'equals' :
                     return " $link (`$table`.`id` IN ('".implode("','",
                                                                  $_SESSION['glpigroups'])."')) ";

                  case 'notequals' :
                     return " $link (`$table`.`id` NOT IN ('".implode("','",
                                                                      $_SESSION['glpigroups'])."')) ";

                  case 'under' :
                     $groups = $_SESSION['glpigroups'];
                     foreach ($_SESSION['glpigroups'] as $g) {
                        $groups += getSonsOf($inittable, $g);
                     }
                     $groups = array_unique($groups);
                     return " $link (`$table`.`id` IN ('".implode("','", $groups)."')) ";

                  case 'notunder' :
                     $groups = $_SESSION['glpigroups'];
                     foreach ($_SESSION['glpigroups'] as $g) {
                        $groups += getSonsOf($inittable, $g);
                     }
                     $groups = array_unique($groups);
                     return " $link (`$table`.`id` NOT IN ('".implode("','", $groups)."')) ";
               }
            }
            break;

         case "glpi_auth_tables.name" :
            $user_searchopt = self::getOptions('User');
            $tmplink        = 'OR';
            if ($nott) {
               $tmplink = 'AND';
            }
            return $link." (`glpi_authmails".$addtable."_".
                              self::computeComplexJoinID($user_searchopt[31]['joinparams']).$addmeta."`.`name`
                           $SEARCH
                           $tmplink `glpi_authldaps".$addtable."_".
                              self::computeComplexJoinID($user_searchopt[30]['joinparams']).$addmeta."`.`name`
                           $SEARCH ) ";

         case "glpi_ipaddresses.name" :
            $search  = ["/\&lt;/","/\&gt;/"];
            $replace = ["<",">"];
            $val     = preg_replace($search, $replace, $val);
            if (preg_match("/^\s*([<>])([=]*)[[:space:]]*([0-9\.]+)/", $val, $regs)) {
               if ($nott) {
                  if ($regs[1] == '<') {
                     $regs[1] = '>';
                  } else {
                     $regs[1] = '<';
                  }
               }
               $regs[1] .= $regs[2];
               return $link." (INET_ATON(`$table`.`$field`) ".$regs[1]." INET_ATON('".$regs[3]."')) ";
            }
            break;

         case "glpi_tickets.status" :
         case "glpi_problems.status" :
         case "glpi_changes.status" :
            if ($val == 'all') {
               return "";
            }
            $tocheck = [];
            if ($item = getItemForItemtype($itemtype)) {
               switch ($val) {
                  case 'process' :
                     $tocheck = $item->getProcessStatusArray();
                     break;

                  case 'notclosed' :
                     $tocheck = $item->getAllStatusArray();
                     foreach ($item->getClosedStatusArray() as $status) {
                        if (isset($tocheck[$status])) {
                           unset($tocheck[$status]);
                        }
                     }
                     $tocheck = array_keys($tocheck);
                     break;

                  case 'old' :
                     $tocheck = array_merge($item->getSolvedStatusArray(),
                                            $item->getClosedStatusArray());
                     break;

                  case 'notold' :
                     $tocheck = $item::getNotSolvedStatusArray();
                     break;
               }
            }

            if (count($tocheck) == 0) {
               $statuses = $item->getAllStatusArray();
               if (isset($statuses[$val])) {
                  $tocheck = [$val];
               }
            }

            if (count($tocheck)) {
               if ($nott) {
                  return $link." `$table`.`$field` NOT IN ('".implode("','", $tocheck)."')";
               }
               return $link." `$table`.`$field` IN ('".implode("','", $tocheck)."')";
            }
            break;

         case "glpi_tickets_tickets.tickets_id_1" :
            $tmplink = 'OR';
            $compare = '=';
            if ($nott) {
               $tmplink = 'AND';
               $compare = '<>';
            }
            $toadd2 = '';
            if ($nott
                && ($val != 'NULL') && ($val != 'null')) {
               $toadd2 = " OR `$table`.`$field` IS NULL";
            }

            return $link." (((`$table`.`tickets_id_1` $compare '$val'
                              $tmplink `$table`.`tickets_id_2` $compare '$val')
                             AND `glpi_tickets`.`id` <> '$val')
                            $toadd2)";

         case "glpi_tickets.priority" :
         case "glpi_tickets.impact" :
         case "glpi_tickets.urgency" :
         case "glpi_problems.priority" :
         case "glpi_problems.impact" :
         case "glpi_problems.urgency" :
         case "glpi_changes.priority" :
         case "glpi_changes.impact" :
         case "glpi_changes.urgency" :
         case "glpi_projects.priority" :
            if (is_numeric($val)) {
               if ($val > 0) {
                  $compare = ($nott ? '<>' : '=');
                  return $link." `$table`.`$field` $compare '$val'";
               }
               if ($val < 0) {
                  $compare = ($nott ? '<' : '>=');
                  return $link." `$table`.`$field` $compare '".abs($val)."'";
               }
               // Show all
               $compare = ($nott ? '<' : '>=');
               return $link." `$table`.`$field` $compare '0' ";
            }
            return "";

         case "glpi_tickets.global_validation" :
         case "glpi_ticketvalidations.status" :
            if ($val == 'all') {
               return "";
            }
            $tocheck = [];
            switch ($val) {
               case 'can' :
                  $tocheck = CommonITILValidation::getCanValidationStatusArray();
                  break;

               case 'all' :
                  $tocheck = CommonITILValidation::getAllValidationStatusArray();
                  break;

            }
            if (count($tocheck) == 0) {
               $tocheck = [$val];
            }
            if (count($tocheck)) {
               if ($nott) {
                  return $link." `$table`.`$field` NOT IN ('".implode("','", $tocheck)."')";
               }
               return $link." `$table`.`$field` IN ('".implode("','", $tocheck)."')";
            }
            break;

         case "glpi_notifications.event" :
            if (in_array($searchtype, ['equals', 'notequals']) && strpos($val, self::SHORTSEP)) {
               $not = 'notequals' === $searchtype ? 'NOT' : '';
               list($itemtype_val, $event_val) = explode(self::SHORTSEP, $val);
               return " $link $not(`$table`.`event` = '$event_val'
                               AND `$table`.`itemtype` = '$itemtype_val')";
            }
            break;

      }

      //// Default cases

      // Link with plugin tables
      if (preg_match("/^glpi_plugin_([a-z0-9]+)/", $inittable, $matches)) {
         if (count($matches) == 2) {
            $plug     = $matches[1];
            $out = Plugin::doOneHook(
               $plug,
               'addWhere',
               $link, $nott, $itemtype, $ID, $val, $searchtype
            );
            if (!empty($out)) {
               return $out;
            }
         }
      }

      $tocompute      = "`$table`.`$field`";
      $tocomputetrans = "`".$table."_trans`.`value`";
      if (isset($searchopt[$ID]["computation"])) {
         $tocompute = $searchopt[$ID]["computation"];
         $tocompute = str_replace($DB->quoteName('TABLE'), 'TABLE', $tocompute);
         $tocompute = str_replace("TABLE", $DB->quoteName("$table"), $tocompute);
      }

      // Preformat items
      if (isset($searchopt[$ID]["datatype"])) {
         switch ($searchopt[$ID]["datatype"]) {
            case "itemtypename" :
               if (in_array($searchtype, ['equals', 'notequals'])) {
                  return " $link (`$table`.`$field`".$SEARCH.') ';
               }
               break;

            case "itemlink" :
               if (in_array($searchtype, ['equals', 'notequals', 'under', 'notunder'])) {
                  return " $link (`$table`.`id`".$SEARCH.') ';
               }
               break;

            case "datetime" :
            case "date" :
            case "date_delay" :
               if ($searchopt[$ID]["datatype"] == 'datetime') {
                  // Specific search for datetime
                  if (in_array($searchtype, ['equals', 'notequals'])) {
                     $val = preg_replace("/:00$/", '', $val);
                     $val = '^'.$val;
                     if ($searchtype == 'notequals') {
                        $nott = !$nott;
                     }
                     return self::makeTextCriteria("`$table`.`$field`", $val, $nott, $link);
                  }
               }
               if ($searchtype == 'lessthan') {
                  $val = '<'.$val;
               }
               if ($searchtype == 'morethan') {
                  $val = '>'.$val;
               }
               if ($searchtype) {
                  $date_computation = $tocompute;
               }
               if (in_array($searchtype, ["contains", "notcontains"])) {
                  $date_computation = "CONVERT($date_computation USING utf8)";
               }
               $search_unit = ' MONTH ';
               if (isset($searchopt[$ID]['searchunit'])) {
                  $search_unit = $searchopt[$ID]['searchunit'];
               }
               if ($searchopt[$ID]["datatype"]=="date_delay") {
                  $delay_unit = ' MONTH ';
                  if (isset($searchopt[$ID]['delayunit'])) {
                     $delay_unit = $searchopt[$ID]['delayunit'];
                  }
                  $add_minus = '';
                  if (isset($searchopt[$ID]["datafields"][3])) {
                     $add_minus = "-`$table`.`".$searchopt[$ID]["datafields"][3]."`";
                  }
                  $date_computation = "ADDDATE(`$table`.".$searchopt[$ID]["datafields"][1].",
                                               INTERVAL (`$table`.".$searchopt[$ID]["datafields"][2]."
                                                         $add_minus)
                                               $delay_unit)";
               }
               if (in_array($searchtype, ['equals', 'notequals'])) {
                  return " $link ($date_computation ".$SEARCH.') ';
               }
               $search  = ["/\&lt;/","/\&gt;/"];
               $replace = ["<",">"];
               $val     = preg_replace($search, $replace, $val);
               if (preg_match("/^\s*([<>=]+)(.*)/", $val, $regs)) {
                  if (is_numeric($regs[2])) {
                     return $link." $date_computation ".$regs[1]."
                            ADDDATE(NOW(), INTERVAL ".$regs[2]." $search_unit) ";
                  }
                  // ELSE Reformat date if needed
                  $regs[2] = preg_replace('@(\d{1,2})(-|/)(\d{1,2})(-|/)(\d{4})@', '\5-\3-\1',
                                          $regs[2]);
                  if (preg_match('/[0-9]{2,4}-[0-9]{1,2}-[0-9]{1,2}/', $regs[2])) {
                     $ret = $link;
                     if ($nott) {
                        $ret .= " NOT(";
                     }
                     $ret .= " $date_computation {$regs[1]} '{$regs[2]}'";
                     if ($nott) {
                        $ret .= ")";
                     }
                     return $ret;
                  }
                  return "";
               }
               // ELSE standard search
               // Date format modification if needed
               $val = preg_replace('@(\d{1,2})(-|/)(\d{1,2})(-|/)(\d{4})@', '\5-\3-\1', $val);
               if ($date_computation) {
                  return self::makeTextCriteria($date_computation, $val, $nott, $link);
               }
               return '';

            case "right" :
               if ($searchtype == 'notequals') {
                  $nott = !$nott;
               }
               return $link. ($nott?' NOT':'')." ($tocompute & '$val') ";

            case "bool" :
               if (!is_numeric($val)) {
                  if (strcasecmp($val, __('No')) == 0) {
                     $val = 0;
                  } else if (strcasecmp($val, __('Yes')) == 0) {
                     $val = 1;
                  }
               }
               // No break here : use number comparaison case

            case "count" :
            case "number" :
            case "decimal" :
            case "timestamp" :
            case "progressbar" :
               $search  = ["/\&lt;/", "/\&gt;/"];
               $replace = ["<", ">"];
               $val     = preg_replace($search, $replace, $val);

               if (preg_match("/([<>])([=]*)[[:space:]]*([0-9]+)/", $val, $regs)) {
                  if (in_array($searchtype, ["notequals", "notcontains"])) {
                     $nott = !$nott;
                  }
                  if ($nott) {
                     if ($regs[1] == '<') {
                        $regs[1] = '>';
                     } else {
                        $regs[1] = '<';
                     }
                  }
                  $regs[1] .= $regs[2];
                  return $link." ($tocompute ".$regs[1]." ".$regs[3].") ";
               }

               if (is_numeric($val)) {
                  $numeric_val = floatval($val);

                  if (in_array($searchtype, ["notequals", "notcontains"])) {
                     $nott = !$nott;
                  }

                  if (isset($searchopt[$ID]["width"])) {
                     $ADD = "";
                     if ($nott
                         && ($val != 'NULL') && ($val != 'null')) {
                        $ADD = " OR $tocompute IS NULL";
                     }
                     if ($nott) {
                        return $link." ($tocompute < ".($numeric_val - $searchopt[$ID]["width"])."
                                        OR $tocompute > ".($numeric_val + $searchopt[$ID]["width"])."
                                        $ADD) ";
                     }
                     return $link." (($tocompute >= ".($numeric_val - $searchopt[$ID]["width"])."
                                      AND $tocompute <= ".($numeric_val + $searchopt[$ID]["width"]).")
                                     $ADD) ";
                  }
                  if (!$nott) {
                     return " $link ($tocompute = $numeric_val) ";
                  }
                  return " $link ($tocompute <> $numeric_val) ";
               }
               break;
         }
      }

      // Default case
      if (in_array($searchtype, ['equals', 'notequals','under', 'notunder'])) {

         if ((!isset($searchopt[$ID]['searchequalsonfield'])
              || !$searchopt[$ID]['searchequalsonfield'])
            && ($itemtype == 'AllAssets'
                || $table != $itemtype::getTable())) {
            $out = " $link (`$table`.`id`".$SEARCH;
         } else {
            $out = " $link (`$table`.`$field`".$SEARCH;
         }
         if ($searchtype == 'notequals') {
            $nott = !$nott;
         }
         // Add NULL if $val = 0 and not negative search
         // Or negative search on real value
         if ((!$nott && ($val == 0))
             || ($nott && ($val != 0))) {
            $out .= " OR `$table`.`id` IS NULL";
         }
         $out .= ')';
         return $out;
      }
      $transitemtype = getItemTypeForTable($inittable);
      if (Session::haveTranslations($transitemtype, $field)) {
         return " $link (".self::makeTextCriteria($tocompute, $val, $nott, '')."
                          OR ".self::makeTextCriteria($tocomputetrans, $val, $nott, '').")";
      }

      return self::makeTextCriteria($tocompute, $val, $nott, $link);
   }


   /**
    * Generic Function to add Default left join to a request
    *
    * @param string $itemtype             Reference item type
    * @param string $ref_table            Reference table
    * @param array &$already_link_tables  Array of tables already joined
    *
    * @return string Left join string
   **/
   static function addDefaultJoin($itemtype, $ref_table, array &$already_link_tables) {

      switch ($itemtype) {
         // No link
         case 'User' :
            return self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                     "glpi_profiles_users", "profiles_users_id", 0, 0,
                                     ['jointype' => 'child']);

         case 'Reminder' :
            return Reminder::addVisibilityJoins();

         case 'RSSFeed' :
            return RSSFeed::addVisibilityJoins();

         case 'ProjectTask' :
            // Same structure in addDefaultWhere
            $out  = '';
            $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                      "glpi_projects", "projects_id");
            $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                      "glpi_projecttaskteams", "projecttaskteams_id", 0, 0,
                                      ['jointype' => 'child']);
            return $out;

         case 'Project' :
            // Same structure in addDefaultWhere
            $out = '';
            if (!Session::haveRight("project", Project::READALL)) {
               $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                          "glpi_projectteams", "projectteams_id", 0, 0,
                                          ['jointype' => 'child']);
            }
            return $out;

         case 'Ticket' :
            // Same structure in addDefaultWhere
            $out = '';
            if (!Session::haveRight("ticket", Ticket::READALL)) {
               $searchopt = &self::getOptions($itemtype);

               // show mine : requester
               $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                         "glpi_tickets_users", "tickets_users_id", 0, 0,
                                         $searchopt[4]['joinparams']['beforejoin']['joinparams']);

               if (Session::haveRight("ticket", Ticket::READGROUP)) {
                  if (count($_SESSION['glpigroups'])) {
                     $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                               "glpi_groups_tickets", "groups_tickets_id", 0, 0,
                                               $searchopt[71]['joinparams']['beforejoin']
                                                         ['joinparams']);
                  }
               }

               // show mine : observer
               $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                         "glpi_tickets_users", "tickets_users_id", 0, 0,
                                         $searchopt[66]['joinparams']['beforejoin']['joinparams']);

               if (count($_SESSION['glpigroups'])) {
                  $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                            "glpi_groups_tickets", "groups_tickets_id", 0, 0,
                                            $searchopt[65]['joinparams']['beforejoin']['joinparams']);
               }

               if (Session::haveRight("ticket", Ticket::OWN)) { // Can own ticket : show assign to me
                  $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                            "glpi_tickets_users", "tickets_users_id", 0, 0,
                                            $searchopt[5]['joinparams']['beforejoin']['joinparams']);
               }

               if (Session::haveRightsOr("ticket", [Ticket::READMY, Ticket::READASSIGN])) { // show mine + assign to me
                  $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                            "glpi_tickets_users", "tickets_users_id", 0, 0,
                                            $searchopt[5]['joinparams']['beforejoin']['joinparams']);

                  if (count($_SESSION['glpigroups'])) {
                     $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                               "glpi_groups_tickets", "groups_tickets_id", 0, 0,
                                               $searchopt[8]['joinparams']['beforejoin']
                                                         ['joinparams']);
                  }
               }

               if (Session::haveRightsOr('ticketvalidation',
                                         [TicketValidation::VALIDATEINCIDENT,
                                               TicketValidation::VALIDATEREQUEST])) {
                  $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                            "glpi_ticketvalidations", "ticketvalidations_id", 0, 0,
                                            $searchopt[58]['joinparams']['beforejoin']['joinparams']);
               }
            }
            return $out;

         case 'Change' :
         case 'Problem' :
            if ($itemtype == 'Change') {
               $right       = 'change';
               $table       = 'changes';
               $groupetable = "glpi_changes_groups";
               $linkfield   = "changes_groups_id";
            } else if ($itemtype == 'Problem') {
               $right       = 'problem';
               $table       = 'problems';
               $groupetable = "glpi_groups_problems";
               $linkfield   = "groups_problems_id";
            }

            // Same structure in addDefaultWhere
            $out = '';
            if (!Session::haveRight("$right", $itemtype::READALL)) {
               $searchopt = &self::getOptions($itemtype);

               if (Session::haveRight("$right", $itemtype::READMY)) {
                  // show mine : requester
                  $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                            "glpi_".$table."_users", $table."_users_id", 0, 0,
                                            $searchopt[4]['joinparams']['beforejoin']['joinparams']);
                  if (count($_SESSION['glpigroups'])) {
                     $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                               $groupetable, $linkfield, 0, 0,
                                               $searchopt[71]['joinparams']['beforejoin']['joinparams']);
                  }

                  // show mine : observer
                  $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                            "glpi_".$table."_users", $table."_users_id", 0, 0,
                                            $searchopt[66]['joinparams']['beforejoin']['joinparams']);
                  if (count($_SESSION['glpigroups'])) {
                     $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                               $groupetable, $linkfield, 0, 0,
                                               $searchopt[65]['joinparams']['beforejoin']['joinparams']);
                  }

                  // show mine : assign
                  $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                            "glpi_".$table."_users", $table."_users_id", 0, 0,
                                            $searchopt[5]['joinparams']['beforejoin']['joinparams']);
                  if (count($_SESSION['glpigroups'])) {
                     $out .= self::addLeftJoin($itemtype, $ref_table, $already_link_tables,
                                               $groupetable, $linkfield, 0, 0,
                                               $searchopt[8]['joinparams']['beforejoin']['joinparams']);
                  }
               }
            }
            return $out;

         default :
            // Plugin can override core definition for its type
            if ($plug = isPluginItemType($itemtype)) {
               $plugin_name   = $plug['plugin'];
               $hook_function = 'plugin_' . strtolower($plugin_name) . '_addDefaultJoin';
               $hook_closure  = function () use ($hook_function, $itemtype, $ref_table, &$already_link_tables) {
                  if (is_callable($hook_function)) {
                     return $hook_function($itemtype, $ref_table, $already_link_tables);
                  }
               };
               $out = Plugin::doOneHook($plugin_name, $hook_closure);
               if (!empty($out)) {
                  return $out;
               }
            }

            return "";
      }
   }


   /**
    * Generic Function to add left join to a request
    *
    * @param string  $itemtype             Item type
    * @param string  $ref_table            Reference table
    * @param array   $already_link_tables  Array of tables already joined
    * @param string  $new_table            New table to join
    * @param string  $linkfield            Linkfield for LeftJoin
    * @param boolean $meta                 Is it a meta item ? (default 0)
    * @param integer $meta_type            Meta type table (default 0)
    * @param array   $joinparams           Array join parameters (condition / joinbefore...)
    * @param string  $field                Field to display (needed for translation join) (default '')
    *
    * @return string Left join string
   **/
   static function addLeftJoin($itemtype, $ref_table, array &$already_link_tables, $new_table,
                               $linkfield, $meta = 0, $meta_type = 0, $joinparams = [], $field = '') {

      // Rename table for meta left join
      $AS = "";
      $nt = $new_table;
      $cleannt    = $nt;

      // Virtual field no link
      if (strpos($linkfield, '_virtual') === 0) {
         return false;
      }

      $complexjoin = self::computeComplexJoinID($joinparams);

      $is_fkey_composite_on_self = getTableNameForForeignKeyField($linkfield) == $ref_table
         && $linkfield != getForeignKeyFieldForTable($ref_table);

      // Auto link
      if (($ref_table == $new_table)
          && empty($complexjoin)
          && !$is_fkey_composite_on_self) {
         $transitemtype = getItemTypeForTable($new_table);
         if (Session::haveTranslations($transitemtype, $field)) {
            $transAS            = $nt.'_trans';
            return self::joinDropdownTranslations(
               $transAS,
               $nt,
               $transitemtype,
               $field
            );
         }
         return "";
      }

      // Multiple link possibilies case
      if (!empty($linkfield) && ($linkfield != getForeignKeyFieldForTable($new_table))) {
         $nt .= "_".$linkfield;
         $AS  = " AS `$nt`";
      }

      if (!empty($complexjoin)) {
         $nt .= "_".$complexjoin;
         $AS  = " AS `$nt`";
      }

      $addmetanum = "";
      $rt         = $ref_table;
      $cleanrt    = $rt;
      if ($meta && $meta_type::getTable() != $new_table) {
         $addmetanum = "_".$meta_type;
         $AS         = " AS `$nt$addmetanum`";
         $nt         = $nt.$addmetanum;
      }

      // Do not take into account standard linkfield
      $tocheck = $nt.".".$linkfield;
      if ($linkfield == getForeignKeyFieldForTable($new_table)) {
         $tocheck = $nt;
      }

      if (in_array($tocheck, $already_link_tables)) {
         return "";
      }
      array_push($already_link_tables, $tocheck);

      $specific_leftjoin = '';

      // Plugin can override core definition for its type
      if ($plug = isPluginItemType($itemtype)) {
         $plugin_name   = $plug['plugin'];
         $hook_function = 'plugin_' . strtolower($plugin_name) . '_addLeftJoin';
         $hook_closure  = function () use ($hook_function, $itemtype, $ref_table, $new_table, $linkfield, &$already_link_tables) {
            if (is_callable($hook_function)) {
               return $hook_function($itemtype, $ref_table, $new_table, $linkfield, $already_link_tables);
            }
         };
         $specific_leftjoin = Plugin::doOneHook($plugin_name, $hook_closure);
      }

      // Link with plugin tables : need to know left join structure
      if (empty($specific_leftjoin)
          && preg_match("/^glpi_plugin_([a-z0-9]+)/", $new_table, $matches)) {
         if (count($matches) == 2) {
            $plugin_name   = $matches[1];
            $hook_function = 'plugin_' . strtolower($plugin_name) . '_addLeftJoin';
            $hook_closure  = function () use ($hook_function, $itemtype, $ref_table, $new_table, $linkfield, &$already_link_tables) {
               if (is_callable($hook_function)) {
                  return $hook_function($itemtype, $ref_table, $new_table, $linkfield, $already_link_tables);
               }
            };
            $specific_leftjoin = Plugin::doOneHook($plugin_name, $hook_closure);
         }
      }
      if (!empty($linkfield)) {
         $before = '';

         if (isset($joinparams['beforejoin']) && is_array($joinparams['beforejoin'])) {

            if (isset($joinparams['beforejoin']['table'])) {
               $joinparams['beforejoin'] = [$joinparams['beforejoin']];
            }

            foreach ($joinparams['beforejoin'] as $tab) {
               if (isset($tab['table'])) {
                  $intertable = $tab['table'];
                  if (isset($tab['linkfield'])) {
                     $interlinkfield = $tab['linkfield'];
                  } else {
                     $interlinkfield = getForeignKeyFieldForTable($intertable);
                  }

                  $interjoinparams = [];
                  if (isset($tab['joinparams'])) {
                     $interjoinparams = $tab['joinparams'];
                  }
                  $before .= self::addLeftJoin($itemtype, $rt, $already_link_tables, $intertable,
                                               $interlinkfield, $meta, $meta_type, $interjoinparams);
               }

               // No direct link with the previous joins
               if (!isset($tab['joinparams']['nolink']) || !$tab['joinparams']['nolink']) {
                  $cleanrt     = $intertable;
                  $complexjoin = self::computeComplexJoinID($interjoinparams);
                  if (!empty($complexjoin)) {
                     $intertable .= "_".$complexjoin;
                  }
                  if ($meta && $meta_type::getTable() != $cleanrt) {
                     $intertable .= "_".$meta_type;
                  }
                  $rt = $intertable;
               }
            }
         }

         $addcondition = '';
         if (isset($joinparams['condition'])) {
            $condition = $joinparams['condition'];
            if (is_array($condition)) {
               $it = new DBmysqlIterator(null);
               $condition = $it->analyseCrit($condition);
            }
            $from         = ["`REFTABLE`", "REFTABLE", "`NEWTABLE`", "NEWTABLE"];
            $to           = ["`$rt`", "`$rt`", "`$nt`", "`$nt`"];
            $addcondition = str_replace($from, $to, $condition);
            $addcondition = $addcondition." ";
         }

         if (!isset($joinparams['jointype'])) {
            $joinparams['jointype'] = 'standard';
         }

         if (empty($specific_leftjoin)) {
            switch ($new_table) {
               // No link
               case "glpi_auth_tables" :
                     $user_searchopt     = self::getOptions('User');

                     $specific_leftjoin  = self::addLeftJoin($itemtype, $rt, $already_link_tables,
                                                             "glpi_authldaps", 'auths_id', 0, 0,
                                                             $user_searchopt[30]['joinparams']);
                     $specific_leftjoin .= self::addLeftJoin($itemtype, $rt, $already_link_tables,
                                                             "glpi_authmails", 'auths_id', 0, 0,
                                                             $user_searchopt[31]['joinparams']);
                     break;
            }
         }

         if (empty($specific_leftjoin)) {
            switch ($joinparams['jointype']) {
               case 'child' :
                  $linkfield = getForeignKeyFieldForTable($cleanrt);
                  if (isset($joinparams['linkfield'])) {
                     $linkfield = $joinparams['linkfield'];
                  }

                  // Child join
                  $specific_leftjoin = " LEFT JOIN `$new_table` $AS
                                             ON (`$rt`.`id` = `$nt`.`$linkfield`
                                                 $addcondition)";
                  break;

               case 'item_item' :
                  // Item_Item join
                  $specific_leftjoin = " LEFT JOIN `$new_table` $AS
                                          ON ((`$rt`.`id`
                                                = `$nt`.`".getForeignKeyFieldForTable($cleanrt)."_1`
                                               OR `$rt`.`id`
                                                 = `$nt`.`".getForeignKeyFieldForTable($cleanrt)."_2`)
                                              $addcondition)";
                  break;

               case 'item_item_revert' :
                  // Item_Item join reverting previous item_item
                  $specific_leftjoin = " LEFT JOIN `$new_table` $AS
                                          ON ((`$nt`.`id`
                                                = `$rt`.`".getForeignKeyFieldForTable($cleannt)."_1`
                                               OR `$nt`.`id`
                                                 = `$rt`.`".getForeignKeyFieldForTable($cleannt)."_2`)
                                              $addcondition)";
                  break;

               case "mainitemtype_mainitem" :
                  $addmain = 'main';

               case "itemtype_item" :
                  if (!isset($addmain)) {
                     $addmain = '';
                  }
                  $used_itemtype = $itemtype;
                  if (isset($joinparams['specific_itemtype'])
                      && !empty($joinparams['specific_itemtype'])) {
                     $used_itemtype = $joinparams['specific_itemtype'];
                  }
                  // Itemtype join
                  $specific_leftjoin = " LEFT JOIN `$new_table` $AS
                                          ON (`$rt`.`id` = `$nt`.`".$addmain."items_id`
                                              AND `$nt`.`".$addmain."itemtype` = '$used_itemtype'
                                              $addcondition) ";
                  break;

               case "itemtype_item_revert" :
                  if (!isset($addmain)) {
                     $addmain = '';
                  }
                  $used_itemtype = $itemtype;
                  if (isset($joinparams['specific_itemtype'])
                      && !empty($joinparams['specific_itemtype'])) {
                     $used_itemtype = $joinparams['specific_itemtype'];
                  }
                  // Itemtype join
                  $specific_leftjoin = " LEFT JOIN `$new_table` $AS
                                          ON (`$nt`.`id` = `$rt`.`".$addmain."items_id`
                                              AND `$rt`.`".$addmain."itemtype` = '$used_itemtype'
                                              $addcondition) ";
                  break;

               case "itemtypeonly" :
                  $used_itemtype = $itemtype;
                  if (isset($joinparams['specific_itemtype'])
                      && !empty($joinparams['specific_itemtype'])) {
                     $used_itemtype = $joinparams['specific_itemtype'];
                  }
                  // Itemtype join
                  $specific_leftjoin = " LEFT JOIN `$new_table` $AS
                                          ON (`$nt`.`itemtype` = '$used_itemtype'
                                              $addcondition) ";
                  break;

               default :
                  // Standard join
                  $specific_leftjoin = "LEFT JOIN `$new_table` $AS
                                          ON (`$rt`.`$linkfield` = `$nt`.`id`
                                              $addcondition)";
                  $transitemtype = getItemTypeForTable($new_table);
                  if (Session::haveTranslations($transitemtype, $field)) {
                     $transAS            = $nt.'_trans';
                     $specific_leftjoin .= self::joinDropdownTranslations(
                        $transAS,
                        $nt,
                        $transitemtype,
                        $field
                     );
                  }
                  break;
            }
         }
         return $before.$specific_leftjoin;
      }
   }


   /**
    * Generic Function to add left join for meta items
    *
    * @param string $from_type             Reference item type ID
    * @param string $to_type               Item type to add
    * @param array  $already_link_tables2  Array of tables already joined
    *
    * @return string Meta Left join string
   **/
   static function addMetaLeftJoin($from_type, $to_type, array &$already_link_tables2,
                                   $joinparams = []) {
      global $CFG_GLPI;

      $from_referencetype = self::getMetaReferenceItemtype($from_type);

      $LINK = " LEFT JOIN ";

      $from_table = $from_type::getTable();
      $from_fk    = getForeignKeyFieldForTable($from_table);
      $to_table   = $to_type::getTable();
      $to_fk      = getForeignKeyFieldForTable($to_table);

      $to_obj        = getItemForItemtype($to_type);
      $to_entity_restrict = $to_obj->isField('entities_id')? getEntitiesRestrictRequest('AND', $to_table) : '';

      $complexjoin = self::computeComplexJoinID($joinparams);
      $alias_suffix = ($complexjoin != '' ? '_' . $complexjoin : '') . '_' . $to_type;

      $JOIN = "";

      // Specific JOIN
      if ($from_referencetype === 'Software' && in_array($to_type, $CFG_GLPI['software_types'])) {
         // From Software to software_types
         $softwareversions_table = "glpi_softwareversions{$alias_suffix}";
         if (!in_array($softwareversions_table, $already_link_tables2)) {
            array_push($already_link_tables2, $softwareversions_table);
            $JOIN .= "$LINK `glpi_softwareversions` AS `$softwareversions_table`
                         ON (`$softwareversions_table`.`softwares_id` = `$from_table`.`id`) ";
         }
         $items_softwareversions_table = "glpi_items_softwareversions_{$alias_suffix}";
         if (!in_array($items_softwareversions_table, $already_link_tables2)) {
            array_push($already_link_tables2, $items_softwareversions_table);
            $JOIN .= "$LINK `glpi_items_softwareversions` AS `$items_softwareversions_table`
                         ON (`$items_softwareversions_table`.`softwareversions_id` = `$softwareversions_table`.`id`
                             AND `$items_softwareversions_table`.`itemtype` = '$to_type'
                             AND `$items_softwareversions_table`.`is_deleted` = 0) ";
         }
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$items_softwareversions_table`.`items_id` = `$to_table`.`id`
                             AND `$items_softwareversions_table`.`itemtype` = '$to_type'
                             $to_entity_restrict) ";
         }
         return $JOIN;
      }

      if ($to_type === 'Software' && in_array($from_referencetype, $CFG_GLPI['software_types'])) {
         // From software_types to Software
         $items_softwareversions_table = "glpi_items_softwareversions{$alias_suffix}";
         if (!in_array($items_softwareversions_table, $already_link_tables2)) {
            array_push($already_link_tables2, $items_softwareversions_table);
            $JOIN .= "$LINK `glpi_items_softwareversions` AS `$items_softwareversions_table`
                         ON (`$items_softwareversions_table`.`items_id` = `$from_table`.`id`
                             AND `$items_softwareversions_table`.`itemtype` = '$from_type'
                             AND `$items_softwareversions_table`.`is_deleted` = 0) ";
         }
         $softwareversions_table = "glpi_softwareversions{$alias_suffix}";
         if (!in_array($softwareversions_table, $already_link_tables2)) {
            array_push($already_link_tables2, $softwareversions_table);
            $JOIN .= "$LINK `glpi_softwareversions` AS `$softwareversions_table`
                         ON (`$items_softwareversions_table`.`softwareversions_id` = `$softwareversions_table`.`id`) ";
         }
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$softwareversions_table`.`softwares_id` = `$to_table`.`id`) ";
         }
         $softwarelicenses_table = "glpi_softwarelicenses{$alias_suffix}";
         if (!in_array($softwarelicenses_table, $already_link_tables2)) {
            array_push($already_link_tables2, $softwarelicenses_table);
            $JOIN .= "$LINK `glpi_softwarelicenses` AS `$softwarelicenses_table`
                        ON ($to_table.`id` = `$softwarelicenses_table`.`softwares_id`"
                          . getEntitiesRestrictRequest(' AND', $softwarelicenses_table, '', '', true).") ";
         }
         return $JOIN;
      }

      if ($from_referencetype === 'Budget' && in_array($to_type, $CFG_GLPI['infocom_types'])) {
         // From Budget to infocom_types
         $infocom_alias = "glpi_infocoms{$alias_suffix}";
         if (!in_array($infocom_alias, $already_link_tables2)) {
            array_push($already_link_tables2, $infocom_alias);
            $JOIN .= "$LINK `glpi_infocoms` AS `$infocom_alias`
                         ON (`$from_table`.`id` = `$infocom_alias`.`budgets_id`) ";
         }
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$to_table`.`id` = `$infocom_alias`.`items_id`
                             AND `$infocom_alias`.`itemtype` = '$to_type'
                             $to_entity_restrict) ";
         }
         return $JOIN;
      }

      if ($to_type === 'Budget' && in_array($from_referencetype, $CFG_GLPI['infocom_types'])) {
         // From infocom_types to Budget
         $infocom_alias = "glpi_infocoms{$alias_suffix}";
         if (!in_array($infocom_alias, $already_link_tables2)) {
            array_push($already_link_tables2, $infocom_alias);
            $JOIN .= "$LINK `glpi_infocoms` AS `$infocom_alias`
                         ON (`$from_table`.`id` = `$infocom_alias`.`items_id`
                             AND `$infocom_alias`.`itemtype` = '$from_type') ";
         }
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$infocom_alias`.`$to_fk` = `$to_table`.`id`
                             $to_entity_restrict) ";
         }
         return $JOIN;
      }

      if ($from_referencetype === 'Reservation' && in_array($to_type, $CFG_GLPI['reservation_types'])) {
         // From Reservation to reservation_types
         $reservationitems_alias = "glpi_reservationitems{$alias_suffix}";
         if (!in_array($reservationitems_alias, $already_link_tables2)) {
            array_push($already_link_tables2, $reservationitems_alias);
            $JOIN .= "$LINK `glpi_reservationitems` AS `$reservationitems_alias`
                         ON (`$from_table`.`reservationitems_id` = `$reservationitems_alias`.`id`) ";
         }
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$to_table`.`id` = `$reservationitems_alias`.`items_id`
                             AND `$reservationitems_alias`.`itemtype` = '$to_type'
                             $to_entity_restrict) ";
         }
         return $JOIN;
      }

      if ($to_type === 'Reservation' && in_array($from_referencetype, $CFG_GLPI['reservation_types'])) {
         // From reservation_types to Reservation
         $reservationitems_alias = "glpi_reservationitems{$alias_suffix}";
         if (!in_array($infocom_alias, $already_link_tables2)) {
            array_push($already_link_tables2, $infocom_alias);
            $JOIN .= "$LINK `glpi_reservationitems` AS `$reservationitems_alias`
                         ON (`$from_table`.`id` = `$reservationitems_alias`.`items_id`
                             AND `$reservationitems_alias`.`itemtype` = '$from_type') ";
         }
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$reservationitems_alias`.`id` = `$to_table`.`reservationitems_id`
                             $to_entity_restrict) ";
         }
         return $JOIN;
      }

      // Generic JOIN
      $from_obj      = getItemForItemtype($from_referencetype);
      $to_obj        = getItemForItemtype($to_type);
      $from_item_obj = getItemForItemtype($from_referencetype . '_Item');
      if (!$from_item_obj) {
         $from_item_obj = getItemForItemtype('Item_' . $from_referencetype);
      }
      $to_item_obj   = getItemForItemtype($to_type . '_Item');
      if (!$to_item_obj) {
         $to_item_obj = getItemForItemtype('Item_' . $to_type);
      }
      if ($from_obj && $from_obj->isField($to_fk)) {
         // $from_table has a foreign key corresponding to $to_table
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$from_table`.`$to_fk` = `$to_table`.`id`
                             $to_entity_restrict) ";
         }
      } else if ($to_obj && $to_obj->isField($from_fk)) {
         // $to_table has a foreign key corresponding to $from_table
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$from_table`.`id` = `$to_table`.`$from_fk`
                             $to_entity_restrict) ";
         }
      } else if ($from_obj && $from_obj->isField('itemtype') && $from_obj->isField('items_id')) {
         // $from_table has items_id/itemtype fields
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$from_table`.`items_id` = `$to_table`.`id`
                             AND `$from_table`.`itemtype` = '$to_type'
                             $to_entity_restrict) ";
         }
      } else if ($to_obj && $to_obj->isField('itemtype') && $to_obj->isField('items_id')) {
         // $to_table has items_id/itemtype fields
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$from_table`.`id` = `$to_table`.`items_id`
                             AND `$to_table`.`itemtype` = '$from_type'
                             $to_entity_restrict) ";
         }
      } else if ($from_item_obj && $from_item_obj->isField($from_fk)) {
         // glpi_$from_items table exists and has a foreign key corresponding to $to_table
         $items_table = $from_item_obj::getTable();
         $items_table_alias = $items_table . $alias_suffix;
         if (!in_array($items_table_alias, $already_link_tables2)) {
            array_push($already_link_tables2, $items_table_alias);
            $deleted = $from_item_obj->isField('is_deleted') ? "AND `$items_table_alias`.`is_deleted` = 0" : "";
            $JOIN .= "$LINK `$items_table` AS `$items_table_alias`
                         ON (`$items_table_alias`.`$from_fk` = `$from_table`.`id`
                             AND `$items_table_alias`.`itemtype` = '$to_type'
                             $deleted)";
         }
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$items_table_alias`.`items_id` = `$to_table`.`id`
                             $to_entity_restrict) ";
         }
      } else if ($to_item_obj && $to_item_obj->isField($to_fk)) {
         // glpi_$to_items table exists and has a foreign key corresponding to $from_table
         $items_table = $to_item_obj::getTable();
         $items_table_alias = $items_table . $alias_suffix;
         if (!in_array($items_table_alias, $already_link_tables2)) {
            array_push($already_link_tables2, $items_table_alias);
            $deleted = $to_item_obj->isField('is_deleted') ? "AND `$items_table_alias`.`is_deleted` = 0" : "";
            $JOIN .= "$LINK `$items_table` AS `$items_table_alias`
                         ON (`$items_table_alias`.`items_id` = `$from_table`.`id`
                             AND `$items_table_alias`.`itemtype` = '$from_type'
                             $deleted)";
         }
         if (!in_array($to_table, $already_link_tables2)) {
            array_push($already_link_tables2, $to_table);
            $JOIN .= "$LINK `$to_table`
                         ON (`$items_table_alias`.`$to_fk` = `$to_table`.`id`
                             $to_entity_restrict) ";
         }
      }

      return $JOIN;

   }


   /**
    * Generic Function to display Items
    *
    * @since 9.4: $num param has been dropped
    *
    * @param string  $itemtype item type
    * @param integer $ID       ID of the SEARCH_OPTION item
    * @param array   $data     array retrieved data array
    *
    * @return string String to print
   **/
   static function displayConfigItem($itemtype, $ID, $data = []) {

      $searchopt  = &self::getOptions($itemtype);

      $table      = $searchopt[$ID]["table"];
      $field      = $searchopt[$ID]["field"];

      // Plugin can override core definition for its type
      if ($plug = isPluginItemType($itemtype)) {
         $out = Plugin::doOneHook(
            $plug['plugin'],
            'displayConfigItem',
            $itemtype, $ID, $data, "{$itemtype}_{$ID}"
         );
         if (!empty($out)) {
            return $out;
         }
      }

      $out = "";
      $NAME = "{$itemtype}_{$ID}";

      switch ($table.".".$field) {
         case "glpi_tickets.time_to_resolve" :
         case "glpi_tickets.internal_time_to_resolve" :
         case "glpi_problems.time_to_resolve" :
         case "glpi_changes.time_to_resolve" :
         case "glpi_tickets.time_to_own" :
         case "glpi_tickets.internal_time_to_own" :
            if (!in_array($ID, [151, 158, 181, 186])
                && !empty($data[$NAME][0]['name'])
                && ($data[$NAME][0]['status'] != CommonITILObject::WAITING)
                && ($data[$NAME][0]['name'] < $_SESSION['glpi_currenttime'])) {
               $out = " style=\"background-color: #cf9b9b\" ";
            }
            break;

         case "glpi_projectstates.color" :
            $out = " style=\"background-color:".$data[$NAME][0]['name'].";\" ";
            break;

         case "glpi_projectstates.name" :
            if (array_key_exists('color', $data[$NAME][0])) {
               $out = " style=\"background-color:".$data[$NAME][0]['color'].";\" ";
            }
            break;
      }

      return $out;
   }


   /**
    * Generic Function to display Items
    *
    * @since 9.4: $num param has been dropped
    *
    * @param string  $itemtype        item type
    * @param integer $ID              ID of the SEARCH_OPTION item
    * @param array   $data            array containing data results
    * @param boolean $meta            is a meta item ? (default 0)
    * @param array   $addobjectparams array added parameters for union search
    * @param string  $orig_itemtype   Original itemtype, used for union_search_type
    *
    * @return string String to print
   **/
   static function giveItem($itemtype, $ID, array $data, $meta = 0,
                            array $addobjectparams = [], $orig_itemtype = null) {
      global $CFG_GLPI;

      $searchopt = &self::getOptions($itemtype);
      if ($itemtype == 'AllAssets' || isset($CFG_GLPI["union_search_type"][$itemtype])
          && ($CFG_GLPI["union_search_type"][$itemtype] == $searchopt[$ID]["table"])) {

         $oparams = [];
         if (isset($searchopt[$ID]['addobjectparams'])
             && $searchopt[$ID]['addobjectparams']) {
            $oparams = $searchopt[$ID]['addobjectparams'];
         }

         // Search option may not exists in subtype
         // This is the case for "Inventory number" for a Software listed from ReservationItem search
         $subtype_so = &self::getOptions($data["TYPE"]);
         if (!array_key_exists($ID, $subtype_so)) {
            return '';
         }

         return self::giveItem($data["TYPE"], $ID, $data, $meta, $oparams, $itemtype);
      }
      $so = $searchopt[$ID];
      $orig_id = $ID;
      $ID = ($orig_itemtype !== null ? $orig_itemtype : $itemtype) . '_' . $ID;

      if (count($addobjectparams)) {
         $so = array_merge($so, $addobjectparams);
      }
      // Plugin can override core definition for its type
      if ($plug = isPluginItemType($itemtype)) {
         $out = Plugin::doOneHook(
            $plug['plugin'],
            'giveItem',
            $itemtype, $orig_id, $data, $ID
         );
         if (!empty($out)) {
            return $out;
         }
      }

      if (isset($so["table"])) {
         $table     = $so["table"];
         $field     = $so["field"];
         $linkfield = $so["linkfield"];

         /// TODO try to clean all specific cases using SpecificToDisplay

         switch ($table.'.'.$field) {
            case "glpi_users.name" :
               if ($itemtype == 'Ticket'
                  && Session::getCurrentInterface() == 'helpdesk'
                  && $orig_id == 5
                  && Entity::getUsedConfig(
                     'anonymize_support_agents',
                     $itemtype::getById($data['id'])->getEntityId()
                  )
               ) {
                  return __("Helpdesk");
               }

               // USER search case
               if (($itemtype != 'User')
                   && isset($so["forcegroupby"]) && $so["forcegroupby"]) {
                  $out           = "";
                  $count_display = 0;
                  $added         = [];

                  $showuserlink = 0;
                  if (Session::haveRight('user', READ)) {
                     $showuserlink = 1;
                  }

                  for ($k=0; $k<$data[$ID]['count']; $k++) {

                     if ((isset($data[$ID][$k]['name']) && ($data[$ID][$k]['name'] > 0))
                         || (isset($data[$ID][$k][2]) && ($data[$ID][$k][2] != ''))) {
                        if ($count_display) {
                           $out .= self::LBBR;
                        }

                        if ($itemtype == 'Ticket') {
                           if (isset($data[$ID][$k]['name'])
                                 && $data[$ID][$k]['name'] > 0) {
                              $userdata = getUserName($data[$ID][$k]['name'], 2);
                              $tooltip  = "";
                              if (Session::haveRight('user', READ)) {
                                 $tooltip = Html::showToolTip($userdata["comment"],
                                                              ['link'    => $userdata["link"],
                                                                    'display' => false]);
                              }
                              $out .= sprintf(__('%1$s %2$s'), $userdata['name'], $tooltip);
                              $count_display++;
                           }
                        } else {
                           $out .= getUserName($data[$ID][$k]['name'], $showuserlink);
                           $count_display++;
                        }

                        // Manage alternative_email for tickets_users
                        if (($itemtype == 'Ticket')
                            && isset($data[$ID][$k][2])) {

                           $split = explode(self::LONGSEP, $data[$ID][$k][2]);
                           for ($l=0; $l<count($split); $l++) {
                              $split2 = explode(" ", $split[$l]);
                              if ((count($split2) == 2) && ($split2[0] == 0) && !empty($split2[1])) {
                                 if ($count_display) {
                                    $out .= self::LBBR;
                                 }
                                 $count_display++;
                                 $out .= "<a href='mailto:".$split2[1]."'>".$split2[1]."</a>";
                              }
                           }
                        }
                     }
                  }
                  return $out;
               }
               if ($itemtype != 'User') {
                  $toadd = '';
                  if (($itemtype == 'Ticket')
                      && ($data[$ID][0]['id'] > 0)) {
                     $userdata = getUserName($data[$ID][0]['id'], 2);
                     $toadd    = Html::showToolTip($userdata["comment"],
                                                   ['link'    => $userdata["link"],
                                                         'display' => false]);
                  }
                  $usernameformat = formatUserName($data[$ID][0]['id'], $data[$ID][0]['name'],
                                                   $data[$ID][0]['realname'],
                                                   $data[$ID][0]['firstname'], 1);
                  return sprintf(__('%1$s %2$s'), $usernameformat, $toadd);
               }
               break;

            case "glpi_profiles.name" :
               if (($itemtype == 'User')
                   && ($orig_id == 20)) {
                  $out           = "";

                  $count_display = 0;
                  $added         = [];
                  for ($k=0; $k<$data[$ID]['count']; $k++) {
                     if (strlen(trim($data[$ID][$k]['name'])) > 0
                         && !in_array($data[$ID][$k]['name']."-".$data[$ID][$k]['entities_id'],
                                      $added)) {
                        $text = sprintf(__('%1$s - %2$s'), $data[$ID][$k]['name'],
                                        Dropdown::getDropdownName('glpi_entities',
                                                                  $data[$ID][$k]['entities_id']));
                        $comp = '';
                        if ($data[$ID][$k]['is_recursive']) {
                           $comp = __('R');
                           if ($data[$ID][$k]['is_dynamic']) {
                              $comp = sprintf(__('%1$s%2$s'), $comp, ", ");
                           }
                        }
                        if ($data[$ID][$k]['is_dynamic']) {
                           $comp = sprintf(__('%1$s%2$s'), $comp, __('D'));
                        }
                        if (!empty($comp)) {
                           $text = sprintf(__('%1$s %2$s'), $text, "(".$comp.")");
                        }
                        if ($count_display) {
                           $out .= self::LBBR;
                        }
                        $count_display++;
                        $out     .= $text;
                        $added[]  = $data[$ID][$k]['name']."-".$data[$ID][$k]['entities_id'];
                     }
                  }
                  return $out;
               }
               break;

            case "glpi_entities.completename" :
               if ($itemtype == 'User') {

                  $out           = "";
                  $added         = [];
                  $count_display = 0;
                  for ($k=0; $k<$data[$ID]['count']; $k++) {
                     if (isset($data[$ID][$k]['name'])
                         && (strlen(trim($data[$ID][$k]['name'])) > 0)
                         && !in_array($data[$ID][$k]['name']."-".$data[$ID][$k]['profiles_id'],
                                      $added)) {
                        $text = sprintf(__('%1$s - %2$s'), $data[$ID][$k]['name'],
                                        Dropdown::getDropdownName('glpi_profiles',
                                                                  $data[$ID][$k]['profiles_id']));
                        $comp = '';
                        if ($data[$ID][$k]['is_recursive']) {
                           $comp = __('R');
                           if ($data[$ID][$k]['is_dynamic']) {
                              $comp = sprintf(__('%1$s%2$s'), $comp, ", ");
                           }
                        }
                        if ($data[$ID][$k]['is_dynamic']) {
                           $comp = sprintf(__('%1$s%2$s'), $comp, __('D'));
                        }
                        if (!empty($comp)) {
                           $text = sprintf(__('%1$s %2$s'), $text, "(".$comp.")");
                        }
                        if ($count_display) {
                           $out .= self::LBBR;
                        }
                        $count_display++;
                        $out    .= $text;
                        $added[] = $data[$ID][$k]['name']."-".$data[$ID][$k]['profiles_id'];
                     }
                  }
                  return $out;
               }
               break;

            case "glpi_documenttypes.icon" :
               if (!empty($data[$ID][0]['name'])) {
                  return "<img class='middle' alt='' src='".$CFG_GLPI["typedoc_icon_dir"]."/".
                           $data[$ID][0]['name']."'>";
               }
               return "&nbsp;";

            case "glpi_documents.filename" :
               $doc = new Document();
               if ($doc->getFromDB($data['id'])) {
                  return $doc->getDownloadLink();
               }
               return NOT_AVAILABLE;

            case "glpi_tickets_tickets.tickets_id_1" :
               $out        = "";
               $displayed  = [];
               for ($k=0; $k<$data[$ID]['count']; $k++) {

                  $linkid = ($data[$ID][$k]['tickets_id_2'] == $data['id'])
                                 ? $data[$ID][$k]['name']
                                 : $data[$ID][$k]['tickets_id_2'];
                  if (($linkid > 0) && !isset($displayed[$linkid])) {
                     $text  = "<a ";
                     $text .= "href=\"".Ticket::getFormURLWithID($linkid)."\">";
                     $text .= Dropdown::getDropdownName('glpi_tickets', $linkid)."</a>";
                     if (count($displayed)) {
                        $out .= self::LBBR;
                     }
                     $displayed[$linkid] = $linkid;
                     $out               .= $text;
                  }
               }
               return $out;

            case "glpi_problems.id" :
               if ($so["datatype"] == 'count') {
                  if (($data[$ID][0]['name'] > 0)
                      && Session::haveRight("problem", Problem::READALL)) {
                     if ($itemtype == 'ITILCategory') {
                        $options['criteria'][0]['field']      = 7;
                        $options['criteria'][0]['searchtype'] = 'equals';
                        $options['criteria'][0]['value']      = $data['id'];
                        $options['criteria'][0]['link']       = 'AND';
                     } else {
                        $options['criteria'][0]['field']       = 12;
                        $options['criteria'][0]['searchtype']  = 'equals';
                        $options['criteria'][0]['value']       = 'all';
                        $options['criteria'][0]['link']        = 'AND';

                        $options['metacriteria'][0]['itemtype']   = $itemtype;
                        $options['metacriteria'][0]['field']      = self::getOptionNumber($itemtype,
                              'name');
                        $options['metacriteria'][0]['searchtype'] = 'equals';
                        $options['metacriteria'][0]['value']      = $data['id'];
                        $options['metacriteria'][0]['link']       = 'AND';
                     }

                     $options['reset'] = 'reset';

                     $out  = "<a id='problem$itemtype".$data['id']."' ";
                     $out .= "href=\"".$CFG_GLPI["root_doc"]."/front/problem.php?".
                              Toolbox::append_params($options, '&amp;')."\">";
                     $out .= $data[$ID][0]['name']."</a>";
                     return $out;
                  }
               }
               break;

            case "glpi_tickets.id" :
               if ($so["datatype"] == 'count') {
                  if (($data[$ID][0]['name'] > 0)
                      && Session::haveRight("ticket", Ticket::READALL)) {

                     if ($itemtype == 'User') {
                        // Requester
                        if ($ID == 'User_60') {
                           $options['criteria'][0]['field']      = 4;
                           $options['criteria'][0]['searchtype']= 'equals';
                           $options['criteria'][0]['value']      = $data['id'];
                           $options['criteria'][0]['link']       = 'AND';
                        }

                        // Writer
                        if ($ID == 'User_61') {
                           $options['criteria'][0]['field']      = 22;
                           $options['criteria'][0]['searchtype']= 'equals';
                           $options['criteria'][0]['value']      = $data['id'];
                           $options['criteria'][0]['link']       = 'AND';
                        }
                        // Assign
                        if ($ID == 'User_64') {
                           $options['criteria'][0]['field']      = 5;
                           $options['criteria'][0]['searchtype']= 'equals';
                           $options['criteria'][0]['value']      = $data['id'];
                           $options['criteria'][0]['link']       = 'AND';
                        }
                     } else if ($itemtype == 'ITILCategory') {
                        $options['criteria'][0]['field']      = 7;
                        $options['criteria'][0]['searchtype'] = 'equals';
                        $options['criteria'][0]['value']      = $data['id'];
                        $options['criteria'][0]['link']       = 'AND';

                     } else {
                        $options['criteria'][0]['field']       = 12;
                        $options['criteria'][0]['searchtype']  = 'equals';
                        $options['criteria'][0]['value']       = 'all';
                        $options['criteria'][0]['link']        = 'AND';

                        $options['metacriteria'][0]['itemtype']   = $itemtype;
                        $options['metacriteria'][0]['field']      = self::getOptionNumber($itemtype,
                                                                                          'name');
                        $options['metacriteria'][0]['searchtype'] = 'equals';
                        $options['metacriteria'][0]['value']      = $data['id'];
                        $options['metacriteria'][0]['link']       = 'AND';
                     }

                     $options['reset'] = 'reset';

                     $out  = "<a id='ticket$itemtype".$data['id']."' ";
                     $out .= "href=\"".$CFG_GLPI["root_doc"]."/front/ticket.php?".
                              Toolbox::append_params($options, '&amp;')."\">";
                     $out .= $data[$ID][0]['name']."</a>";
                     return $out;
                  }
               }
               break;

            case "glpi_tickets.time_to_resolve" :
            case "glpi_problems.time_to_resolve" :
            case "glpi_changes.time_to_resolve" :
            case "glpi_tickets.time_to_own" :
            case "glpi_tickets.internal_time_to_own" :
            case "glpi_tickets.internal_time_to_resolve" :
               // Due date + progress
               if (in_array($orig_id, [151, 158, 181, 186])) {
                  $out = Html::convDateTime($data[$ID][0]['name']);

                  // No due date in waiting status
                  if ($data[$ID][0]['status'] == CommonITILObject::WAITING) {
                     return '';
                  }
                  if (empty($data[$ID][0]['name'])) {
                     return '';
                  }
                  if (($data[$ID][0]['status'] == Ticket::SOLVED)
                      || ($data[$ID][0]['status'] == Ticket::CLOSED)) {
                     return $out;
                  }

                  $itemtype = getItemTypeForTable($table);
                  $item = new $itemtype();
                  $item->getFromDB($data['id']);
                  $percentage  = 0;
                  $totaltime   = 0;
                  $currenttime = 0;
                  $slaField    = 'slas_id';

                  // define correct sla field
                  switch ($table.'.'.$field) {
                     case "glpi_tickets.time_to_resolve" :
                        $slaField = 'slas_id_ttr';
                        break;
                     case "glpi_tickets.time_to_own" :
                        $slaField = 'slas_id_tto';
                        break;
                     case "glpi_tickets.internal_time_to_own" :
                        $slaField = 'olas_id_tto';
                        break;
                     case "glpi_tickets.internal_time_to_resolve" :
                        $slaField = 'olas_id_ttr';
                        break;
                  }

                  switch ($table.'.'.$field) {
                     // If ticket has been taken into account : no progression display
                     case "glpi_tickets.time_to_own" :
                     case "glpi_tickets.internal_time_to_own" :
                        if (($item->fields['takeintoaccount_delay_stat'] > 0)) {
                           return $out;
                        }
                        break;
                  }

                  if ($item->isField($slaField) && $item->fields[$slaField] != 0) { // Have SLA
                     $sla = new SLA();
                     $sla->getFromDB($item->fields[$slaField]);
                     $currenttime = $sla->getActiveTimeBetween($item->fields['date'],
                                                               date('Y-m-d H:i:s'));
                     $totaltime   = $sla->getActiveTimeBetween($item->fields['date'],
                                                               $data[$ID][0]['name']);
                  } else {
                     $calendars_id = Entity::getUsedConfig('calendars_id',
                                                           $item->fields['entities_id']);
                     if ($calendars_id != 0) { // Ticket entity have calendar
                        $calendar = new Calendar();
                        $calendar->getFromDB($calendars_id);
                        $currenttime = $calendar->getActiveTimeBetween($item->fields['date'],
                                                                       date('Y-m-d H:i:s'));
                        $totaltime   = $calendar->getActiveTimeBetween($item->fields['date'],
                                                                       $data[$ID][0]['name']);
                     } else { // No calendar
                        $currenttime = strtotime(date('Y-m-d H:i:s'))
                                                 - strtotime($item->fields['date']);
                        $totaltime   = strtotime($data[$ID][0]['name'])
                                                 - strtotime($item->fields['date']);
                     }
                  }
                  if ($totaltime != 0) {
                     $percentage  = round((100 * $currenttime) / $totaltime);
                  } else {
                     // Total time is null : no active time
                     $percentage = 100;
                  }
                  if ($percentage > 100) {
                     $percentage = 100;
                  }
                  $percentage_text = $percentage;

                  if ($_SESSION['glpiduedatewarning_unit'] == '%') {
                     $less_warn_limit = $_SESSION['glpiduedatewarning_less'];
                     $less_warn       = (100 - $percentage);
                  } else if ($_SESSION['glpiduedatewarning_unit'] == 'hour') {
                     $less_warn_limit = $_SESSION['glpiduedatewarning_less'] * HOUR_TIMESTAMP;
                     $less_warn       = ($totaltime - $currenttime);
                  } else if ($_SESSION['glpiduedatewarning_unit'] == 'day') {
                     $less_warn_limit = $_SESSION['glpiduedatewarning_less'] * DAY_TIMESTAMP;
                     $less_warn       = ($totaltime - $currenttime);
                  }

                  if ($_SESSION['glpiduedatecritical_unit'] == '%') {
                     $less_crit_limit = $_SESSION['glpiduedatecritical_less'];
                     $less_crit       = (100 - $percentage);
                  } else if ($_SESSION['glpiduedatecritical_unit'] == 'hour') {
                     $less_crit_limit = $_SESSION['glpiduedatecritical_less'] * HOUR_TIMESTAMP;
                     $less_crit       = ($totaltime - $currenttime);
                  } else if ($_SESSION['glpiduedatecritical_unit'] == 'day') {
                     $less_crit_limit = $_SESSION['glpiduedatecritical_less'] * DAY_TIMESTAMP;
                     $less_crit       = ($totaltime - $currenttime);
                  }

                  $color = $_SESSION['glpiduedateok_color'];
                  if ($less_crit < $less_crit_limit) {
                     $color = $_SESSION['glpiduedatecritical_color'];
                  } else if ($less_warn < $less_warn_limit) {
                     $color = $_SESSION['glpiduedatewarning_color'];
                  }

                  if (!isset($so['datatype'])) {
                     $so['datatype'] = 'progressbar';
                  }

                  $progressbar_data = [
                     'text'         => Html::convDateTime($data[$ID][0]['name']),
                     'percent'      => $percentage,
                     'percent_text' => $percentage_text,
                     'color'        => $color
                  ];
               }
               break;

            case "glpi_softwarelicenses.number" :
               if ($data[$ID][0]['min'] == -1) {
                  return __('Unlimited');
               }
               if (empty($data[$ID][0]['name'])) {
                  return 0;
               }
               return $data[$ID][0]['name'];

            case "glpi_auth_tables.name" :
               return Auth::getMethodName($data[$ID][0]['name'], $data[$ID][0]['auths_id'], 1,
                                          $data[$ID][0]['ldapname'].$data[$ID][0]['mailname']);

            case "glpi_reservationitems.comment" :
               if (empty($data[$ID][0]['name'])) {
                  $text = __('None');
               } else {
                  $text = Html::resume_text($data[$ID][0]['name']);
               }
               if (Session::haveRight('reservation', UPDATE)) {
                  return "<a title=\"".__s('Modify the comment')."\"
                           href='".ReservationItem::getFormURLWithID($data['refID'])."' >".$text."</a>";
               }
               return $text;

            case 'glpi_crontasks.description' :
               $tmp = new CronTask();
               return $tmp->getDescription($data[$ID][0]['name']);

            case 'glpi_changes.status':
               $status = Change::getStatus($data[$ID][0]['name']);
               return "<span class='no-wrap'>".
                      Change::getStatusIcon($data[$ID][0]['name']) . "&nbsp;$status".
                      "</span>";

            case 'glpi_problems.status':
               $status = Problem::getStatus($data[$ID][0]['name']);
               return "<span class='no-wrap'>".
                      Problem::getStatusIcon($data[$ID][0]['name']) . "&nbsp;$status".
                      "</span>";

            case 'glpi_tickets.status':
               $status = Ticket::getStatus($data[$ID][0]['name']);
               return "<span class='no-wrap'>".
                      Ticket::getStatusIcon($data[$ID][0]['name']) . "&nbsp;$status".
                      "</span>";

            case 'glpi_projectstates.name':
               $out = '';
               $name = $data[$ID][0]['name'];
               if (isset($data[$ID][0]['trans'])) {
                  $name = $data[$ID][0]['trans'];
               }
               if ($itemtype == 'ProjectState') {
                  $out =   "<a href='".ProjectState::getFormURLWithID($data[$ID][0]["id"])."'>". $name."</a></div>";
               } else {
                  $out = $name;
               }
               return $out;

            case 'glpi_items_tickets.items_id' :
            case 'glpi_items_problems.items_id' :
            case 'glpi_changes_items.items_id' :
            case 'glpi_certificates_items.items_id' :
            case 'glpi_appliances_items.items_id' :
               if (!empty($data[$ID])) {
                  $items = [];
                  foreach ($data[$ID] as $key => $val) {
                     if (is_numeric($key)) {
                        if (!empty($val['itemtype'])
                                && ($item = getItemForItemtype($val['itemtype']))) {
                           if ($item->getFromDB($val['name'])) {
                              $items[] = $item->getLink(['comments' => true]);
                           }
                        }
                     }
                  }
                  if (!empty($items)) {
                     return implode("<br>", $items);
                  }
               }
               return '&nbsp;';

            case 'glpi_items_tickets.itemtype' :
            case 'glpi_items_problems.itemtype' :
               if (!empty($data[$ID])) {
                  $itemtypes = [];
                  foreach ($data[$ID] as $key => $val) {
                     if (is_numeric($key)) {
                        if (!empty($val['name'])
                              && ($item = getItemForItemtype($val['name']))) {
                           $item = new $val['name']();
                           $name = $item->getTypeName();
                           $itemtypes[] = __($name);
                        }
                     }
                  }
                  if (!empty($itemtypes)) {
                     return implode("<br>", $itemtypes);
                  }
               }

               return '&nbsp;';

            case 'glpi_tickets.name' :
            case 'glpi_problems.name' :
            case 'glpi_changes.name' :

               if (isset($data[$ID][0]['content'])
                   && isset($data[$ID][0]['id'])
                   && isset($data[$ID][0]['status'])) {
                  $link = $itemtype::getFormURLWithID($data[$ID][0]['id']);

                  $out  = "<a id='$itemtype".$data[$ID][0]['id']."' href=\"".$link;
                  // Force solution tab if solved
                  if ($item = getItemForItemtype($itemtype)) {
                     if (in_array($data[$ID][0]['status'], $item->getSolvedStatusArray())) {
                        $out .= "&amp;forcetab=$itemtype$2";
                     }
                  }
                  $out .= "\">";
                  $name = $data[$ID][0]['name'];
                  if ($_SESSION["glpiis_ids_visible"]
                      || empty($data[$ID][0]['name'])) {
                     $name = sprintf(__('%1$s (%2$s)'), $name, $data[$ID][0]['id']);
                  }
                  $out    .= $name."</a>";
                  $hdecode = Html::entity_decode_deep($data[$ID][0]['content']);
                  $content = Toolbox::unclean_cross_side_scripting_deep($hdecode);
                  $out     = sprintf(__('%1$s %2$s'), $out,
                                     Html::showToolTip(nl2br(Html::Clean($content)),
                                                             ['applyto' => $itemtype.
                                                                                $data[$ID][0]['id'],
                                                                   'display' => false]));
                  return $out;
               }

            case 'glpi_ticketvalidations.status' :
               $out   = '';
               for ($k=0; $k<$data[$ID]['count']; $k++) {
                  if ($data[$ID][$k]['name']) {
                     $status  = TicketValidation::getStatus($data[$ID][$k]['name']);
                     $bgcolor = TicketValidation::getStatusColor($data[$ID][$k]['name']);
                     $out    .= (empty($out)?'':self::LBBR).
                                 "<div style=\"background-color:".$bgcolor.";\">".$status.'</div>';
                  }
               }
               return $out;

            case 'glpi_ticketsatisfactions.satisfaction' :
               if (self::$output_type == self::HTML_OUTPUT) {
                  return TicketSatisfaction::displaySatisfaction($data[$ID][0]['name']);
               }
               break;

            case 'glpi_projects._virtual_planned_duration' :
               return Html::timestampToString(ProjectTask::getTotalPlannedDurationForProject($data["id"]),
                                              false);

            case 'glpi_projects._virtual_effective_duration' :
               return Html::timestampToString(ProjectTask::getTotalEffectiveDurationForProject($data["id"]),
                                              false);

            case 'glpi_cartridgeitems._virtual' :
               return Cartridge::getCount($data["id"], $data[$ID][0]['alarm_threshold'],
                                          self::$output_type != self::HTML_OUTPUT);

            case 'glpi_printers._virtual' :
               return Cartridge::getCountForPrinter($data["id"],
                                                    self::$output_type != self::HTML_OUTPUT);

            case 'glpi_consumableitems._virtual' :
               return Consumable::getCount($data["id"], $data[$ID][0]['alarm_threshold'],
                                           self::$output_type != self::HTML_OUTPUT);

            case 'glpi_links._virtual' :
               $out = '';
               $link = new Link();
               if (($item = getItemForItemtype($itemtype))
                   && $item->getFromDB($data['id'])
               ) {
                  $data = Link::getLinksDataForItem($item);
                  $count_display = 0;
                  foreach ($data as $val) {
                     $links = Link::getAllLinksFor($item, $val);
                     foreach ($links as $link) {
                        if ($count_display) {
                           $out .=  self::LBBR;
                        }
                        $out .= $link;
                        $count_display++;
                     }
                  }
               }
               return $out;

            case 'glpi_reservationitems._virtual' :
               if ($data[$ID][0]['is_active']) {
                  return "<a href='reservation.php?reservationitems_id=".
                                          $data["refID"]."' title=\"".__s('See planning')."\">".
                                          "<i class='far fa-calendar-alt'></i><span class='sr-only'>".__('See planning')."</span></a>";
               } else {
                  return "&nbsp;";
               }

            case "glpi_tickets.priority" :
            case "glpi_problems.priority" :
            case "glpi_changes.priority" :
            case "glpi_projects.priority" :
               $index = $data[$ID][0]['name'];
               $color = $_SESSION["glpipriority_$index"];
               $name  = CommonITILObject::getPriorityName($index);
               return "<div class='priority_block' style='border-color: $color'>
                        <span style='background: $color'></span>&nbsp;$name
                       </div>";
         }
      }

      //// Default case

      if ($itemtype == 'Ticket'
         && Session::getCurrentInterface() == 'helpdesk'
         && $orig_id == 8
         && Entity::getUsedConfig(
            'anonymize_support_agents',
            $itemtype::getById($data['id'])->getEntityId()
         )
      ) {
         // Assigned groups
         return __("Helpdesk group");
      }

      // Link with plugin tables : need to know left join structure
      if (isset($table)) {
         if (preg_match("/^glpi_plugin_([a-z0-9]+)/", $table.'.'.$field, $matches)) {
            if (count($matches) == 2) {
               $plug     = $matches[1];
               $out = Plugin::doOneHook(
                  $plug,
                  'giveItem',
                  $itemtype, $orig_id, $data, $ID
               );
               if (!empty($out)) {
                  return $out;
               }
            }
         }
      }
      $unit = '';
      if (isset($so['unit'])) {
         $unit = $so['unit'];
      }

      // Preformat items
      if (isset($so["datatype"])) {
         switch ($so["datatype"]) {
            case "itemlink" :
               $linkitemtype  = getItemTypeForTable($so["table"]);

               $out           = "";
               $count_display = 0;
               $separate      = self::LBBR;
               if (isset($so['splititems']) && $so['splititems']) {
                  $separate = self::LBHR;
               }

               for ($k=0; $k<$data[$ID]['count']; $k++) {
                  if (isset($data[$ID][$k]['id'])) {
                     if ($count_display) {
                        $out .= $separate;
                     }
                     $count_display++;
                     $page  = $linkitemtype::getFormURLWithID($data[$ID][$k]['id']);
                     $name  = Dropdown::getValueWithUnit($data[$ID][$k]['name'], $unit);
                     if ($_SESSION["glpiis_ids_visible"] || empty($data[$ID][$k]['name'])) {
                        $name = sprintf(__('%1$s (%2$s)'), $name, $data[$ID][$k]['id']);
                     }
                     $out  .= "<a id='".$linkitemtype."_".$data['id']."_".
                                $data[$ID][$k]['id']."' href='$page'>".
                               $name."</a>";
                  }
               }
               return $out;

            case "text" :
               $separate = self::LBBR;
               if (isset($so['splititems']) && $so['splititems']) {
                  $separate = self::LBHR;
               }

               $out           = '';
               $count_display = 0;
               for ($k=0; $k<$data[$ID]['count']; $k++) {
                  if (strlen(trim($data[$ID][$k]['name'])) > 0) {
                     if ($count_display) {
                        $out .= $separate;
                     }
                     $count_display++;
                     $text = "";
                     if (isset($so['htmltext']) && $so['htmltext']) {
                        $text = Html::clean(Toolbox::unclean_cross_side_scripting_deep(nl2br($data[$ID][$k]['name'])));
                     } else {
                        $text = nl2br($data[$ID][$k]['name']);
                     }

                     if (self::$output_type == self::HTML_OUTPUT
                         && (Toolbox::strlen($text) > $CFG_GLPI['cut'])) {
                        $rand = mt_rand();
                        $popup_params = [
                           'display'   => false
                        ];
                        if (Toolbox::strlen($text) > $CFG_GLPI['cut']) {
                           $popup_params += [
                              'awesome-class'   => 'fa-comments',
                              'autoclose'       => false,
                              'onclick'         => true
                           ];
                        } else {
                           $popup_params += [
                              'applyto'   => "text$rand",
                           ];
                        }
                        $out .= sprintf(
                           __('%1$s %2$s'),
                           "<span id='text$rand'>". Html::resume_text($text, $CFG_GLPI['cut']).'</span>',
                           Html::showToolTip(
                              '<div class="fup-popup">'.$text.'</div>', $popup_params
                              )
                        );
                     } else {
                        $out .= $text;
                     }
                  }
               }
               return $out;

            case "date" :
            case "date_delay" :
               $out   = '';
               for ($k=0; $k<$data[$ID]['count']; $k++) {
                  if (is_null($data[$ID][$k]['name'])
                      && isset($so['emptylabel']) && $so['emptylabel']) {
                     $out .= (empty($out)?'':self::LBBR).$so['emptylabel'];
                  } else {
                     $out .= (empty($out)?'':self::LBBR).Html::convDate($data[$ID][$k]['name']);
                  }
               }
               return $out;

            case "datetime" :
               $out   = '';
               for ($k=0; $k<$data[$ID]['count']; $k++) {
                  if (is_null($data[$ID][$k]['name'])
                      && isset($so['emptylabel']) && $so['emptylabel']) {
                     $out .= (empty($out)?'':self::LBBR).$so['emptylabel'];
                  } else {
                     $out .= (empty($out)?'':self::LBBR).Html::convDateTime($data[$ID][$k]['name']);
                  }
               }
               return $out;

            case "timestamp" :
               $withseconds = false;
               if (isset($so['withseconds'])) {
                  $withseconds = $so['withseconds'];
               }
               $withdays = true;
               if (isset($so['withdays'])) {
                  $withdays = $so['withdays'];
               }

               $out   = '';
               for ($k=0; $k<$data[$ID]['count']; $k++) {
                   $out .= (empty($out)?'':'<br>').Html::timestampToString($data[$ID][$k]['name'],
                                                                           $withseconds,
                                                                           $withdays);
               }
               return $out;

            case "email" :
               $out           = '';
               $count_display = 0;
               for ($k=0; $k<$data[$ID]['count']; $k++) {
                  if ($count_display) {
                     $out .= self::LBBR;
                  }
                  $count_display++;
                  if (!empty($data[$ID][$k]['name'])) {
                     $out .= (empty($out)?'':self::LBBR);
                     $out .= "<a href='mailto:".Html::entities_deep($data[$ID][$k]['name'])."'>".$data[$ID][$k]['name'];
                     $out .= "</a>";
                  }
               }
               return (empty($out) ? "&nbsp;" : $out);

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

            case "count" :
            case "number" :
               $out           = "";
               $count_display = 0;
               for ($k=0; $k<$data[$ID]['count']; $k++) {
                  if (strlen(trim($data[$ID][$k]['name'])) > 0) {
                     if ($count_display) {
                        $out .= self::LBBR;
                     }
                     $count_display++;
                     if (isset($so['toadd'])
                           && isset($so['toadd'][$data[$ID][$k]['name']])) {
                        $out .= $so['toadd'][$data[$ID][$k]['name']];
                     } else {
                        $number = str_replace(' ', '&nbsp;',
                                              Html::formatNumber($data[$ID][$k]['name'], false, 0));
                        $out .= Dropdown::getValueWithUnit($number, $unit);
                     }
                  }
               }
               return $out;

            case "decimal" :
               $out           = "";
               $count_display = 0;
               for ($k=0; $k<$data[$ID]['count']; $k++) {
                  if (strlen(trim($data[$ID][$k]['name'])) > 0) {

                     if ($count_display) {
                        $out .= self::LBBR;
                     }
                     $count_display++;
                     if (isset($so['toadd'])
                           && isset($so['toadd'][$data[$ID][$k]['name']])) {
                        $out .= $so['toadd'][$data[$ID][$k]['name']];
                     } else {
                        $number = str_replace(' ', '&nbsp;',
                                              Html::formatNumber($data[$ID][$k]['name']));
                        $out   .= Dropdown::getValueWithUnit($number, $unit);
                     }
                  }
               }
               return $out;

            case "bool" :
               $out           = "";
               $count_display = 0;
               for ($k=0; $k<$data[$ID]['count']; $k++) {
                  if (strlen(trim($data[$ID][$k]['name'])) > 0) {
                     if ($count_display) {
                        $out .= self::LBBR;
                     }
                     $count_display++;
                     $out .= Dropdown::getValueWithUnit(Dropdown::getYesNo($data[$ID][$k]['name']),
                                                        $unit);
                  }
               }
               return $out;

            case "itemtypename":
               if ($obj = getItemForItemtype($data[$ID][0]['name'])) {
                  return $obj->getTypeName();
               }
               return "";

            case "language":
               if (isset($CFG_GLPI['languages'][$data[$ID][0]['name']])) {
                  return $CFG_GLPI['languages'][$data[$ID][0]['name']][0];
               }
               return __('Default value');
            case 'progressbar':
               if (!isset($progressbar_data)) {
                  $bar_color = 'green';
                  $progressbar_data = [
                     'percent'      => $data[$ID][0]['name'],
                     'percent_text' => $data[$ID][0]['name'],
                     'color'        => $bar_color,
                     'text'         => ''
                  ];
               }

               $out = "{$progressbar_data['text']}<div class='center' style='background-color: #ffffff; width: 100%;
                        border: 1px solid #9BA563; position: relative;' >";
               $out .= "<div style='position:absolute;'>&nbsp;{$progressbar_data['percent_text']}%</div>";
               $out .= "<div class='center' style='background-color: {$progressbar_data['color']};
                        width: {$progressbar_data['percent']}%; height: 12px' ></div>";
               $out .= "</div>";

               return $out;
               break;
         }
      }
      // Manage items with need group by / group_concat
      $out           = "";
      $count_display = 0;
      $separate      = self::LBBR;
      if (isset($so['splititems']) && $so['splititems']) {
         $separate = self::LBHR;
      }
      for ($k=0; $k<$data[$ID]['count']; $k++) {
         if (strlen(trim($data[$ID][$k]['name'])) > 0) {
            if ($count_display) {
               $out .= $separate;
            }
            $count_display++;
            // Get specific display if available
            if (isset($table)) {
               $itemtype = getItemTypeForTable($table);
               if ($item = getItemForItemtype($itemtype)) {
                  $tmpdata  = $data[$ID][$k];
                  // Copy name to real field
                  $tmpdata[$field] = $data[$ID][$k]['name'];

                  $specific = $item->getSpecificValueToDisplay(
                     $field,
                     $tmpdata, [
                        'html'      => true,
                        'searchopt' => $so,
                        'raw_data'  => $data
                     ]
                  );
               }
            }
            if (!empty($specific)) {
               $out .= $specific;
            } else {
               if (isset($so['toadd'])
                   && isset($so['toadd'][$data[$ID][$k]['name']])) {
                  $out .= $so['toadd'][$data[$ID][$k]['name']];
               } else {
                  // Empty is 0 or empty
                  if (empty($split[0])&& isset($so['emptylabel'])) {
                     $out .= $so['emptylabel'];
                  } else {
                     // Trans field exists
                     if (isset($data[$ID][$k]['trans']) && !empty($data[$ID][$k]['trans'])) {
                        $out .=  Dropdown::getValueWithUnit($data[$ID][$k]['trans'], $unit);
                     } else {
                        $out .= Dropdown::getValueWithUnit($data[$ID][$k]['name'], $unit);
                     }
                  }
               }
            }
         }
      }
      return $out;
   }


   /**
    * Reset save searches
    *
    * @return void
   **/
   static function resetSaveSearch() {

      unset($_SESSION['glpisearch']);
      $_SESSION['glpisearch']       = [];
   }


   /**
    * Completion of the URL $_GET values with the $_SESSION values or define default values
    *
    * @param string  $itemtype        Item type to manage
    * @param array   $params          Params to parse
    * @param boolean $usesession      Use datas save in session (true by default)
    * @param boolean $forcebookmark   Force trying to load parameters from default bookmark:
    *                                  used for global search (false by default)
    *
    * @return array parsed params
   **/
   static function manageParams($itemtype, $params = [], $usesession = true,
                                $forcebookmark = false) {
      $default_values = [];

      $default_values["start"]       = 0;
      $default_values["order"]       = "ASC";
      $default_values["sort"]        = 1;
      $default_values["is_deleted"]  = 0;
      $default_values["as_map"]      = 0;

      if (isset($params['start'])) {
         $params['start'] = (int)$params['start'];
      }

      $default_values["criteria"]     = self::getDefaultCriteria($itemtype);
      $default_values["metacriteria"] = [];

      // Reorg search array
      // start
      // order
      // sort
      // is_deleted
      // itemtype
      // criteria : array (0 => array (link =>
      //                               field =>
      //                               searchtype =>
      //                               value =>   (contains)
      // metacriteria : array (0 => array (itemtype =>
      //                                  link =>
      //                                  field =>
      //                                  searchtype =>
      //                                  value =>   (contains)

      if (($itemtype != 'AllAssets')
          && class_exists($itemtype)) {

         // retrieve default values for current itemtype
         $itemtype_default_values = [];
         if (method_exists($itemtype, 'getDefaultSearchRequest')) {
            $itemtype_default_values = call_user_func([$itemtype, 'getDefaultSearchRequest']);
         }

         // retrieve default values for the current user
         $user_default_values = SavedSearch_User::getDefault(Session::getLoginUserID(), $itemtype);
         if ($user_default_values === false) {
            $user_default_values = [];
         }

         // we construct default values in this order:
         // - general default
         // - itemtype default
         // - user default
         //
         // The last ones erase values or previous
         // So, we can combine each part (order from itemtype, criteria from user, etc)
         $default_values = array_merge($default_values,
                                       $itemtype_default_values,
                                       $user_default_values);
      }

      // First view of the page or force bookmark : try to load a bookmark
      if ($forcebookmark
          || ($usesession
              && !isset($params["reset"])
              && !isset($_SESSION['glpisearch'][$itemtype]))) {

         $user_default_values = SavedSearch_User::getDefault(Session::getLoginUserID(), $itemtype);
         if ($user_default_values) {
            $_SESSION['glpisearch'][$itemtype] = [];
            // Only get datas for bookmarks
            if ($forcebookmark) {
               $params = $user_default_values;
            } else {
               $bookmark = new SavedSearch();
               $bookmark->load($user_default_values['savedsearches_id'], false);
            }
         }
      }
      // Force reorder criterias
      if (isset($params["criteria"])
          && is_array($params["criteria"])
          && count($params["criteria"])) {

         $tmp                = $params["criteria"];
         $params["criteria"] = [];
         foreach ($tmp as $val) {
            $params["criteria"][] = $val;
         }
      }

      // transform legacy meta-criteria in criteria (with flag meta=true)
      // at the end of the array, as before there was only at the end of the query
      if (isset($params["metacriteria"])
          && is_array($params["metacriteria"])) {
         // as we will append meta to criteria, check the key exists
         if (!isset($params["criteria"])) {
            $params["criteria"] = [];
         }
         foreach ($params["metacriteria"] as $val) {
            $params["criteria"][] = $val + ['meta' => 1];
         }
         $params["metacriteria"] = [];
      }

      if ($usesession
          && isset($params["reset"])) {
         if (isset($_SESSION['glpisearch'][$itemtype])) {
            unset($_SESSION['glpisearch'][$itemtype]);
         }
      }

      if (isset($params) && is_array($params)
          && $usesession) {
         foreach ($params as $key => $val) {
            $_SESSION['glpisearch'][$itemtype][$key] = $val;
         }
      }

      $saved_params = $params;
      foreach ($default_values as $key => $val) {
         if (!isset($params[$key])) {
            if ($usesession
                && ($key == 'is_deleted' || $key == 'as_map' || !isset($saved_params['criteria'])) // retrieve session only if not a new request
                && isset($_SESSION['glpisearch'][$itemtype][$key])) {
               $params[$key] = $_SESSION['glpisearch'][$itemtype][$key];
            } else {
               $params[$key]                    = $val;
               $_SESSION['glpisearch'][$itemtype][$key] = $val;
            }
         }
      }

      return $params;
   }


   /**
    * Clean search options depending of user active profile
    *
    * @param string  $itemtype     Item type to manage
    * @param integer $action       Action which is used to manupulate searchoption
    *                               (default READ)
    * @param boolean $withplugins  Get plugins options (true by default)
    *
    * @return array Clean $SEARCH_OPTION array
   **/
   static function getCleanedOptions($itemtype, $action = READ, $withplugins = true) {
      global $CFG_GLPI;

      $options = &self::getOptions($itemtype, $withplugins);
      $todel   = [];

      if (!Session::haveRight('infocom', $action)
          && Infocom::canApplyOn($itemtype)) {
         $itemstodel = Infocom::getSearchOptionsToAdd($itemtype);
         $todel      = array_merge($todel, array_keys($itemstodel));
      }

      if (!Session::haveRight('contract', $action)
          && in_array($itemtype, $CFG_GLPI["contract_types"])) {
         $itemstodel = Contract::getSearchOptionsToAdd();
         $todel      = array_merge($todel, array_keys($itemstodel));
      }

      if (!Session::haveRight('document', $action)
          && Document::canApplyOn($itemtype)) {
         $itemstodel = Document::getSearchOptionsToAdd();
         $todel      = array_merge($todel, array_keys($itemstodel));
      }

      // do not show priority if you don't have right in profile
      if (($itemtype == 'Ticket')
          && ($action == UPDATE)
          && !Session::haveRight('ticket', Ticket::CHANGEPRIORITY)) {
         $todel[] = 3;
      }

      if ($itemtype == 'Computer') {
         if (!Session::haveRight('networking', $action)) {
            $itemstodel = NetworkPort::getSearchOptionsToAdd($itemtype);
            $todel      = array_merge($todel, array_keys($itemstodel));
         }
      }
      if (!Session::haveRight(strtolower($itemtype), READNOTE)) {
         $todel[] = 90;
      }

      if (count($todel)) {
         foreach ($todel as $ID) {
            if (isset($options[$ID])) {
               unset($options[$ID]);
            }
         }
      }

      return $options;
   }


   /**
    *
    * Get an option number in the SEARCH_OPTION array
    *
    * @param string $itemtype  Item type
    * @param string $field     Name
    *
    * @return integer
   **/
   static function getOptionNumber($itemtype, $field) {

      $table = $itemtype::getTable();
      $opts  = &self::getOptions($itemtype);

      foreach ($opts as $num => $opt) {
         if (is_array($opt) && isset($opt['table'])
             && ($opt['table'] == $table)
             && ($opt['field'] == $field)) {
            return $num;
         }
      }
      return 0;
   }


   /**
    * Get the SEARCH_OPTION array
    *
    * @param string  $itemtype     Item type
    * @param boolean $withplugins  Get search options from plugins (true by default)
    *
    * @return array The reference to the array of search options for the given item type
   **/
   static function &getOptions($itemtype, $withplugins = true) {
      global $CFG_GLPI;

      $item = null;

      if (!isset(self::$search[$itemtype])) {
         // standard type first
         switch ($itemtype) {
            case 'Internet' :
               self::$search[$itemtype]['common']            = __('Characteristics');

               self::$search[$itemtype][1]['table']          = 'networkport_types';
               self::$search[$itemtype][1]['field']          = 'name';
               self::$search[$itemtype][1]['name']           = __('Name');
               self::$search[$itemtype][1]['datatype']       = 'itemlink';
               self::$search[$itemtype][1]['searchtype']     = 'contains';

               self::$search[$itemtype][2]['table']          = 'networkport_types';
               self::$search[$itemtype][2]['field']          = 'id';
               self::$search[$itemtype][2]['name']           = __('ID');
               self::$search[$itemtype][2]['searchtype']     = 'contains';

               self::$search[$itemtype][31]['table']         = 'glpi_states';
               self::$search[$itemtype][31]['field']         = 'completename';
               self::$search[$itemtype][31]['name']          = __('Status');

               self::$search[$itemtype] += NetworkPort::getSearchOptionsToAdd('networkport_types');
               break;

            case 'AllAssets' :
               self::$search[$itemtype]['common']            = __('Characteristics');

               self::$search[$itemtype][1]['table']          = 'asset_types';
               self::$search[$itemtype][1]['field']          = 'name';
               self::$search[$itemtype][1]['name']           = __('Name');
               self::$search[$itemtype][1]['datatype']       = 'itemlink';
               self::$search[$itemtype][1]['searchtype']     = 'contains';

               self::$search[$itemtype][2]['table']          = 'asset_types';
               self::$search[$itemtype][2]['field']          = 'id';
               self::$search[$itemtype][2]['name']           = __('ID');
               self::$search[$itemtype][2]['searchtype']     = 'contains';

               self::$search[$itemtype][31]['table']         = 'glpi_states';
               self::$search[$itemtype][31]['field']         = 'completename';
               self::$search[$itemtype][31]['name']          = __('Status');

               self::$search[$itemtype] += Location::getSearchOptionsToAdd();

               self::$search[$itemtype][5]['table']          = 'asset_types';
               self::$search[$itemtype][5]['field']          = 'serial';
               self::$search[$itemtype][5]['name']           = __('Serial number');

               self::$search[$itemtype][6]['table']          = 'asset_types';
               self::$search[$itemtype][6]['field']          = 'otherserial';
               self::$search[$itemtype][6]['name']           = __('Inventory number');

               self::$search[$itemtype][16]['table']         = 'asset_types';
               self::$search[$itemtype][16]['field']         = 'comment';
               self::$search[$itemtype][16]['name']          = __('Comments');
               self::$search[$itemtype][16]['datatype']      = 'text';

               self::$search[$itemtype][70]['table']         = 'glpi_users';
               self::$search[$itemtype][70]['field']         = 'name';
               self::$search[$itemtype][70]['name']          = User::getTypeName(1);

               self::$search[$itemtype][7]['table']          = 'asset_types';
               self::$search[$itemtype][7]['field']          = 'contact';
               self::$search[$itemtype][7]['name']           = __('Alternate username');
               self::$search[$itemtype][7]['datatype']       = 'string';

               self::$search[$itemtype][8]['table']          = 'asset_types';
               self::$search[$itemtype][8]['field']          = 'contact_num';
               self::$search[$itemtype][8]['name']           = __('Alternate username number');
               self::$search[$itemtype][8]['datatype']       = 'string';

               self::$search[$itemtype][71]['table']         = 'glpi_groups';
               self::$search[$itemtype][71]['field']         = 'completename';
               self::$search[$itemtype][71]['name']          = Group::getTypeName(1);

               self::$search[$itemtype][19]['table']         = 'asset_types';
               self::$search[$itemtype][19]['field']         = 'date_mod';
               self::$search[$itemtype][19]['name']          = __('Last update');
               self::$search[$itemtype][19]['datatype']      = 'datetime';
               self::$search[$itemtype][19]['massiveaction'] = false;

               self::$search[$itemtype][23]['table']         = 'glpi_manufacturers';
               self::$search[$itemtype][23]['field']         = 'name';
               self::$search[$itemtype][23]['name']          = Manufacturer::getTypeName(1);

               self::$search[$itemtype][24]['table']         = 'glpi_users';
               self::$search[$itemtype][24]['field']         = 'name';
               self::$search[$itemtype][24]['linkfield']     = 'users_id_tech';
               self::$search[$itemtype][24]['name']          = __('Technician in charge of the hardware');
               self::$search[$itemtype][24]['condition']     = ['is_assign' => 1];

               self::$search[$itemtype][49]['table']          = 'glpi_groups';
               self::$search[$itemtype][49]['field']          = 'completename';
               self::$search[$itemtype][49]['linkfield']      = 'groups_id_tech';
               self::$search[$itemtype][49]['name']           = __('Group in charge of the hardware');
               self::$search[$itemtype][49]['condition']      = ['is_assign' => 1];
               self::$search[$itemtype][49]['datatype']       = 'dropdown';

               self::$search[$itemtype][80]['table']         = 'glpi_entities';
               self::$search[$itemtype][80]['field']         = 'completename';
               self::$search[$itemtype][80]['name']          = Entity::getTypeName(1);
               break;

            default :
               if ($item = getItemForItemtype($itemtype)) {
                  self::$search[$itemtype] = $item->searchOptions();
               }
               break;
         }

         if (Session::getLoginUserID()
             && in_array($itemtype, $CFG_GLPI["ticket_types"])) {
            self::$search[$itemtype]['tracking']          = __('Assistance');

            self::$search[$itemtype][60]['table']         = 'glpi_tickets';
            self::$search[$itemtype][60]['field']         = 'id';
            self::$search[$itemtype][60]['datatype']      = 'count';
            self::$search[$itemtype][60]['name']          = _x('quantity', 'Number of tickets');
            self::$search[$itemtype][60]['forcegroupby']  = true;
            self::$search[$itemtype][60]['usehaving']     = true;
            self::$search[$itemtype][60]['massiveaction'] = false;
            self::$search[$itemtype][60]['joinparams']    = ['beforejoin'
                                                              => ['table'
                                                                        => 'glpi_items_tickets',
                                                                       'joinparams'
                                                                        => ['jointype'
                                                                                  => 'itemtype_item']],
                                                              'condition'
                                                              => getEntitiesRestrictRequest('AND',
                                                                                            'NEWTABLE')];

            self::$search[$itemtype][140]['table']         = 'glpi_problems';
            self::$search[$itemtype][140]['field']         = 'id';
            self::$search[$itemtype][140]['datatype']      = 'count';
            self::$search[$itemtype][140]['name']          = _x('quantity', 'Number of problems');
            self::$search[$itemtype][140]['forcegroupby']  = true;
            self::$search[$itemtype][140]['usehaving']     = true;
            self::$search[$itemtype][140]['massiveaction'] = false;
            self::$search[$itemtype][140]['joinparams']    = ['beforejoin'
                                                              => ['table'
                                                                        => 'glpi_items_problems',
                                                                       'joinparams'
                                                                        => ['jointype'
                                                                                  => 'itemtype_item']],
                                                              'condition'
                                                              => getEntitiesRestrictRequest('AND',
                                                                                            'NEWTABLE')];
         }

         if (in_array($itemtype, $CFG_GLPI["networkport_types"])
             || ($itemtype == 'AllAssets')) {
            self::$search[$itemtype] += NetworkPort::getSearchOptionsToAdd($itemtype);
         }

         if (in_array($itemtype, $CFG_GLPI["contract_types"])
             || ($itemtype == 'AllAssets')) {
            self::$search[$itemtype] += Contract::getSearchOptionsToAdd();
         }

         if (Document::canApplyOn($itemtype)
             || ($itemtype == 'AllAssets')) {
            self::$search[$itemtype] += Document::getSearchOptionsToAdd();
         }

         if (Infocom::canApplyOn($itemtype)
             || ($itemtype == 'AllAssets')) {
            self::$search[$itemtype] += Infocom::getSearchOptionsToAdd($itemtype);
         }

         if (in_array($itemtype, $CFG_GLPI["domain_types"])
             || ($itemtype == 'AllAssets')) {
            self::$search[$itemtype] += Domain::getSearchOptionsToAdd($itemtype);
         }

         if (in_array($itemtype, $CFG_GLPI["appliance_types"])
             || ($itemtype == 'AllAssets')) {
            self::$search[$itemtype] += Appliance::getSearchOptionsToAdd($itemtype);
         }

         if (in_array($itemtype, $CFG_GLPI["link_types"])) {
            self::$search[$itemtype]['link'] = _n('External link', 'External links', Session::getPluralNumber());
            self::$search[$itemtype] += Link::getSearchOptionsToAdd($itemtype);
         }

         if ($withplugins) {
            // Search options added by plugins
            $plugsearch = Plugin::getAddSearchOptions($itemtype);
            $plugsearch = $plugsearch + Plugin::getAddSearchOptionsNew($itemtype);
            if (count($plugsearch)) {
               self::$search[$itemtype] += ['plugins' => _n('Plugin', 'Plugins', Session::getPluralNumber())];
               self::$search[$itemtype] += $plugsearch;
            }
         }

         // Complete linkfield if not define
         if (is_null($item)) { // Special union type
            $itemtable = $CFG_GLPI['union_search_type'][$itemtype];
         } else {
            if ($item = getItemForItemtype($itemtype)) {
               $itemtable = $item->getTable();
            }
         }

         foreach (self::$search[$itemtype] as $key => $val) {
            if (!is_array($val) || count($val) == 1) {
               // skip sub-menu
               continue;
            }
            // Compatibility before 0.80 : Force massive action to false if linkfield is empty :
            if (isset($val['linkfield']) && empty($val['linkfield'])) {
               self::$search[$itemtype][$key]['massiveaction'] = false;
            }

            // Set default linkfield
            if (!isset($val['linkfield']) || empty($val['linkfield'])) {
               if ((strcmp($itemtable, $val['table']) == 0)
                   && (!isset($val['joinparams']) || (count($val['joinparams']) == 0))) {
                  self::$search[$itemtype][$key]['linkfield'] = $val['field'];
               } else {
                  self::$search[$itemtype][$key]['linkfield'] = getForeignKeyFieldForTable($val['table']);
               }
            }
            // Add default joinparams
            if (!isset($val['joinparams'])) {
               self::$search[$itemtype][$key]['joinparams'] = [];
            }
         }

      }

      return self::$search[$itemtype];
   }

   /**
    * Is the search item related to infocoms
    *
    * @param string  $itemtype  Item type
    * @param integer $searchID  ID of the element in $SEARCHOPTION
    *
    * @return boolean
   **/
   static function isInfocomOption($itemtype, $searchID) {
      if (!Infocom::canApplyOn($itemtype)) {
         return false;
      }

      $infocom_options = Infocom::rawSearchOptionsToAdd($itemtype);
      $found_infocoms  = array_filter($infocom_options, function($option) use ($searchID) {
         return isset($option['id']) && $searchID == $option['id'];
      });

      return (count($found_infocoms) > 0);
   }


   /**
    * @param string  $itemtype
    * @param integer $field_num
   **/
   static function getActionsFor($itemtype, $field_num) {

      $searchopt = &self::getOptions($itemtype);
      $actions   = [
         'contains'    => __('contains'),
         'notcontains' => __('not contains'),
         'searchopt'   => []
      ];

      if (isset($searchopt[$field_num]) && isset($searchopt[$field_num]['table'])) {
         $actions['searchopt'] = $searchopt[$field_num];

         // Force search type
         if (isset($actions['searchopt']['searchtype'])) {
            // Reset search option
            $actions              = [];
            $actions['searchopt'] = $searchopt[$field_num];
            if (!is_array($actions['searchopt']['searchtype'])) {
               $actions['searchopt']['searchtype'] = [$actions['searchopt']['searchtype']];
            }
            foreach ($actions['searchopt']['searchtype'] as $searchtype) {
               switch ($searchtype) {
                  case "equals" :
                     $actions['equals'] = __('is');
                     break;

                  case "notequals" :
                     $actions['notequals'] = __('is not');
                     break;

                  case "contains" :
                     $actions['contains']    = __('contains');
                     $actions['notcontains'] = __('not contains');
                     break;

                  case "notcontains" :
                     $actions['notcontains'] = __('not contains');
                     break;

                  case "under" :
                     $actions['under'] = __('under');
                     break;

                  case "notunder" :
                     $actions['notunder'] = __('not under');
                     break;

                  case "lessthan" :
                     $actions['lessthan'] = __('before');
                     break;

                  case "morethan" :
                     $actions['morethan'] = __('after');
                     break;
               }
            }
            return $actions;
         }

         if (isset($searchopt[$field_num]['datatype'])) {
            switch ($searchopt[$field_num]['datatype']) {
               case 'count' :
               case 'number' :
                  $opt = [
                     'contains'    => __('contains'),
                     'notcontains' => __('not contains'),
                     'equals'      => __('is'),
                     'notequals'   => __('is not'),
                     'searchopt'   => $searchopt[$field_num]
                  ];
                  // No is / isnot if no limits defined
                  if (!isset($searchopt[$field_num]['min'])
                      && !isset($searchopt[$field_num]['max'])) {
                     unset($opt['equals']);
                     unset($opt['notequals']);

                     // https://github.com/glpi-project/glpi/issues/6917
                     // change filter wording for numeric values to be more
                     // obvious if the number dropdown will not be used
                     $opt['contains']    = __('is');
                     $opt['notcontains'] = __('is not');
                  }
                  return $opt;

               case 'bool' :
                  return [
                     'equals'      => __('is'),
                     'notequals'   => __('is not'),
                     'contains'    => __('contains'),
                     'notcontains' => __('not contains'),
                     'searchopt'   => $searchopt[$field_num]
                  ];

               case 'right' :
                  return ['equals'    => __('is'),
                               'notequals' => __('is not'),
                               'searchopt' => $searchopt[$field_num]];

               case 'itemtypename' :
                  return ['equals'    => __('is'),
                               'notequals' => __('is not'),
                               'searchopt' => $searchopt[$field_num]];

               case 'date' :
               case 'datetime' :
               case 'date_delay' :
                  return [
                     'equals'      => __('is'),
                     'notequals'   => __('is not'),
                     'lessthan'    => __('before'),
                     'morethan'    => __('after'),
                     'contains'    => __('contains'),
                     'notcontains' => __('not contains'),
                     'searchopt'   => $searchopt[$field_num]
                  ];
            }
         }

         // switch ($searchopt[$field_num]['table']) {
         //    case 'glpi_users_validation' :
         //       return array('equals'    => __('is'),
         //                    'notequals' => __('is not'),
         //                    'searchopt' => $searchopt[$field_num]);
         // }

         switch ($searchopt[$field_num]['field']) {
            case 'id' :
               return ['equals'    => __('is'),
                            'notequals' => __('is not'),
                            'searchopt' => $searchopt[$field_num]];

            case 'name' :
            case 'completename' :
               $actions = [
                  'contains'    => __('contains'),
                  'notcontains' => __('not contains'),
                  'equals'      => __('is'),
                  'notequals'   => __('is not'),
                  'searchopt'   => $searchopt[$field_num]
               ];

               // Specific case of TreeDropdown : add under
               $itemtype_linked = getItemTypeForTable($searchopt[$field_num]['table']);
               if ($itemlinked = getItemForItemtype($itemtype_linked)) {
                  if ($itemlinked instanceof CommonTreeDropdown) {
                     $actions['under']    = __('under');
                     $actions['notunder'] = __('not under');
                  }
                  return $actions;
               }
         }
      }
      return $actions;
   }


   /**
    * Print generic Header Column
    *
    * @param integer          $type     Display type (0=HTML, 1=Sylk, 2=PDF, 3=CSV)
    * @param string           $value    Value to display
    * @param integer          &$num     Column number
    * @param string           $linkto   Link display element (HTML specific) (default '')
    * @param boolean|integer  $issort   Is the sort column ? (default 0)
    * @param string           $order    Order type ASC or DESC (defaut '')
    * @param string           $options  Options to add (default '')
    *
    * @return string HTML to display
   **/
   static function showHeaderItem($type, $value, &$num, $linkto = "", $issort = 0, $order = "",
                                  $options = "") {
      $out = "";
      switch ($type) {
         case self::PDF_OUTPUT_LANDSCAPE : //pdf

         case self::PDF_OUTPUT_PORTRAIT :
            global $PDF_TABLE;
            $PDF_TABLE .= "<th $options>";
            $PDF_TABLE .= Html::clean($value);
            $PDF_TABLE .= "</th>\n";
            break;

         case self::SYLK_OUTPUT : //sylk
            global $SYLK_HEADER,$SYLK_SIZE;
            $SYLK_HEADER[$num] = self::sylk_clean($value);
            $SYLK_SIZE[$num]   = Toolbox::strlen($SYLK_HEADER[$num]);
            break;

         case self::CSV_OUTPUT : //CSV
            $out = "\"".self::csv_clean($value)."\"".$_SESSION["glpicsv_delimiter"];
            break;

         default :
            $class = "";
            if ($issort) {
               $class = "order_$order";
            }
            $out = "<th $options class='$class'>";
            if (!empty($linkto)) {
               $out .= "<a href=\"$linkto\">";
            }
            $out .= $value;
            if (!empty($linkto)) {
               $out .= "</a>";
            }
            $out .= "</th>\n";
      }
      $num++;
      return $out;
   }


   /**
    * Print generic normal Item Cell
    *
    * @param integer $type        Display type (0=HTML, 1=Sylk, 2=PDF, 3=CSV)
    * @param string  $value       Value to display
    * @param integer &$num        Column number
    * @param integer $row         Row number
    * @param string  $extraparam  Extra parameters for display (default '')
    *
    * @return string HTML to display
   **/
   static function showItem($type, $value, &$num, $row, $extraparam = '') {

      $out = "";
      switch ($type) {
         case self::PDF_OUTPUT_LANDSCAPE : //pdf
         case self::PDF_OUTPUT_PORTRAIT :
            global $PDF_TABLE;
            $value = preg_replace('/'.self::LBBR.'/', '<br>', $value);
            $value = preg_replace('/'.self::LBHR.'/', '<hr>', $value);
            $PDF_TABLE .= "<td $extraparam valign='top'>";
            $PDF_TABLE .= Html::weblink_extract(Html::clean($value));
            $PDF_TABLE .= "</td>\n";

            break;

         case self::SYLK_OUTPUT : //sylk
            global $SYLK_ARRAY,$SYLK_SIZE;
            $value                  = Html::weblink_extract(Html::clean($value));
            $value = preg_replace('/'.self::LBBR.'/', '<br>', $value);
            $value = preg_replace('/'.self::LBHR.'/', '<hr>', $value);
            $SYLK_ARRAY[$row][$num] = self::sylk_clean($value);
            $SYLK_SIZE[$num]        = max($SYLK_SIZE[$num],
                                          Toolbox::strlen($SYLK_ARRAY[$row][$num]));
            break;

         case self::CSV_OUTPUT : //csv
            $value = preg_replace('/'.self::LBBR.'/', '<br>', $value);
            $value = preg_replace('/'.self::LBHR.'/', '<hr>', $value);
            $value = Html::weblink_extract(Html::clean($value));
            $out   = "\"".self::csv_clean($value)."\"".$_SESSION["glpicsv_delimiter"];
            break;

         default :
            global $CFG_GLPI;
            $out = "<td $extraparam valign='top'>";

            if (!preg_match('/'.self::LBHR.'/', $value)) {
               $values = preg_split('/'.self::LBBR.'/i', $value);
               $line_delimiter = '<br>';
            } else {
               $values = preg_split('/'.self::LBHR.'/i', $value);
               $line_delimiter = '<hr>';
            }

            if (count($values) > 1
                && Toolbox::strlen($value) > $CFG_GLPI['cut']) {
               $value = '';
               foreach ($values as $v) {
                  $value .= $v.$line_delimiter;
               }
               $value = preg_replace('/'.self::LBBR.'/', '<br>', $value);
               $value = preg_replace('/'.self::LBHR.'/', '<hr>', $value);
               $value = '<div class="fup-popup">'.$value.'</div>';
               $valTip = "&nbsp;".Html::showToolTip(
                  $value, [
                     'awesome-class'   => 'fa-comments',
                     'display'         => false,
                     'autoclose'       => false,
                     'onclick'         => true
                  ]
               );
               $out .= $values[0] . $valTip;
            } else {
               $value = preg_replace('/'.self::LBBR.'/', '<br>', $value);
               $value = preg_replace('/'.self::LBHR.'/', '<hr>', $value);
               $out .= $value;
            }
            $out .= "</td>\n";
      }
      $num++;
      return $out;
   }


   /**
    * Print generic error
    *
    * @param integer $type     Display type (0=HTML, 1=Sylk, 2=PDF, 3=CSV)
    * @param string  $message  Message to display, if empty "no item found" will be displayed
    *
    * @return string HTML to display
   **/
   static function showError($type, $message = "") {
      if (strlen($message) == 0) {
         $message = __('No item found');
      }

      $out = "";
      switch ($type) {
         case self::PDF_OUTPUT_LANDSCAPE : //pdf
         case self::PDF_OUTPUT_PORTRAIT :
         case self::SYLK_OUTPUT : //sylk
         case self::CSV_OUTPUT : //csv
            break;

         default :
            $out = "<div class='center b'>$message</div>\n";
      }
      return $out;
   }


   /**
    * Print generic footer
    *
    * @param integer $type  Display type (0=HTML, 1=Sylk, 2=PDF, 3=CSV)
    * @param string  $title title of file : used for PDF (default '')
    * @param integer $count Total number of results
    *
    * @return string HTML to display
   **/
   static function showFooter($type, $title = "", $count = null) {

      $out = "";
      switch ($type) {
         case self::PDF_OUTPUT_LANDSCAPE : //pdf
         case self::PDF_OUTPUT_PORTRAIT :
            global $PDF_TABLE;
            if ($type == self::PDF_OUTPUT_LANDSCAPE) {
               $pdf = new GLPIPDF('L', 'mm', 'A4', true, 'UTF-8', false);
            } else {
               $pdf = new GLPIPDF('P', 'mm', 'A4', true, 'UTF-8', false);
            }
            if ($count !== null) {
               $pdf->setTotalCount($count);
            }
            $pdf->SetCreator('GLPI');
            $pdf->SetAuthor('GLPI');
            $pdf->SetTitle($title);
            $pdf->SetHeaderData('', '', $title, '');
            $font       = 'helvetica';
            //$subsetting = true;
            $fontsize   = 8;
            if (isset($_SESSION['glpipdffont']) && $_SESSION['glpipdffont']) {
               $font       = $_SESSION['glpipdffont'];
               //$subsetting = false;
            }
            $pdf->setHeaderFont([$font, 'B', $fontsize]);
            $pdf->setFooterFont([$font, 'B', $fontsize]);

            //set margins
            $pdf->SetMargins(10, 15, 10);
            $pdf->SetHeaderMargin(10);
            $pdf->SetFooterMargin(10);

            //set auto page breaks
            $pdf->SetAutoPageBreak(true, 15);

            // For standard language
            //$pdf->setFontSubsetting($subsetting);
            // set font
            $pdf->SetFont($font, '', $fontsize);
            $pdf->AddPage();
            $PDF_TABLE .= '</table>';
            $pdf->writeHTML($PDF_TABLE, true, false, true, false, '');
            $pdf->Output('glpi.pdf', 'I');
            break;

         case self::SYLK_OUTPUT : //sylk
            global $SYLK_HEADER,$SYLK_ARRAY,$SYLK_SIZE;
            // largeurs des colonnes
            foreach ($SYLK_SIZE as $num => $val) {
               $out .= "F;W".$num." ".$num." ".min(50, $val)."\n";
            }
            $out .= "\n";
            // Header
            foreach ($SYLK_HEADER as $num => $val) {
               $out .= "F;SDM4;FG0C;".($num == 1 ? "Y1;" : "")."X$num\n";
               $out .= "C;N;K\"$val\"\n";
               $out .= "\n";
            }
            // Datas
            foreach ($SYLK_ARRAY as $row => $tab) {
               foreach ($tab as $num => $val) {
                  $out .= "F;P3;FG0L;".($num == 1 ? "Y".$row.";" : "")."X$num\n";
                  $out .= "C;N;K\"$val\"\n";
               }
            }
            $out.= "E\n";
            break;

         case self::CSV_OUTPUT : //csv
            break;

         default :
            $out = "</table></div>\n";
      }
      return $out;
   }


   /**
    * Print generic footer
    *
    * @param integer         $type   Display type (0=HTML, 1=Sylk, 2=PDF, 3=CSV)
    * @param integer         $rows   Number of rows
    * @param integer         $cols   Number of columns
    * @param boolean|integer $fixed  Used tab_cadre_fixe table for HTML export ? (default 0)
    *
    * @return string HTML to display
   **/
   static function showHeader($type, $rows, $cols, $fixed = 0) {

      $out = "";
      switch ($type) {
         case self::PDF_OUTPUT_LANDSCAPE : //pdf
         case self::PDF_OUTPUT_PORTRAIT :
            global $PDF_TABLE;
            $PDF_TABLE = "<table cellspacing=\"0\" cellpadding=\"1\" border=\"1\" >";
            break;

         case self::SYLK_OUTPUT : // Sylk
            global $SYLK_ARRAY, $SYLK_HEADER, $SYLK_SIZE;
            $SYLK_ARRAY  = [];
            $SYLK_HEADER = [];
            $SYLK_SIZE   = [];
            // entetes HTTP
            header("Expires: Mon, 26 Nov 1962 00:00:00 GMT");
            header('Pragma: private'); /// IE BUG + SSL
            header('Cache-control: private, must-revalidate'); /// IE BUG + SSL
            header("Content-disposition: filename=glpi.slk");
            header('Content-type: application/octetstream');
            // entete du fichier
            echo "ID;PGLPI_EXPORT\n"; // ID;Pappli
            echo "\n";
            // formats
            echo "P;PGeneral\n";
            echo "P;P#,##0.00\n";       // P;Pformat_1 (reels)
            echo "P;P#,##0\n";          // P;Pformat_2 (entiers)
            echo "P;P@\n";              // P;Pformat_3 (textes)
            echo "\n";
            // polices
            echo "P;EArial;M200\n";
            echo "P;EArial;M200\n";
            echo "P;EArial;M200\n";
            echo "P;FArial;M200;SB\n";
            echo "\n";
            // nb lignes * nb colonnes
            echo "B;Y".$rows;
            echo ";X".$cols."\n"; // B;Yligmax;Xcolmax
            echo "\n";
            break;

         case self::CSV_OUTPUT : // csv
            header("Expires: Mon, 26 Nov 1962 00:00:00 GMT");
            header('Pragma: private'); /// IE BUG + SSL
            header('Cache-control: private, must-revalidate'); /// IE BUG + SSL
            header("Content-disposition: filename=glpi.csv");
            header('Content-type: text/csv');
            // zero width no break space (for excel)
            echo"\xEF\xBB\xBF";
            break;

         default :
            if ($fixed) {
               $out = "<div class='center'><table border='0' class='tab_cadre_fixehov'>\n";
            } else {
               $out = "<div class='center'><table border='0' class='tab_cadrehov'>\n";
            }
      }
      return $out;
   }


   /**
    * Print begin of header part
    *
    * @param integer $type   Display type (0=HTML, 1=Sylk, 2=PDF, 3=CSV)
    *
    * @since 0.85
    *
    * @return string HTML to display
   **/
   static function showBeginHeader($type) {

      $out = "";
      switch ($type) {
         case self::PDF_OUTPUT_LANDSCAPE : //pdf
         case self::PDF_OUTPUT_PORTRAIT :
            global $PDF_TABLE;
            $PDF_TABLE .= "<thead>";
            break;

         case self::SYLK_OUTPUT : //sylk
         case self::CSV_OUTPUT : //csv
            break;

         default :
            $out = "<thead>";
      }
      return $out;
   }


   /**
    * Print end of header part
    *
    * @param integer $type   Display type (0=HTML, 1=Sylk, 2=PDF, 3=CSV)
    *
    * @since 0.85
    *
    * @return string to display
   **/
   static function showEndHeader($type) {

      $out = "";
      switch ($type) {
         case self::PDF_OUTPUT_LANDSCAPE : //pdf
         case self::PDF_OUTPUT_PORTRAIT :
            global $PDF_TABLE;
            $PDF_TABLE .= "</thead>";
            break;

         case self::SYLK_OUTPUT : //sylk
         case self::CSV_OUTPUT : //csv
            break;

         default :
            $out = "</thead>";
      }
      return $out;
   }


   /**
    * Print generic new line
    *
    * @param integer $type        Display type (0=HTML, 1=Sylk, 2=PDF, 3=CSV)
    * @param boolean $odd         Is it a new odd line ? (false by default)
    * @param boolean $is_deleted  Is it a deleted search ? (false by default)
    *
    * @return string HTML to display
   **/
   static function showNewLine($type, $odd = false, $is_deleted = false) {

      $out = "";
      switch ($type) {
         case self::PDF_OUTPUT_LANDSCAPE : //pdf
         case self::PDF_OUTPUT_PORTRAIT :
            global $PDF_TABLE;
            $style = "";
            if ($odd) {
               $style = " style=\"background-color:#DDDDDD;\" ";
            }
            $PDF_TABLE .= "<tr $style nobr=\"true\">";
            break;

         case self::SYLK_OUTPUT : //sylk
         case self::CSV_OUTPUT : //csv
            break;

         default :
            $class = " class='tab_bg_2".($is_deleted?'_2':'')."' ";
            if ($odd) {
               $class = " class='tab_bg_1".($is_deleted?'_2':'')."' ";
            }
            $out = "<tr $class>";
      }
      return $out;
   }


   /**
    * Print generic end line
    *
    * @param integer $type  Display type (0=HTML, 1=Sylk, 2=PDF, 3=CSV)
    *
    * @return string HTML to display
   **/
   static function showEndLine($type) {

      $out = "";
      switch ($type) {
         case self::PDF_OUTPUT_LANDSCAPE : //pdf
         case self::PDF_OUTPUT_PORTRAIT :
            global $PDF_TABLE;
            $PDF_TABLE.= '</tr>';
            break;

         case self::SYLK_OUTPUT : //sylk
            break;

         case self::CSV_OUTPUT : //csv
            $out = "\n";
            break;

         default :
            $out = "</tr>";
      }
      return $out;
   }


   /**
    * @param array $joinparams
    */
   static function computeComplexJoinID(array $joinparams) {

      $complexjoin = '';

      if (isset($joinparams['condition'])) {
         if (is_array($joinparams['condition'])) {
            $complexjoin .= print_r($joinparams['condition'], true);
         } else {
            $complexjoin .= $joinparams['condition'];
         }
      }

      // For jointype == child
      if (isset($joinparams['jointype']) && ($joinparams['jointype'] == 'child')
          && isset($joinparams['linkfield'])) {
         $complexjoin .= $joinparams['linkfield'];
      }

      if (isset($joinparams['beforejoin'])) {
         if (isset($joinparams['beforejoin']['table'])) {
            $joinparams['beforejoin'] = [$joinparams['beforejoin']];
         }
         foreach ($joinparams['beforejoin'] as $tab) {
            if (isset($tab['table'])) {
               $complexjoin .= $tab['table'];
            }
            if (isset($tab['joinparams']) && isset($tab['joinparams']['condition'])) {
               if (is_array($tab['joinparams']['condition'])) {
                  $complexjoin .= print_r($tab['joinparams']['condition'], true);
               } else {
                  $complexjoin .= $tab['joinparams']['condition'];
               }
            }
         }
      }

      if (!empty($complexjoin)) {
         $complexjoin = md5($complexjoin);
      }
      return $complexjoin;
   }


   /**
    * Clean display value for csv export
    *
    * @param string $value value
    *
    * @return string Clean value
    **/
   static function csv_clean($value) {

      $value = str_replace("\"", "''", $value);
      $value = Html::clean($value, true, 2, false);
      $value = str_replace("&gt;", ">", $value);
      $value = str_replace("&lt;", "<", $value);

      return $value;
   }


   /**
    * Clean display value for sylk export
    *
    * @param string $value value
    *
    * @return string Clean value
    **/
   static function sylk_clean($value) {

      $value = preg_replace('/\x0A/', ' ', $value);
      $value = preg_replace('/\x0D/', null, $value);
      $value = str_replace("\"", "''", $value);
      $value = Html::clean($value);
      $value = str_replace("\n", " | ", $value);
      $value = str_replace("&gt;", ">", $value);
      $value = str_replace("&lt;", "<", $value);

      return $value;
   }


   /**
    * Create SQL search condition
    *
    * @param string  $field  Nname (should be ` protected)
    * @param string  $val    Value to search
    * @param boolean $not    Is a negative search ? (false by default)
    * @param string  $link   With previous criteria (default 'AND')
    *
    * @return search SQL string
   **/
   static function makeTextCriteria ($field, $val, $not = false, $link = 'AND') {

      $sql = $field . self::makeTextSearch($val, $not);
      // mange empty field (string with length = 0)
      $sql_or = "";
      if (strtolower($val) == "null") {
         $sql_or = "OR $field = ''";
      }

      if (($not && ($val != 'NULL') && ($val != 'null') && ($val != '^$'))    // Not something
          ||(!$not && ($val == '^$'))) {   // Empty
         $sql = "($sql OR $field IS NULL)";
      }
      return " $link ($sql $sql_or)";
   }

   /**
    * Create SQL search value
    *
    * @since 9.4
    *
    * @param string  $val value to search
    *
    * @return string|null
   **/
   static function makeTextSearchValue($val) {
      // Unclean to permit < and > search
      $val = Toolbox::unclean_cross_side_scripting_deep($val);

      // escape _ char used as wildcard in mysql likes
      $val = str_replace('_', '\\_', $val);

      if ($val === 'NULL' || $val === 'null') {
         return null;
      }

      $val = trim($val);

      if ($val === '^') {
         // Special case, searching "^" means we are searching for a non empty/null field
         return '%';
      }

      if ($val === '' || $val === '^$' || $val === '$') {
         return '';
      }

      if (preg_match('/^\^/', $val)) {
         // Remove leading `^`
         $val = ltrim(preg_replace('/^\^/', '', $val));
      } else {
         // Add % wildcard before searched string if not begining by a `^`
         $val = '%' . $val;
      }

      if (preg_match('/\$$/', $val)) {
         // Remove trailing `$`
         $val = rtrim(preg_replace('/\$$/', '', $val));
      } else {
         // Add % wildcard after searched string if not ending by a `$`
         $val = $val . '%';
      }

      return $val;
   }


   /**
    * Create SQL search condition
    *
    * @param string  $val  Value to search
    * @param boolean $not  Is a negative search ? (false by default)
    *
    * @return string Search string
   **/
   static function makeTextSearch($val, $not = false) {

      $NOT = "";
      if ($not) {
         $NOT = "NOT";
      }

      $val = self::makeTextSearchValue($val);
      if ($val == null) {
         $SEARCH = " IS $NOT NULL ";
      } else {
         $SEARCH = " $NOT LIKE '$val' ";
      }
      return $SEARCH;
   }


   /**
    * @since 0.84
    *
    * @param string $pattern
    * @param string $subject
   **/
   static function explodeWithID($pattern, $subject) {

      $tab = explode($pattern, $subject);

      if (isset($tab[1]) && !is_numeric($tab[1])) {
         // Report $ to tab[0]
         if (preg_match('/^(\\$*)(.*)/', $tab[1], $matchs)) {
            if (isset($matchs[2]) && is_numeric($matchs[2])) {
               $tab[1]  = $matchs[2];
               $tab[0] .= $matchs[1];
            }
         }
      }
      // Manage NULL value
      if ($tab[0] == self::NULLVALUE) {
         $tab[0] = null;
      }
      return $tab;
   }

   /**
    * Add join for dropdown translations
    *
    * @param string $alias    Alias for translation table
    * @param string $table    Table to join on
    * @param string $itemtype Item type
    * @param string $field    Field name
    *
    * @return string
    */
   public static function joinDropdownTranslations($alias, $table, $itemtype, $field) {
      return "LEFT JOIN `glpi_dropdowntranslations` AS `$alias`
                  ON (`$alias`.`itemtype` = '$itemtype'
                        AND `$alias`.`items_id` = `$table`.`id`
                        AND `$alias`.`language` = '".
                              $_SESSION['glpilanguage']."'
                        AND `$alias`.`field` = '$field')";
   }

   /**
    * Get table name for item type
    *
    * @param string $itemtype
    *
    * @return string
    */
   public static function getOrigTableName(string $itemtype): string {
      return (is_a($itemtype, CommonDBTM::class, true)) ? $itemtype::getTable() : getTableForItemType($itemtype);
   }
}
