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

use Glpi\Event;

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

/**
 * Document class
**/
class Document extends CommonDBTM {

   // From CommonDBTM
   public $dohistory                   = true;

   static protected $forward_entity_to = ['Document_Item'];

   static $rightname                   = 'document';
   static $tag_prefix                  = '#';
   protected $usenotepad               = true;


   static function getTypeName($nb = 0) {
      return _n('Document', 'Documents', $nb);
   }


   /**
    * Check if given object can have Document
    *
    * @since 0.85
    *
    * @param string|object $item An object or a string
    *
    * @return boolean
   **/
   static function canApplyOn($item) {
      global $CFG_GLPI;

      // All devices can have documents!
      if (is_a($item, 'Item_Devices', true)
          || is_a($item, 'CommonDevice', true)) {
         return true;
      }

      // We also allow direct items to check
      if ($item instanceof CommonGLPI) {
         $item = $item->getType();
      }

      if (in_array($item, $CFG_GLPI['document_types'])) {
         return true;
      }

      return false;
   }


   /**
    * Get all the types that can have a document
    *
    * @since 0.85
    *
    * @return array of the itemtypes
   **/
   static function getItemtypesThatCanHave() {
      global $CFG_GLPI;

      return array_merge($CFG_GLPI['document_types'],
                         CommonDevice::getDeviceTypes(),
                         Item_Devices::getDeviceTypes());
   }


   /**
    * @see CommonGLPI::getMenuShorcut()
    *
    * @since 0.85
   **/
   static function getMenuShorcut() {
      return 'd';
   }


   static function canCreate() {

      // Have right to add document OR ticket followup
      return (Session::haveRight('document', CREATE)
              || Session::haveRight('followup', ITILFollowup::ADDMYTICKET));
   }


   function canCreateItem() {

      if (isset($this->input['itemtype']) && isset($this->input['items_id'])) {
         if ($item = getItemForItemtype($this->input['itemtype'])) {
            if ($item->canAddItem('Document')) {
               return true;
            }
         }
      }

      // From Ticket Document Tab => check right to add followup.
      if (isset($this->fields['tickets_id'])
          && ($this->fields['tickets_id'] > 0)) {

         $ticket = new Ticket();
         if ($ticket->getFromDB($this->fields['tickets_id'])) {
            return $ticket->canAddFollowups();
         }
      }

      if (Document::canCreate()) {
         return parent::canCreateItem();
      }
      return false;
   }


   function cleanDBonPurge() {

      $this->deleteChildrenAndRelationsFromDb(
         [
            Document_Item::class,
         ]
      );

      // UNLINK DU FICHIER
      if (!empty($this->fields["filepath"])) {
         if (is_file(GLPI_DOC_DIR."/".$this->fields["filepath"])
             && !is_dir(GLPI_DOC_DIR."/".$this->fields["filepath"])
             && (countElementsInTable($this->getTable(),
                                     ['sha1sum' => $this->fields["sha1sum"] ]) <= 1)) {

            if (unlink(GLPI_DOC_DIR."/".$this->fields["filepath"])) {
               Session::addMessageAfterRedirect(sprintf(__('Succesful deletion of the file %s'),
                                                         GLPI_DOC_DIR."/".$this->fields["filepath"]));
            } else {
               Session::addMessageAfterRedirect(sprintf(__('Failed to delete the file %s'),
                                                        GLPI_DOC_DIR."/".$this->fields["filepath"]),
                                                false, ERROR);
            }
         }
      }
   }


   function defineTabs($options = []) {

      $ong = [];
      $this->addDefaultFormTab($ong);
      $this->addStandardTab('Document_Item', $ong, $options);
      $this->addStandardTab('Notepad', $ong, $options);
      $this->addStandardTab('Log', $ong, $options);

      return $ong;
   }


   function prepareInputForAdd($input) {
      global $CFG_GLPI;

      // security (don't accept filename from $_REQUEST)
      if (array_key_exists('filename', $_REQUEST)) {
         unset($input['filename']);
      }

      if ($uid = Session::getLoginUserID()) {
         $input["users_id"] = Session::getLoginUserID();
      }

      // Create a doc only selecting a file from a item form
      $create_from_item = false;
      if (isset($input["items_id"])
          && isset($input["itemtype"])
          && ($item = getItemForItemtype($input["itemtype"]))
          && ($input["items_id"] > 0)) {

         $typename = $item->getTypeName(1);
         $name     = NOT_AVAILABLE;

         if ($item->getFromDB($input["items_id"])) {
            $name = $item->getNameID();
         }
         //TRANS: %1$s is Document, %2$s is item type, %3$s is item name
         $input["name"] = addslashes(Html::resume_text(sprintf(__('%1$s: %2$s'),
                                                               Document::getTypeName(1),
                                                       sprintf(__('%1$s - %2$s'), $typename, $name)),
                                                       200));
         $create_from_item = true;
      }

      $upload_ok = false;
      if (isset($input["_filename"]) && !(empty($input["_filename"]) == 1)) {
         $upload_ok = $this->moveDocument($input, stripslashes(array_shift($input["_filename"])));
      } else if (isset($input["upload_file"]) && !empty($input["upload_file"])) {
         // Move doc from upload dir
         $upload_ok = $this->moveUploadedDocument($input, $input["upload_file"]);
      } else if (isset($input['filepath']) && file_exists(GLPI_DOC_DIR.'/'.$input['filepath'])) {
         // Document is created using an existing document file
         $upload_ok = true;
      }

      // Tag
      if (isset($input["_tag_filename"]) && !empty($input["_tag_filename"]) == 1) {
         $input['tag'] = array_shift($input["_tag_filename"]);
      }

      if (!isset($input["tag"]) || empty($input["tag"])) {
         $input['tag'] = Rule::getUuid();
      }

      // Upload failed : do not create document
      if ($create_from_item && !$upload_ok) {
         return false;
      }

      // Default document name
      if ((!isset($input['name']) || empty($input['name']))
          && isset($input['filename'])) {
         $input['name'] = $input['filename'];
      }

      unset($input["upload_file"]);

      // Don't add if no file
      if (isset($input["_only_if_upload_succeed"])
          && $input["_only_if_upload_succeed"]
          && (!isset($input['filename']) || empty($input['filename']))) {
         return false;
      }

      // Set default category for document linked to tickets
      if (isset($input['itemtype']) && ($input['itemtype'] == 'Ticket')
          && (!isset($input['documentcategories_id']) || ($input['documentcategories_id'] == 0))) {
         $input['documentcategories_id'] = $CFG_GLPI["documentcategories_id_forticket"];
      }

      if (isset($input['link']) && !empty($input['link']) && !Toolbox::isValidWebUrl($input['link'])) {
         Session::addMessageAfterRedirect(
            __('Invalid link'),
            false,
            ERROR
         );
         return false;
      }

      /* Unicity check
      if (isset($input['sha1sum'])) {
         // Check if already upload in the current entity
         $crit = array('sha1sum'=>$input['sha1sum'],
                       'entities_id'=>$input['entities_id']);
         foreach ($DB->request($this->getTable(), $crit) as $data) {
            $link=$this->getFormURL();
            Session::addMessageAfterRedirect(__('"A document with that filename has already been attached to another record.').
               "&nbsp;: <a href=\"".$link."?id=".
                     $data['id']."\">".$data['name']."</a>",
               false, ERROR, true);
            return false;
         }
      } */
      return $input;
   }


