Commit d52a27ba authored by Cédric Anne's avatar Cédric Anne Committed by Johan Cwiklinski
Browse files

Handle max upload limit; see #8176

- Use fileupload even when pasting in rich text editor
- Split uploads into chuncks to bypass PHP upload size limit
- Add configurable limit and add browser side validation
parent 346f9ff4
......@@ -2255,6 +2255,7 @@ class Config extends CommonDBTM {
2 => __('Default values'), // Prefs
3 => __('Assets'),
4 => __('Assistance'),
12 => __('Management'),
];
if (Config::canUpdate()) {
$tabs[9] = __('Logs purge');
......@@ -2342,6 +2343,10 @@ class Config extends CommonDBTM {
case 11:
Impact::showConfigForm();
break;
case 12:
$item->showFormManagement();
break;
}
}
return true;
......@@ -3404,6 +3409,67 @@ class Config extends CommonDBTM {
Html::closeForm();
}
/**
* Security form related to management entries.
*
* @since x.x.x
*
* @return void|boolean (display) Returns false if there is a rights error.
*/
function showFormManagement () {
global $CFG_GLPI;
if (!self::canView()) {
return;
}
$rand = mt_rand();
$canedit = Session::haveRight(self::$rightname, UPDATE);
echo '<div class="center" id="tabsbody">';
if ($canedit) {
echo '<form name="form" action="' . Toolbox::getItemTypeFormURL(__CLASS__) . '" method="post" data-track-changes="true">';
}
echo '<table class="tab_cadre_fixe">';
echo '<tr><th colspan="4">' . __('Documents setup') . '</th></tr>';
echo '<tr class="tab_bg_2">';
echo '<td>';
echo '<label for="dropdown_document_max_size' . $rand . '">';
echo __('Document files maximum size (Mio)');
echo '</label>';
echo '</td>';
echo '<td>';
Dropdown::showNumber(
'document_max_size',
[
'value' => $CFG_GLPI['document_max_size'],
'min' => 1,
'max' => 250,
'rand' => $rand,
]
);
echo '</td>';
echo '<td colspan="2"></td>';
echo '</tr>';
if ($canedit) {
echo '<tr class="tab_bg_2">';
echo '<td colspan="4" class="center">';
echo '<input type="submit" name="update" class="submit" value="' . _sx('button', 'Save') . '">';
echo '</td>';
echo '</tr>';
}
echo '</table>';
if ($canedit) {
Html::closeForm();
}
echo '</div>';
}
public function rawSearchOptions() {
$tab = [];
......
......@@ -448,11 +448,10 @@ class Document extends CommonDBTM {
* Get max upload size from php config
**/
static function getMaxUploadSize() {
global $CFG_GLPI;
$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));
return sprintf(__('%s Mio max'), $CFG_GLPI['document_max_size']);
}
......
......@@ -183,4 +183,36 @@ class DocumentType extends CommonDropdown {
return $display;
}
}
/**
* Return pattern that can be used to validate that name of an uploaded file matches accepted extensions.
*
* @return string
*/
public static function getUploadableFilePattern(): string {
global $DB;
$valid_type_iterator = $DB->request([
'FROM' => 'glpi_documenttypes',
'WHERE' => [
'is_uploadable' => 1
]
]);
$valid_ext_patterns = [];
foreach ($valid_type_iterator as $valid_type) {
$valid_ext = $valid_type['ext'];
if (preg_match('/\/.+\//', $valid_ext)) {
// Filename matches pattern
// Remove surrounding '/' as it will be included in a larger pattern
// and protect by surrounding parenthesis to prevent conflict with other patterns
$valid_ext_patterns[] = '(' . substr($valid_ext, 1, -1) . ')';
} else {
// Filename ends with allowed ext
$valid_ext_patterns[] = '\.' . preg_quote($valid_type['ext'], '/') . '$';
}
}
return '/(' . implode('|', $valid_ext_patterns) . ')/i';
}
}
......@@ -120,7 +120,7 @@ class GLPIUploadHandler extends UploadHandler {
static function uploadFiles($params = []) {
global $DB;
global $CFG_GLPI, $DB;
$default_params = [
'name' => '',
......@@ -135,37 +135,17 @@ class GLPIUploadHandler extends UploadHandler {
$name = $rand_name . $name;
}
$valid_type_iterator = $DB->request([
'FROM' => 'glpi_documenttypes',
'WHERE' => [
'is_uploadable' => 1
]
]);
$valid_ext_patterns = [];
foreach ($valid_type_iterator as $valid_type) {
$valid_ext = $valid_type['ext'];
if (preg_match('/\/.+\//', $valid_ext)) {
// Filename matches pattern
// Remove surrounding '/' as it will be included in a larger pattern
// and protect by surrounding parenthesis to prevent conflict with other patterns
$valid_ext_patterns[] = '(' . substr($valid_ext, 1, -1) . ')';
} else {
// Filename ends with allowed ext
$valid_ext_patterns[] = '\.' . preg_quote($valid_type['ext'], '/') . '$';
}
}
$upload_dir = GLPI_TMP_DIR.'/';
$upload_handler = new self(
[
'accept_file_types' => '/(' . implode('|', $valid_ext_patterns) . ')/i',
'accept_file_types' => DocumentType::getUploadableFilePattern(),
'image_versions' => [
'auto_orient' => false,
],
'param_name' => $pname,
'replace_dots_in_filenames' => false,
'upload_dir' => $upload_dir,
'max_file_size' => $CFG_GLPI['document_max_size'] * 1024 * 1024,
],
false
);
......
......@@ -5525,8 +5525,12 @@ JAVASCRIPT;
* @param array $options theses following keys:
* - editor_id the dom id of the tinymce editor
* @return string The Html
*
* @deprecated x.x
*/
static function fileForRichText($options = []) {
Toolbox::deprecated();
$p['editor_id'] = '';
$p['name'] = 'filename';
$p['filecontainer'] = 'fileupload_info';
......@@ -5585,6 +5589,7 @@ JAVASCRIPT;
* - showtitle boolean show the title above file list
* (with max upload size indication)
* - enable_richtext boolean switch to richtext fileupload
* - editor_id string id attribute for the richtext editor
* - pasteZone string DOM ID of the paste zone
* - dropZone string DOM ID of the drop zone
* - rand string already computed rand value
......@@ -5610,6 +5615,7 @@ JAVASCRIPT;
$p['display'] = true;
$p['multiple'] = false;
$p['uploads'] = [];
$p['editor_id'] = null;
if (is_array($options) && count($options)) {
foreach ($options as $key => $val) {
......@@ -5618,7 +5624,7 @@ JAVASCRIPT;
}
$display = "";
$display .= "<div class='fileupload draghoverable'>";
$display .= "<div class='fileupload draghoverable' id='{$p['dropZone']}'>";
if ($p['showtitle']) {
$display .= "<b>";
......@@ -5634,87 +5640,73 @@ JAVASCRIPT;
'uploads' => $p['uploads'],
]);
if (!empty($p['editor_id'])
&& $p['enable_richtext']) {
$options_rt = $options;
$options_rt['display'] = false;
$display .= self::fileForRichText($options_rt);
} else {
$max_file_size = $CFG_GLPI['document_max_size'] * 1024 * 1024;
$max_chunk_size = round(Toolbox::getPhpUploadSizeLimit() * 0.9); // keep some place for extra data
// manage file upload without tinymce editor
$display .= "<div id='{$p['dropZone']}'>";
$display .= "<span class='b'>".__('Drag and drop your file here, or').'</span><br>';
$display .= "<input id='fileupload{$p['rand']}' type='file' name='".$p['name']."[]'
data-url='".$CFG_GLPI["root_doc"]."/ajax/fileupload.php'
data-form-data='{\"name\": \"".$p['name']."\",
\"showfilesize\": \"".$p['showfilesize']."\"}'"
.($p['multiple']?" multiple='multiple'":"")
.($p['onlyimages']?" accept='.gif,.png,.jpg,.jpeg'":"").">";
$display .= "<div id='progress{$p['rand']}' style='display:none'>".
"<div class='uploadbar' style='width: 0%;'></div></div>";
$display .= "</div>";
// manage file upload without tinymce editor
$display .= "<span class='b'>".__('Drag and drop your file here, or').'</span><br>';
$display .= "<input id='fileupload{$p['rand']}' type='file' name='".$p['name']."[]'
data-url='".$CFG_GLPI["root_doc"]."/ajax/fileupload.php'
data-form-data='{\"name\": \"".$p['name']."\", \"showfilesize\": \"".$p['showfilesize']."\"}'"
.($p['multiple']?" multiple='multiple'":"")
.($p['onlyimages']?" accept='.gif,.png,.jpg,.jpeg'":"").">";
$display .= "<div id='progress{$p['rand']}' style='display:none'>".
"<div class='uploadbar' style='width: 0%;'></div></div>";
$display .= Html::scriptBlock("
$(function() {
var fileindex{$p['rand']} = 0;
$('#fileupload{$p['rand']}').fileupload({
dataType: 'json',
pasteZone: ".($p['pasteZone'] !== false
? "$('#{$p['pasteZone']}')"
: "false").",
dropZone: ".($p['dropZone'] !== false
? "$('#{$p['dropZone']}')"
: "false").",
acceptFileTypes: ".($p['onlyimages']
? "'/(\.|\/)(gif|jpe?g|png)$/i'"
: "undefined").",
progressall: function(event, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$('#progress{$p['rand']}')
.show()
.filter('.uploadbar')
.css({
width: progress + '%'
})
.text(progress + '%')
.show();
},
done: function (event, data) {
var filedata = data;
// Load image tag, and display image uploaded
$.ajax({
type: 'POST',
url: '".$CFG_GLPI['root_doc']."/ajax/getFileTag.php',
data: {
data: data.result.{$p['name']}
},
dataType: 'JSON',
success: function(tag) {
$.each(filedata.result.{$p['name']}, function(index, file) {
if (file.error === undefined) {
//create a virtual editor to manage filelist, see displayUploadedFile()
var editor = {
targetElm: $('#fileupload{$p['rand']}')
};
displayUploadedFile(file, tag[index], editor, '{$p['name']}');
$('#progress{$p['rand']} .uploadbar')
.text('".addslashes(__('Upload successful'))."')
.css('width', '100%')
.delay(2000)
.fadeOut('slow');
} else {
$('#progress{$p['rand']} .uploadbar')
.text(file.error)
.css('width', '100%');
}
});
$display .= Html::scriptBlock("
$(function() {
var fileindex{$p['rand']} = 0;
$('#fileupload{$p['rand']}').fileupload({
dataType: 'json',
pasteZone: ".($p['pasteZone'] !== false
? "$('#{$p['pasteZone']}')"
: "false").",
dropZone: ".($p['dropZone'] !== false
? "$('#{$p['dropZone']}')"
: "false").",
acceptFileTypes: ".($p['onlyimages']
? "'/(\.|\/)(gif|jpe?g|png)$/i'"
: DocumentType::getUploadableFilePattern()).",
maxFileSize: {$max_file_size},
maxChunkSize: {$max_chunk_size},
progressall: function(event, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$('#progress{$p['rand']}').show();
$('#progress{$p['rand']} .uploadbar')
.text(progress + '%')
.css('width', progress + '%')
.show();
},
done: function (event, data) {
handleUploadedFile(
data.files, // files as blob
data.result.{$p['name']}, // response from '/ajax/fileupload.php'
'{$p['name']}',
$('#{$p['filecontainer']}'),
'{$p['editor_id']}'
);
},
processfail: function (e, data) {
$.each(
data.files,
function(index, file) {
if (file.error) {
$('#progress{$p['rand']}').show();
$('#progress{$p['rand']} .uploadbar')
.text(file.error)
.css('width', '100%');
return;
}
});
}
});
});");
}
}
);
},
messages: {
acceptFileTypes: __('Filetype not allowed'),
maxFileSize: __('File is too big'),
}
});
});");
$display .= "</div>"; // .fileupload
if ($p['display']) {
......
......@@ -1761,6 +1761,19 @@ class Toolbox {
return $val;
}
/**
* Get max upload size from php config.
*
* @return int
*/
static function getPhpUploadSizeLimit(): int {
$post_max = Toolbox::return_bytes_from_ini_vars(ini_get("post_max_size"));
$upload_max = Toolbox::return_bytes_from_ini_vars(ini_get("upload_max_filesize"));
$max_size = $post_max > 0 ? min($post_max, $upload_max) : $upload_max;
return $max_size;
}
/**
* Parse imap open connect string
*
......
......@@ -293,7 +293,9 @@ $default_prefs = [
'default_dashboard_mini_ticket' => 'mini_tickets',
'admin_email_noreply' => '',
'admin_email_noreply_name' => '',
Impact::CONF_ENABLED => exportArrayToDB(Impact::getDefaultItemtypes())
Impact::CONF_ENABLED => exportArrayToDB(Impact::getDefaultItemtypes()),
// Default size corresponds to the 'upload_max_filesize' directive in Mio (rounded down) or 1 Mio if 'upload_max_filesize' is too low.
'document_max_size' => max(1, floor(Toolbox::return_bytes_from_ini_vars(ini_get('upload_max_filesize')) / 1024 / 1024)),
];
$tables['glpi_configs'] = [];
......
......@@ -49,12 +49,13 @@ function update95toXX() {
$update_scripts = [
'comment_fields',
'devicebattery',
'documents',
'domains',
'native_inventory',
'recurrentchange',
'reservationitem',
'softwares',
'recurrentchange',
'uuids'
'uuids',
];
foreach ($update_scripts as $update_script) {
......
<?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/>.
* ---------------------------------------------------------------------
*/
/**
* @var DB $DB
* @var Migration $migration
*/
// Add a config entry for document max size.
// Default size corresponds to the 'upload_max_filesize' directive in Mio (rounded down)
// or 1 Mio if 'upload_max_filesize' is too low.
$upload_max = Toolbox::return_bytes_from_ini_vars(ini_get('upload_max_filesize'));
$migration->addConfig(['document_max_size' => max(1, floor($upload_max / 1024 / 1024))]);
......@@ -31,42 +31,14 @@
/* global fileType, getExtIcon, getSize, isImage, stopEvent, Uint8Array */
function uploadFile(file, editor, input_name) {
var returnTag = false;
//Create formdata from file to send with ajax request
var formdata = new FormData();
formdata.append('filename[0]', file, file.name);
formdata.append('name', 'filename');
// upload file with ajax
$.ajax({
type: 'POST',
url: CFG_GLPI.root_doc+'/ajax/fileupload.php',
data: formdata,
processData: false,
contentType: false,
dataType: 'JSON',
async: false,
success: function(data) {
$.each(data, function(index, element) {
if (element[0].error === undefined) {
returnTag = '';
var tag = getFileTag(element);
//if is an image add tag
if (isImage(file)) {
returnTag = tag.tag;
}
//display uploaded file
displayUploadedFile(element[0], tag, editor, input_name);
} else {
returnTag = false;
alert(element[0].error);
}
});
},
var insertIntoEditor = []; // contains flags that indicate if uploaded file (image) should be added to editor contents
error: function (request) {
function uploadFile(file, editor) {
insertIntoEditor[file.name] = isImage(file);
$(editor.getElement()).siblings('.fileupload').find('[type="file"]')
.fileupload('send', {files: [file]})
.error(function (request) {
// If this is an error on the return
if ("responseText" in request && request.responseText.length > 0) {
alert(request.responseText);
......@@ -74,37 +46,58 @@ function uploadFile(file, editor, input_name) {
// Error before sending request #3866
alert(request.statusText);
}
}
});
return returnTag;
});
}
/**
* Gets the file tag.
*
* @param {(boolean|string)} data receive from uploadFile
* @return {(boolean|string)} The file tag.
*/
var getFileTag = function(data) {
var returnString = '';
$.ajax({
type: 'POST',
url: CFG_GLPI.root_doc+'/ajax/getFileTag.php',
data: {'data':data},
dataType: 'JSON',
async: false,
success: function(data) {
returnString = data[0];
},
error: function (request) {
console.warn(request.responseText);
returnString=false;
}
});
var handleUploadedFile = function (files, files_data, input_name, container, editor_id) {
$.ajax(
{
type: 'POST',
url: CFG_GLPI.root_doc + '/ajax/getFileTag.php',
data: {data: files_data},
dataType: 'JSON',
success: function(tags) {
$.each(
files,
function(index, file) {
if (files_data[index].error !== undefined) {
container.parent().find('.uploadbar')
.text(files_data[index].error)
.css('width', '100%');
return;
}
var tag_data = tags[index];
var editor = null;
if (editor_id && Object.prototype.hasOwnProperty.call(insertIntoEditor, file.name) && insertIntoEditor[file.name]) {
editor = tinyMCE.get(editor_id);
insertImgFromFile(editor, file, tag_data.tag);
}
displayUploadedFile(files_data[index], tag_data, editor, input_name, container);
return returnString;
container.parent().find('.uploadbar')
.text(__('Upload successful'))
.css('width', '100%')
.delay(2000)
.fadeOut('slow');
}
);
},
error: function (request) {
console.warn(request.responseText);
},
complete: function () {
$.each(
files,
function(index, file) {
delete(insertIntoEditor[file.name]);
}
);
}
}
);
};
/**
......@@ -114,63 +107,49 @@ var getFileTag = function(data) {
* @param {String} tag The tag
* @param {Object} editor The TinyMCE editor instance
* @param {String} input_name Name of generated input hidden (default filename)
* @param {Object} container The fileinfo container
*/
var fileindex = 0;
var displayUploadedFile = function(file, tag, editor, input_name) {
// default argument(s)
input_name = (typeof input_name === 'undefined' || input_name == null) ? 'filename' : input_name;