. * --------------------------------------------------------------------- */ 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"]; } /* 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.'). " : ".$data['name']."", 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']); 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 ""; if ($this->fields["users_id"]>0) { printf(__('Added by %s'), getUserName($this->fields["users_id"], $showuserlink)); } else { echo " "; } echo ""; echo ""; //TRANS: %s is the datetime of update printf(__('Last update on %s'), Html::convDateTime($this->fields["date_mod"])); echo "\n"; } echo ""; echo "".__('Name').""; echo ""; Html::autocompletionTextField($this, "name"); echo ""; if ($ID > 0) { echo "".__('Current file').""; echo "".$this->getDownloadLink('', 45); echo ""; echo ""; echo ""; } else { echo " "; } echo ""; echo ""; echo "".__('Heading').""; echo ""; DocumentCategory::dropdown(['value' => $this->fields["documentcategories_id"]]); echo ""; if ($ID > 0) { echo "".sprintf(__('%1$s (%2$s)'), __('Checksum'), __('SHA1')).""; echo "".$this->fields["sha1sum"]; echo ""; } else { echo " "; } echo ""; echo ""; echo "".__('Web link').""; echo ""; Html::autocompletionTextField($this, "link"); echo ""; echo "".__('Comments').""; echo ""; echo ""; echo ""; echo ""; echo "".__('MIME type').""; echo ""; Html::autocompletionTextField($this, "mime"); echo ""; echo ""; echo "".__('Blacklisted for import').""; echo ""; Dropdown::showYesNo("is_blacklisted", $this->fields["is_blacklisted"]); echo ""; echo ""; echo "".__('Use a FTP installed file').""; echo ""; $this->showUploadedFilesDropdown("upload_file"); echo ""; echo "".sprintf(__('%1$s (%2$s)'), __('File'), self::getMaxUploadSize()).""; echo ""; Html::file(); echo ""; $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)."…"; } $out = ''; $open = ''; $close = ''; if (self::canView() || $this->canViewFile(['tickets_id' => $this->fields['tickets_id']])) { $open = ""; $close = ""; } $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 .= " \""."; } } $out .= "$open$fileout$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 .= " " . __('Manage document types') . ""; } 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 .= ""; $out .= "\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'] = "". _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 = 400; $mheight = 300; break; case 'timeline': $mwidth = 100; $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"; } }