   function post_addItem() {

      if (isset($this->input["items_id"])
          && isset($this->input["itemtype"])
          && (($this->input["items_id"] > 0)
              || (($this->input["items_id"] == 0)
                  && ($this->input["itemtype"] == 'Entity')))
          && !empty($this->input["itemtype"])) {

         $docitem = new Document_Item();
         $docitem->add(['documents_id' => $this->fields['id'],
                             'itemtype'     => $this->input["itemtype"],
                             'items_id'     => $this->input["items_id"]]);

         Event::log($this->fields['id'], "documents", 4, "document",
                  //TRANS: %s is the user login
                    sprintf(__('%s adds a link with an item'), $_SESSION["glpiname"]));
      }
   }


   public function post_getFromDB() {
      if (isAPI()
          && (isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] == 'application/octet-stream'
              || isset($_GET['alt']) && $_GET['alt'] == 'media')) {
         // This is a API request to download the document
         $this->send();
         exit();
      }
   }


   function prepareInputForUpdate($input) {

      // security (don't accept filename from $_REQUEST)
      if (array_key_exists('filename', $_REQUEST)) {
         unset($input['filename']);
      }

      if (isset($input['current_filepath'])) {
         if (isset($input["_filename"]) && !empty($input["_filename"]) == 1) {
            $this->moveDocument($input, stripslashes(array_shift($input["_filename"])));
         } else if (isset($input["upload_file"]) && !empty($input["upload_file"])) {
            // Move doc from upload dir
            $this->moveUploadedDocument($input, $input["upload_file"]);
         }
      }

      unset($input['current_filepath']);
      unset($input['current_filename']);

      if (isset($input['link']) && !empty($input['link'])  && !Toolbox::isValidWebUrl($input['link'])) {
         Session::addMessageAfterRedirect(
            __('Invalid link'),
            false,
            ERROR
         );
         return false;
      }

      return $input;
   }


   /**
    * Print the document form
    *
    * @param $ID        integer ID of the item
    * @param $options   array
    *     - target filename : where to go when done.
    *     - withtemplate boolean : template or basic item
    *
    * @return void
   **/
   function showForm($ID, $options = []) {
      $this->initForm($ID, $options);
      // $options['formoptions'] = " enctype='multipart/form-data'";
      $this->showFormHeader($options);

      $showuserlink = 0;
      if (Session::haveRight('user', READ)) {
         $showuserlink = 1;
      }
      if ($ID > 0) {
         echo "<tr><th colspan='2'>";
         if ($this->fields["users_id"]>0) {
            printf(__('Added by %s'), getUserName($this->fields["users_id"], $showuserlink));
         } else {
            echo "&nbsp;";
         }
         echo "</th>";
         echo "<th colspan='2'>";

         //TRANS: %s is the datetime of update
         printf(__('Last update on %s'), Html::convDateTime($this->fields["date_mod"]));

         echo "</th></tr>\n";
      }

      echo "<tr class='tab_bg_1'>";
      echo "<td>".__('Name')."</td>";
      echo "<td>";
      Html::autocompletionTextField($this, "name");
      echo "</td>";
      if ($ID > 0) {
         echo "<td>".__('Current file')."</td>";
         echo "<td>".$this->getDownloadLink('', 45);
         echo "<input type='hidden' name='current_filepath' value='".$this->fields["filepath"]."'>";
         echo "<input type='hidden' name='current_filename' value='".$this->fields["filename"]."'>";
         echo "</td>";
      } else {
         echo "<td colspan=2>&nbsp;</td>";
      }
      echo "</tr>";

      echo "<tr class='tab_bg_1'>";
      echo "<td>".__('Heading')."</td>";
      echo "<td>";
      DocumentCategory::dropdown(['value' => $this->fields["documentcategories_id"]]);
      echo "</td>";
      if ($ID > 0) {
         echo "<td>".sprintf(__('%1$s (%2$s)'), __('Checksum'), __('SHA1'))."</td>";
         echo "<td>".$this->fields["sha1sum"];
         echo "</td>";
      } else {
         echo "<td colspan=2>&nbsp;</td>";
      }
      echo "</tr>";

      echo "<tr class='tab_bg_1'>";
      echo "<td>".__('Web link')."</td>";
      echo "<td>";
      Html::autocompletionTextField($this, "link");
      echo "</td>";
      echo "<td rowspan='3' class='middle'>".__('Comments')."</td>";
      echo "<td class='middle' rowspan='3'>";
      echo "<textarea cols='45' rows='6' name='comment' >".$this->fields["comment"]."</textarea>";
      echo "</td></tr>";

      echo "<tr class='tab_bg_1'>";
      echo "<td>".__('MIME type')."</td>";
      echo "<td>";
      Html::autocompletionTextField($this, "mime");
      echo "</td></tr>";

      echo "<tr class='tab_bg_1'>";
      echo "<td>".__('Blacklisted for import')."</td>";
      echo "<td>";
      Dropdown::showYesNo("is_blacklisted", $this->fields["is_blacklisted"]);
      echo "</td></tr>";

      echo "<tr class='tab_bg_1'>";
      echo "<td>".__('Use a FTP installed file')."</td>";
      echo "<td>";
      $this->showUploadedFilesDropdown("upload_file");
      echo "</td>";

      echo "<td>".sprintf(__('%1$s (%2$s)'), __('File'), self::getMaxUploadSize())."</td>";
      echo "<td>";
      Html::file();
      echo "</td></tr>";

      $this->showFormButtons($options);

      return true;
   }


   /**
    * Get max upload size from php config
   **/
   static function getMaxUploadSize() {

      $max_size  = Toolbox::return_bytes_from_ini_vars(ini_get("upload_max_filesize"));
      $max_size /= 1024*1024;
      //TRANS: %s is a size
      return sprintf(__('%s Mio max'), round($max_size, 1));
   }


   /**
    * Send a document to navigator
    *
    * @param string $context Context to resize image, if any
   **/
   function send($context = null) {
      $file = GLPI_DOC_DIR."/".$this->fields['filepath'];
      if ($context !== null) {
         $file = self::getImage($file, $context);
      }
      Toolbox::sendFile($file, $this->fields['filename'], $this->fields['mime']);
   }


   /**
    * Get download link for a document
    *
    * @param string  $params    additonal parameters to be added to the link (default '')
    * @param integer $len       maximum length of displayed string (default 20)
    *
   **/
   function getDownloadLink($params = '', $len = 20) {
      global $DB,$CFG_GLPI;

      $splitter = explode("/", $this->fields['filename']);

      if (count($splitter) == 2) {
         // Old documents in EXT/filename
         $fileout = $splitter[1];
      } else {
         // New document
         $fileout = $this->fields['filename'];
      }

      $initfileout = $fileout;

      if (Toolbox::strlen($fileout) > $len) {
         $fileout = Toolbox::substr($fileout, 0, $len)."&hellip;";
      }

      $out   = '';
      $open  = '';
      $close = '';
      if (self::canView()
          || $this->canViewFile(['tickets_id' => $this->fields['tickets_id']])) {
         $open  = "<a href='".$CFG_GLPI["root_doc"]."/front/document.send.php?docid=".
                    $this->fields['id'].$params."' alt=\"".$initfileout."\"
                    title=\"".$initfileout."\"target='_blank'>";
         $close = "</a>";
      }
      $splitter = explode("/", $this->fields['filepath']);

      if (count($splitter)) {
         $iterator = $DB->request([
            'SELECT' => 'icon',
            'FROM'   => 'glpi_documenttypes',
            'WHERE'  => [
               'ext'    => ['LIKE', $splitter[0]],
               'icon'   => ['<>', '']
            ]
         ]);

         if (count($iterator) > 0) {
            $result = $iterator->next();
            $icon = $result['icon'];
            if (!file_exists(GLPI_ROOT."/pics/icones/$icon")) {
               $icon = "defaut-dist.png";
            }
            $out .= "&nbsp;<img class='middle' style='margin-left:3px; margin-right:6px;' alt=\"".
                              $initfileout."\" title=\"".$initfileout."\" src='".
                              $CFG_GLPI["typedoc_icon_dir"]."/$icon'>";
         }
      }
      $out .= "$open<span class='b'>$fileout</span>$close";

      return $out;
   }


   /**
    * find a document with a file attached
    *
    * @param integer $entity    entity of the document
    * @param string  $path      path of the searched file
    *
    * @return boolean
   **/
   function getFromDBbyContent($entity, $path) {

      global $DB;

      if (empty($path)) {
         return false;
      }

      $sum = sha1_file($path);
      if (!$sum) {
         return false;
      }

      $doc_iterator = $DB->request(
         [
            'SELECT' => 'id',
            'FROM'   => $this->getTable(),
            'WHERE'  => [
               $this->getTable() . '.sha1sum'      => $sum,
               $this->getTable() . '.entities_id'  => $entity
            ],
            'LIMIT'  => 1,
         ]
      );

      if ($doc_iterator->count() === 0) {
         return false;
      }

      $doc_data = $doc_iterator->next();
      return $this->getFromDB($doc_data['id']);
   }


   /**
    * Check is the curent user is allowed to see the file
    *
    * @param array $options Options (only 'tickets_id' used)
    *
    * @return boolean
   **/
   function canViewFile(array $options = []) {

      // Check if it is my doc
      if (Session::getLoginUserID()
          && ($this->can($this->fields["id"], READ)
              || ($this->fields["users_id"] === Session::getLoginUserID()))) {
         return true;
      }

      if ($this->canViewFileFromReminder()) {
         return true;
      }

      if ($this->canViewFileFromKnowbaseItem()) {
         return true;
      }

      if (isset($options["changes_id"])
          && $this->canViewFileFromItilObject('Change', $options["changes_id"])) {
         return true;
      }

      if (isset($options["problems_id"])
          && $this->canViewFileFromItilObject('Problem', $options["problems_id"])) {
         return true;
      }

      // The following case should be reachable from the API
      self::loadAPISessionIfExist();

      if (isset($options["tickets_id"])
          && $this->canViewFileFromItilObject('Ticket', $options["tickets_id"])) {
         return true;
      }

      return false;
   }

   /**
    * Try to load the session from the API Tolen
    *
    * @since 9.5
    */
   private static function loadAPISessionIfExist() {
      $session_token = \Toolbox::getHeader('Session-Token');

      // No api token found
      if ($session_token === null) {
         return;
      }

      $current_session = session_id();

      // Clean current session
      if (!empty($current_session) && $current_session !== $session_token) {
         session_destroy();
      }

      // Load API session
      session_id($session_token);
      Session::start();
   }

   /**
    * Check if file of current instance can be viewed from a Reminder.
    *
    * @global DBmysql $DB
    * @return boolean
    *
    * @TODO Use DBmysqlIterator instead of raw SQL
    */
   private function canViewFileFromReminder() {

      global $DB;

      if (!Session::getLoginUserID()) {
         return false;
      }

      $criteria = array_merge_recursive(
         [
            'COUNT'     => 'cpt',
            'FROM'      => 'glpi_documents_items',
            'LEFT JOIN' => [
               'glpi_reminders'  => [
                  'ON' => [
                     'glpi_documents_items'  => 'items_id',
                     'glpi_reminders'        => 'id', [
                        'AND' => [
                           'glpi_documents_items.itemtype'  => 'Reminder'
                        ]
                     ]
                  ]
               ]
            ],
            'WHERE'     => [
               'glpi_documents_items.documents_id' => $this->fields['id']
            ]
         ],
         Reminder::getVisibilityCriteria()
      );

      $result = $DB->request($criteria)->next();
      return $result['cpt'] > 0;
   }

   /**
    * Check if file of current instance can be viewed from a KnowbaseItem.
    *
    * @global array $CFG_GLPI
    * @global DBmysql $DB
    * @return boolean
    */
   private function canViewFileFromKnowbaseItem() {

      global $CFG_GLPI, $DB;

      // Knowbase items can be viewed by non connected user in case of public FAQ
      if (!Session::getLoginUserID() && !$CFG_GLPI['use_public_faq']) {
         return false;
      }

      if (!Session::haveRight(KnowbaseItem::$rightname, READ)
          && !Session::haveRight(KnowbaseItem::$rightname, KnowbaseItem::READFAQ)
          && !$CFG_GLPI['use_public_faq']) {
         return false;
      }

      $visibilityCriteria = KnowbaseItem::getVisibilityCriteria();

      $request = [
         'FROM'      => 'glpi_documents_items',
         'COUNT'     => 'cpt',
         'LEFT JOIN' => [
            'glpi_knowbaseitems' => [
               'FKEY' => [
                  'glpi_knowbaseitems'   => 'id',
                  'glpi_documents_items' => 'items_id',
                  ['AND' => ['glpi_documents_items.itemtype' => 'KnowbaseItem']]
               ]
            ]
         ],
         'WHERE'     => [
            'glpi_documents_items.documents_id' => $this->fields['id'],
         ]
      ];

      if (array_key_exists('LEFT JOIN', $visibilityCriteria) && count($visibilityCriteria['LEFT JOIN']) > 0) {
         $request['LEFT JOIN'] += $visibilityCriteria['LEFT JOIN'];
      }
      if (array_key_exists('WHERE', $visibilityCriteria) && count($visibilityCriteria['WHERE']) > 0) {
         $request['WHERE'] += $visibilityCriteria['WHERE'];
      }

      $result = $DB->request($request)->next();

      return $result['cpt'] > 0;
   }

   /**
    * Check if file of current instance can be viewed from a CommonITILObject.
    *
    * @global DBmysql $DB
    * @param string  $itemtype
    * @param integer $items_id
    * @return boolean
    */
   private function canViewFileFromItilObject($itemtype, $items_id) {

      global $DB;

      if (!Session::getLoginUserID()) {
         return false;
      }

      /* @var CommonITILObject $itil */
      $itil = new $itemtype();

      if (!$itil->can($items_id, READ)) {
         return false;
      }

      $itil->getFromDB($items_id);

      $result = $DB->request([
         'FROM'  => Document_Item::getTable(),
         'COUNT' => 'cpt',
         'WHERE' => [
            $itil->getAssociatedDocumentsCriteria(),
            'documents_id' => $this->fields['id']
         ]
      ])->next();

      return $result['cpt'] > 0;
   }

   static function rawSearchOptionsToAdd($itemtype = null) {
      $tab = [];

      $tab[] = [
         'id'                 => 'document',
         'name'               => self::getTypeName(Session::getPluralNumber())
      ];

      $tab[] = [
         'id'                 => '119',
         'table'              => 'glpi_documents_items',
         'field'              => 'id',
         'name'               => _x('quantity', 'Number of documents'),
         'forcegroupby'       => true,
         'usehaving'          => true,
         'datatype'           => 'count',
         'massiveaction'      => false,
         'joinparams'         => [
            'jointype'           => 'itemtype_item'
         ]
      ];

      return $tab;
   }


   function rawSearchOptions() {
      $tab = [];

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

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

      $tab[] = [
         'id'                 => '2',
         'table'              => $this->getTable(),
         'field'              => 'id',
         'name'               => __('ID'),
         'massiveaction'      => false,
         'datatype'           => 'number'
      ];

      $tab[] = [
         'id'                 => '3',
         'table'              => $this->getTable(),
         'field'              => 'filename',
         'name'               => __('File'),
         'massiveaction'      => false,
         'datatype'           => 'string'
      ];

      $tab[] = [
         'id'                 => '4',
         'table'              => $this->getTable(),
         'field'              => 'link',
         'name'               => __('Web link'),
         'datatype'           => 'weblink',
         'autocomplete'       => true,
      ];

      $tab[] = [
         'id'                 => '5',
         'table'              => $this->getTable(),
         'field'              => 'mime',
         'name'               => __('MIME type'),
         'datatype'           => 'string',
         'autocomplete'       => true,
      ];

      $tab[] = [
         'id'                 => '6',
         'table'              => $this->getTable(),
         'field'              => 'tag',
         'name'               => __('Tag'),
         'datatype'           => 'text',
         'massiveaction'      => false
      ];

      $tab[] = [
         'id'                 => '7',
         'table'              => 'glpi_documentcategories',
         'field'              => 'completename',
         'name'               => __('Heading'),
         'datatype'           => 'dropdown'
      ];

      $tab[] = [
         'id'                 => '80',
         'table'              => 'glpi_entities',
         'field'              => 'completename',
         'name'               => Entity::getTypeName(1),
         'massiveaction'      => false,
         'datatype'           => 'dropdown'
      ];

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

      $tab[] = [
         'id'                 => '19',
         'table'              => $this->getTable(),
         'field'              => 'date_mod',
         'name'               => __('Last update'),
         'datatype'           => 'datetime',
         'massiveaction'      => false
      ];

      $tab[] = [
         'id'                 => '121',
         'table'              => $this->getTable(),
         'field'              => 'date_creation',
         'name'               => __('Creation date'),
         'datatype'           => 'datetime',
         'massiveaction'      => false
      ];

      $tab[] = [
         'id'                 => '20',
         'table'              => $this->getTable(),
         'field'              => 'sha1sum',
         'name'               => sprintf(__('%1$s (%2$s)'), __('Checksum'), __('SHA1')),
         'massiveaction'      => false,
         'datatype'           => 'string'
      ];

      $tab[] = [
         'id'                 => '16',
         'table'              => $this->getTable(),
         'field'              => 'comment',
         'name'               => __('Comments'),
         'datatype'           => 'text'
      ];

      $tab[] = [
         'id'                 => '72',
         'table'              => 'glpi_documents_items',
         'field'              => 'id',
         'name'               => _x('quantity', 'Number of associated items'),
         'forcegroupby'       => true,
         'usehaving'          => true,
         'datatype'           => 'count',
         'massiveaction'      => false,
         'joinparams'         => [
            'jointype'           => 'child'
         ]
      ];

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

      $tab = array_merge($tab, Notepad::rawSearchOptionsToAdd());

      return $tab;
   }


   /**
    * Move a file to a new location
    * Work even if dest file already exists
    *
    * @param string $srce   source file path
    * @param string $dest   destination file path
    *
    * @return boolean : success
   **/
   static function renameForce($srce, $dest) {

      // File already present
      if (is_file($dest)) {
         // As content is the same (sha1sum), no need to copy
         @unlink($srce);
         return true;
      }
      // Move
      return rename($srce, $dest);
   }


   /**
    * Move an uploadd document (files in GLPI_DOC_DIR."/_uploads" dir)
    *
    * @param array  $input     array of datas used in adding process (need current_filepath)
    * @param string $filename  filename to move
    *
    * @return boolean for success / $input array is updated
   **/
   public function moveUploadedDocument(array &$input, $filename) {
      $prefix = '';
      if (isset($input['_prefix_filename'])) {
         $prefix = array_shift($input['_prefix_filename']);
      }

      $fullpath = GLPI_UPLOAD_DIR."/".$filename;
      $filename = str_replace($prefix, '', $filename);

      if (!is_dir(GLPI_UPLOAD_DIR)) {
         Session::addMessageAfterRedirect(__("Upload directory doesn't exist"), false, ERROR);
         return false;
      }

      if (!is_file($fullpath)) {
         Session::addMessageAfterRedirect(sprintf(__('File %s not found.'), $fullpath),
                                          false, ERROR);
         return false;
      }
      $sha1sum  = sha1_file($fullpath);
      $dir      = self::isValidDoc($filename);
      $new_path = self::getUploadFileValidLocationName($dir, $sha1sum);

      if (!$sha1sum || !$dir || !$new_path) {
         return false;
      }

      // Delete old file (if not used by another doc)
      if (isset($input['current_filepath'])
          && !empty($input['current_filepath'])
          && is_file(GLPI_DOC_DIR."/".$input['current_filepath'])
          && (countElementsInTable('glpi_documents',
                                  ['sha1sum' => sha1_file(GLPI_DOC_DIR."/".
                                             $input['current_filepath']) ]) <= 1)) {

         if (unlink(GLPI_DOC_DIR."/".$input['current_filepath'])) {
            Session::addMessageAfterRedirect(sprintf(__('Succesful deletion of the file %s'),
                                                    $input['current_filename']));
         } else {
            // TRANS: %1$s is the curent filename, %2$s is its directory
            Session::addMessageAfterRedirect(sprintf(__('Failed to delete the file %1$s (%2$s)'),
                                                     $input['current_filename'],
                                                     GLPI_DOC_DIR."/".$input['current_filepath']),
                                             false, ERROR);
         }
      }

      // Local file : try to detect mime type
      $input['mime'] = Toolbox::getMime($fullpath);

      if (is_writable(GLPI_UPLOAD_DIR)
          && is_writable ($fullpath)) { // Move if allowed

         if (self::renameForce($fullpath, GLPI_DOC_DIR."/".$new_path)) {
            Session::addMessageAfterRedirect(__('Document move succeeded.'));
         } else {
            Session::addMessageAfterRedirect(__('File move failed.'), false, ERROR);
            return false;
         }

      } else { // Copy (will overwrite dest file is present)
         if (copy($fullpath, GLPI_DOC_DIR."/".$new_path)) {
            Session::addMessageAfterRedirect(__('Document copy succeeded.'));
         } else {
            Session::addMessageAfterRedirect(__('File move failed'), false, ERROR);
            return false;
         }
      }

      // For display
      $input['filename'] = addslashes($filename);
      // Storage path
      $input['filepath'] = $new_path;
      // Checksum
      $input['sha1sum']  = $sha1sum;
      return true;
   }

   /**
    * Move a document (files in GLPI_DOC_DIR."/_tmp" dir)
    *
    * @param array  $input     array of datas used in adding process (need current_filepath)
    * @param string $filename  filename to move
    *
    * @return boolean for success / $input array is updated
   **/
   static function moveDocument(array &$input, $filename) {
      $prefix = '';
      if (isset($input['_prefix_filename'])) {
         $prefix = array_shift($input['_prefix_filename']);
      }

      $fullpath = GLPI_TMP_DIR."/".$filename;
      $filename = str_replace($prefix, '', $filename);
      if (!is_dir(GLPI_TMP_DIR)) {
         Session::addMessageAfterRedirect(__("Temporary directory doesn't exist"), false, ERROR);
         return false;
      }

      if (!is_file($fullpath)) {
         Session::addMessageAfterRedirect(sprintf(__('File %s not found.'), $fullpath),
                                          false, ERROR);
         return false;
      }
      $sha1sum  = sha1_file($fullpath);
      $dir      = self::isValidDoc($filename);
      $new_path = self::getUploadFileValidLocationName($dir, $sha1sum);

      if (!$sha1sum || !$dir || !$new_path) {
         return false;
      }

      // Delete old file (if not used by another doc)
      if (isset($input['current_filepath'])
          && !empty($input['current_filepath'])
          && is_file(GLPI_DOC_DIR."/".$input['current_filepath'])
          && (countElementsInTable('glpi_documents',
                                  ['sha1sum' => sha1_file(GLPI_DOC_DIR."/".
                                             $input['current_filepath']) ]) <= 1)) {

         if (unlink(GLPI_DOC_DIR."/".$input['current_filepath'])) {
            Session::addMessageAfterRedirect(sprintf(__('Succesful deletion of the file %s'),
                                                    $input['current_filename']));
         } else {
            // TRANS: %1$s is the curent filename, %2$s is its directory
            Session::addMessageAfterRedirect(sprintf(__('Failed to delete the file %1$s (%2$s)'),
                                                     $input['current_filename'],
                                                     GLPI_DOC_DIR."/".$input['current_filepath']),
                                             false, ERROR);
         }
      }

      // Local file : try to detect mime type
      $input['mime'] = Toolbox::getMime($fullpath);

      if (is_writable(GLPI_TMP_DIR)
          && is_writable ($fullpath)) { // Move if allowed

         if (self::renameForce($fullpath, GLPI_DOC_DIR."/".$new_path)) {
            Session::addMessageAfterRedirect(__('Document move succeeded.'));
         } else {
            Session::addMessageAfterRedirect(__('File move failed.'), false, ERROR);
            return false;
         }

      } else { // Copy (will overwrite dest file is present)
         if (copy($fullpath, GLPI_DOC_DIR."/".$new_path)) {
            Session::addMessageAfterRedirect(__('Document copy succeeded.'));
         } else {
            Session::addMessageAfterRedirect(__('File move failed'), false, ERROR);
            return false;
         }
      }

      // For display
      $input['filename'] = addslashes($filename);
      // Storage path
      $input['filepath'] = $new_path;
      // Checksum
      $input['sha1sum']  = $sha1sum;
      return true;
   }


   /**
    * Upload a new file
    *
    * @param &$input    array of datas need for add/update (will be completed)
    * @param $FILEDESC        FILE descriptor
    *
    * @return true on success
   **/
   static function uploadDocument(array &$input, $FILEDESC) {

      if (!count($FILEDESC)
          || empty($FILEDESC['name'])
          || !is_file($FILEDESC['tmp_name'])) {

         switch ($FILEDESC['error']) {
            case 1 :
            case 2 :
               Session::addMessageAfterRedirect(__('File too large to be added.'), false, ERROR);
               break;

            case 4 :
               // Session::addMessageAfterRedirect(__('No file specified.'),false,ERROR);
               break;
         }

         return false;
      }

      $sha1sum = sha1_file($FILEDESC['tmp_name']);
      $dir     = self::isValidDoc($FILEDESC['name']);
      $path    = self::getUploadFileValidLocationName($dir, $sha1sum);

      if (!$sha1sum || !$dir || !$path) {
         return false;
      }

      // Delete old file (if not used by another doc)
      if (isset($input['current_filepath'])
          && !empty($input['current_filepath'])
          && (countElementsInTable('glpi_documents',
                                  ['sha1sum'=> sha1_file(GLPI_DOC_DIR."/".
                                             $input['current_filepath']) ]) <= 1)) {

         if (unlink(GLPI_DOC_DIR."/".$input['current_filepath'])) {
            Session::addMessageAfterRedirect(sprintf(__('Succesful deletion of the file %s'),
                                                     $input['current_filename']));
         } else {
            // TRANS: %1$s is the curent filename, %2$s is its directory
            Session::addMessageAfterRedirect(sprintf(__('Failed to delete the file %1$s (%2$s)'),
                                                     $input['current_filename'],
                                                     GLPI_DOC_DIR."/".$input['current_filepath']),
                                             false, ERROR);
         }
      }

      // Mime type from client
      if (isset($FILEDESC['type']) && !empty($FILEDESC['type'])) {
         $input['mime'] = $FILEDESC['type'];
      }

      // Move uploaded file
      if (self::renameForce($FILEDESC['tmp_name'], GLPI_DOC_DIR."/".$path)) {
         Session::addMessageAfterRedirect(__('The file is valid. Upload is successful.'));
         // For display
         $input['filename'] = addslashes($FILEDESC['name']);
         // Storage path
         $input['filepath'] = $path;
         // Checksum
         $input['sha1sum']  = $sha1sum;
         return true;
      }
      Session::addMessageAfterRedirect(__('Potential upload attack or file too large. Moving temporary file failed.'),
                                       false, ERROR);
      return false;
   }


   /**
    * Find a valid path for the new file
    *
    * @param string $dir      dir to search a free path for the file
    * @param string $sha1sum  SHA1 of the file
    *
    * @return string
   **/
   static function getUploadFileValidLocationName($dir, $sha1sum) {
      if (empty($dir)) {
         $message = __('Unauthorized file type');

         if (Session::haveRight('dropdown', READ)) {
            $dt       = new DocumentType();
            $message .= " <a target='_blank' href='".$dt->getSearchURL()."' class='pointer'>
                         <i class='fa fa-info'</i><span class='sr-only'>" . __('Manage document types')  . "</span></a>";
         }
         Session::addMessageAfterRedirect($message, false, ERROR);
         return '';
      }

      if (!is_dir(GLPI_DOC_DIR)) {
         Session::addMessageAfterRedirect(sprintf(__("The directory %s doesn't exist."),
                                                  GLPI_DOC_DIR),
                                          false, ERROR);
         return '';
      }
      $subdir = $dir.'/'.substr($sha1sum, 0, 2);

      if (!is_dir(GLPI_DOC_DIR."/".$subdir)
          && @mkdir(GLPI_DOC_DIR."/".$subdir, 0777, true)) {
         Session::addMessageAfterRedirect(sprintf(__('Create the directory %s'),
                                                  GLPI_DOC_DIR."/".$subdir));
      }

      if (!is_dir(GLPI_DOC_DIR."/".$subdir)) {
         Session::addMessageAfterRedirect(sprintf(__('Failed to create the directory %s. Verify that you have the correct permission'),
                                                  GLPI_DOC_DIR."/".$subdir),
                                          false, ERROR);
         return '';
      }
      return $subdir.'/'.substr($sha1sum, 2).'.'.$dir;
   }


   /**
    * Show dropdown of uploaded files
    *
    * @param $myname dropdown name
   **/
   static function showUploadedFilesDropdown($myname) {
      if (is_dir(GLPI_UPLOAD_DIR)) {

         $uploaded_files = [];
         if ($handle = opendir(GLPI_UPLOAD_DIR)) {
            while (false !== ($file = readdir($handle))) {
               if (($file != '.') && ($file != '..') && ($file != 'remove.txt')) {
                  $dir = self::isValidDoc($file);
                  if (!empty($dir)) {
                     $uploaded_files[$file] = $file;
                  }
               }
            }
            closedir($handle);
         }

         if (count($uploaded_files)) {
            Dropdown::showFromArray($myname, $uploaded_files, ['display_emptychoice' => true]);
         } else {
            echo __('No file available');
         }

      } else {
         echo __("Upload directory doesn't exist");
      }
   }


   /**
    * Is this file a valid file ? check based on file extension
    *
    * @param string $filename filename to clean
   **/
   static function isValidDoc($filename) {
      global $DB;

      $splitter = explode(".", $filename);
      $ext      = end($splitter);

      $iterator = $DB->request([
         'FROM'   => 'glpi_documenttypes',
         'WHERE'  => [
            'ext'             => ['LIKE', $ext],
            'is_uploadable'   => 1
         ]
      ]);

      if (count($iterator)) {
         return Toolbox::strtoupper($ext);
      }

      // Not found try with regex one
      $iterator = $DB->request([
         'FROM'   => 'glpi_documenttypes',
         'WHERE'  => [
            'ext'             => ['LIKE', '/%/'],
            'is_uploadable'   => 1
         ]
      ]);

      while ($data = $iterator->next()) {
         if (preg_match(Toolbox::unclean_cross_side_scripting_deep($data['ext'])."i",
                        $ext, $results) > 0) {
            return Toolbox::strtoupper($ext);
         }
      }

      return "";
   }

   /**
    * Make a select box for link document
    *
    * Parameters which could be used in options array :
    *    - name : string / name of the select (default is documents_id)
    *    - entity : integer or array / restrict to a defined entity or array of entities
    *                   (default -1 : no restriction)
    *    - used : array / Already used items ID: not to display in dropdown (default empty)
    *
    * @param $options array of possible options
    *
    * @return integer|string
    *    integer if option display=true (random part of elements id)
    *    string if option display=false (HTML code)
   **/
   static function dropdown($options = []) {
      global $DB, $CFG_GLPI;

      $p['name']    = 'documents_id';
      $p['entity']  = '';
      $p['used']    = [];
      $p['display'] = true;

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

      $subwhere = [
         'glpi_documents.is_deleted'   => 0,
      ] + getEntitiesRestrictCriteria('glpi_documents', '', $p['entity'], true);

      if (count($p['used'])) {
         $subwhere['NOT'] = ['id' => array_merge([0], $p['used'])];
      }

      $criteria = [
         'FROM'   => 'glpi_documentcategories',
         'WHERE'  => [
            'id' => new QuerySubQuery([
               'SELECT'          => 'documentcategories_id',
               'DISTINCT'        => true,
               'FROM'            => 'glpi_documents',
               'WHERE'           => $subwhere
            ])
         ],
         'ORDER'  => 'name'
      ];
      $iterator = $DB->request($criteria);

      $values = [];
      while ($data = $iterator->next()) {
         $values[$data['id']] = $data['name'];
      }
      $rand = mt_rand();
      $out  = Dropdown::showFromArray('_rubdoc', $values, ['width'               => '30%',
                                                                'rand'                => $rand,
                                                                'display'             => false,
                                                                'display_emptychoice' => true]);
      $field_id = Html::cleanId("dropdown__rubdoc$rand");

      $params   = ['rubdoc' => '__VALUE__',
                        'entity' => $p['entity'],
                        'rand'   => $rand,
                        'myname' => $p['name'],
                        'used'   => $p['used']];

      $out .= Ajax::updateItemOnSelectEvent($field_id, "show_".$p['name'].$rand,
                                            $CFG_GLPI["root_doc"]."/ajax/dropdownRubDocument.php",
                                            $params, false);
      $out .= "<span id='show_".$p['name']."$rand'>";
      $out .= "</span>\n";

      $params['rubdoc'] = 0;
      $out .= Ajax::updateItem("show_".$p['name'].$rand,
                               $CFG_GLPI["root_doc"]. "/ajax/dropdownRubDocument.php",
                               $params, false);
      if ($p['display']) {
         echo $out;
         return $rand;
      }
      return $out;
   }


   static function getMassiveActionsForItemtype(array &$actions, $itemtype, $is_deleted = 0,
                                                CommonDBTM $checkitem = null) {
      $action_prefix = 'Document_Item'.MassiveAction::CLASS_ACTION_SEPARATOR;

      if (self::canApplyOn($itemtype)) {
         if (Document::canView()) {
            $actions[$action_prefix.'add']    = "<i class='ma-icon far fa-file'></i>".
                                                _x('button', 'Add a document');
            $actions[$action_prefix.'remove'] = _x('button', 'Remove a document');
         }
      }

      if ((is_a($itemtype, __CLASS__, true)) && (static::canUpdate())) {
         $actions[$action_prefix.'add_item']    = _x('button', 'Add an item');
         $actions[$action_prefix.'remove_item'] = _x('button', 'Remove an item');
      }
   }


   /**
    * @since 0.85
    *
    * @param $string
    *
    * @return string
   **/
   static function getImageTag($string) {
      return self::$tag_prefix.$string.self::$tag_prefix;
   }

   /**
    * Is file an image
    *
    * @since 9.2.1
    *
    * @param string $file File name
    *
    * @return boolean
    */
   public static function isImage($file) {
      if (!file_exists($file)) {
         return false;
      }
      if (extension_loaded('exif')) {
         if (filesize($file) < 12) {
            return false;
         }
         $etype = exif_imagetype($file);
         return in_array($etype, [IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG, IMAGETYPE_BMP]);
      } else {
         Toolbox::logWarning('For security reasons, you should consider using exif PHP extension to properly check images.');
         $fileinfo = finfo_open(FILEINFO_MIME_TYPE);
         return in_array(
            finfo_file($fileinfo, $file),
            ['image/jpeg', 'image/png','image/gif', 'image/bmp']
         );
      }
   }

   /**
    * Get image path for a specified context.
    * Will call image resize if needed.
    *
    * @since 9.2.1
    *
    * @param string  $path    Original path
    * @param string  $context Context
    * @param integer $mwidth  Maximal width
    * @param integer $mheight Maximal height
    *
    * @return string Image path on disk
    */
   public static function getImage($path, $context, $mwidth = null, $mheight = null) {
      if ($mwidth === null || $mheight === null) {
         switch ($context) {
            case 'mail':
               $mwidth  = $mwidth ?? 400;
               $mheight = $mheight ?? 300;
               break;
            case 'timeline':
               $mwidth  = $mwidth ?? 100;
               $mheight = $mheight ?? 100;
               break;
            default:
               throw new \RuntimeException("Unknown context $context!");
         }
      }

      //let's see if original image needs resize
      $img_infos  = getimagesize($path);
      if (!($img_infos[0] > $mwidth) && !($img_infos[1] > $mheight)) {
         //no resize needed
         return $path;
      }

      $infos = pathinfo($path);
      // output images with possible transparency to png, other to jpg
      $extension = in_array(strtolower($infos['extension']), ['png', 'gif']) ? 'png' : 'jpg';
      $context_path = sprintf(
         '%1$s_%2$s-%3$s.%4$s',
         $infos['dirname'] . '/' . $infos['filename'],
         $mwidth,
         $mheight,
         $extension
      );

      //let's check if file already exists
      if (file_exists($context_path)) {
         return $context_path;
      }

      //do resize
      $result = Toolbox::resizePicture(
         $path,
         $context_path,
         $mwidth,
         $mheight,
         0,
         0,
         0,
         0,
         ($mwidth > $mheight ? $mwidth : $mheight)
      );
      return ($result ? $context_path : $path);
   }

   /**
    * Give cron information
    *
    * @param string $name task's name
    *
    * @return array of information
   **/
   static function cronInfo($name) {

      switch ($name) {
         case 'cleanorphans' :
            return ['description' => __('Clean orphaned documents')];
      }
      return [];
   }

   /**
    * Cron for clean orphan documents (without Document_Item)
    *
    * @param CronTask $task CronTask object
    *
    * @return integer (0 : nothing done - 1 : done)
   **/
   static function cronCleanOrphans(CronTask $task) {
      global $DB;

      $dtable = static::getTable();
      $ditable = Document_Item::getTable();
      //documents tht are nt present in Document_Item are oprhan
      $iterator = $DB->request([
         'SELECT'    => ["$dtable.id"],
         'FROM'      => $dtable,
         'LEFT JOIN' => [
            $ditable => [
               'ON'  => [
                  $dtable  => 'id',
                  $ditable => 'documents_id'
               ]
            ]
         ],
         'WHERE'     => [
               "$ditable.documents_id" => null
         ]
      ]);

      $nb = 0;
      if (count($iterator)) {
         while ($row = $iterator->next()) {
            $doc = new Document();
            $doc->delete(['id' => $row['id']], true);
            ++$nb;
         }
      }

      if ($nb) {
         $task->addVolume($nb);
         $task->log("Documents : $nb");
      }

      return ($nb > 0 ? 1 : 0);
   }


   static function getIcon() {
      return "far fa-file";
   }
}
