.
* ---------------------------------------------------------------------
*/
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";
}
}