Merge pull request #663 from kijin/pr/chunked-uploads

대용량 파일 첨부 지원
This commit is contained in:
Kijin Sung 2016-12-18 00:01:06 +09:00 committed by GitHub
commit a8a85bd7cb
11 changed files with 289 additions and 64 deletions

View file

@ -256,12 +256,22 @@ class FileHandler
return $size . 'Bytes';
}
if($size >= 1024 && $size < 1024 * 1024)
if($size >= 1024 && $size < (1024 * 1024))
{
return sprintf("%0.1fKB", $size / 1024);
}
return sprintf("%0.2fMB", $size / (1024 * 1024));
if($size >= (1024 * 1024) && $size < (1024 * 1024 * 1024))
{
return sprintf("%0.2fMB", $size / (1024 * 1024));
}
if($size >= (1024 * 1024 * 1024) && $size < (1024 * 1024 * 1024 * 1024))
{
return sprintf("%0.2fGB", $size / (1024 * 1024 * 1024));
}
return sprintf("%0.2fTB", $size / (1024 * 1024 * 1024 * 1024));
}
/**
@ -426,11 +436,14 @@ class FileHandler
*/
public static function returnBytes($val)
{
$val = preg_replace('/[^0-9\.PTGMK]/', '', $val);
$unit = strtoupper(substr($val, -1));
$val = (float)$val;
switch ($unit)
{
case 'P': $val *= 1024;
case 'T': $val *= 1024;
case 'G': $val *= 1024;
case 'M': $val *= 1024;
case 'K': $val *= 1024;

View file

@ -62,24 +62,24 @@
var currentEnforce_ssl = window.enforce_ssl;
if(location.protocol == 'https:') { window.enforce_ssl = true; }
var chunkStatus = true;
var defaultFormData = {
"editor_sequence": data.editorSequence,
"upload_target_srl" : data.uploadTargetSrl,
"mid" : window.current_mid,
"act": 'procFileUpload'
};
var settings = {
url: request_uri
.setQuery('module', 'file')
.setQuery('act', 'procFileUpload')
.setQuery('mid', window.current_mid),
formData: {
"editor_sequence": data.editorSequence,
"upload_target_srl" : data.uploadTargetSrl,
"mid" : window.current_mid,
"act": 'procFileUpload'
},
url: request_uri,
formData: defaultFormData,
dropZone: $container,
add: function(e, d) {
var dfd = jQuery.Deferred();
$.each(d.files, function(index, file) {
if(data.settings.maxFileSize <= file.size) {
if(data.settings.maxFileSize > 0 && data.settings.maxFileSize < file.size) {
dfd.reject();
alert(window.xe.msg_exceeds_limit_size);
return false;
@ -91,15 +91,55 @@
d.submit();
});
},
submit: function(e, data) {
data.formData = defaultFormData;
data.formData.nonce = "T" + new Date().getTime() + "." + Math.random();
chunkStatus = true;
},
chunksend: function(e, data) {
if (!chunkStatus) {
return false;
}
},
chunkdone: function(e, res) {
if (res.result) {
if (res.result.error != 0) {
if (res.result.message) {
alert(res.result.message);
} else {
alert(window.xe.msg_file_upload_error + " (Type 1)");
}
return chunkStatus = false;
}
} else {
alert(window.xe.msg_file_upload_error + " (Type 2)");
return chunkStatus = false;
}
},
chunkfail: function(e, data) {
if (chunkStatus) {
alert(window.xe.msg_file_upload_error + " (Type 3)" + "<br>\n" + data.errorThrown + "<br>\n" + data.textStatus);
return chunkStatus = false;
}
},
done: function(e, res) {
data.settings.progressbarGraph.width('100%');
data.settings.progressPercent.text('100%');
data.settings.progressbar.delay(1000).slideUp();
data.settings.progressStatus.delay(1000).slideUp();
var result = res.response().result;
var temp_code = '';
if(!result) return;
if(!jQuery.isPlainObject(result)) result = jQuery.parseJSON(result);
if(!result) return;
if (!result) {
alert(window.xe.msg_file_upload_error + " (Type 4)");
return false;
}
if (!jQuery.isPlainObject(result)) {
result = jQuery.parseJSON(result);
}
if (!result) {
alert(window.xe.msg_file_upload_error + " (Type 5)" + "<br>\n" + res.response().result);
return false;
}
if(result.error == 0) {
if(/\.(jpe?g|png|gif)$/i.test(result.source_filename)) {
@ -108,8 +148,20 @@
}
_getCkeInstance(settings.formData.editor_sequence).insertHtml(temp_code, "unfiltered_html");
} else {
} else if (result.message) {
alert(result.message);
return false;
} else {
alert(window.xe.msg_file_upload_error + " (Type 6)" + "<br>\n" + res.response().result);
return false;
}
},
fail: function(e, data) {
data.settings.progressbar.delay(1000).slideUp();
data.settings.progressStatus.delay(1000).slideUp();
if (chunkStatus) {
alert(window.xe.msg_file_upload_error + " (Type 7)" + "<br>\n" + data.errorThrown + "<br>\n" + data.textStatus);
return false;
}
},
stop: function() {
@ -121,14 +173,9 @@
data.settings.progressbar.show();
},
progressall: function (e, d) {
var progress = parseInt(d.loaded / d.total * 100, 10);
var progress = Math.round(d.loaded / d.total * 999) / 10;
data.settings.progressbarGraph.width(progress+'%');
data.settings.progressPercent.text(progress+'%');
if(progress >= 100) {
data.settings.progressbar.delay(3000).slideUp();
data.settings.progressStatus.delay(3000).slideUp();
}
}
};
window.enforce_ssl = currentEnforce_ssl;
@ -296,6 +343,7 @@
var obj = {};
obj.mid = window.current_mid;
obj.editor_sequence = data.editorSequence;
obj.allow_chunks = 'Y';
$.exec_json('file.getFileList', obj, function(res){
data.uploadTargetSrl = res.upload_target_srl;

View file

@ -1 +0,0 @@
main.js

View file

@ -0,0 +1 @@
This file is not used in Rhymix.

View file

@ -300,11 +300,39 @@ class editorModel extends editor
$files_count = 0;
if($allow_fileupload)
{
// Get file upload limits
$oFileModel = getModel('file');
// Get upload configuration to set on SWFUploader
$file_config = $oFileModel->getUploadConfig();
$file_config->allowed_attach_size = $file_config->allowed_attach_size*1024*1024;
$file_config->allowed_filesize = $file_config->allowed_filesize*1024*1024;
if (PHP_INT_SIZE < 8)
{
$file_config->allowed_filesize = min($file_config->allowed_filesize, 2147483647);
}
$file_config->allowed_chunk_size = min(FileHandler::returnBytes(ini_get('upload_max_filesize')), FileHandler::returnBytes(ini_get('post_max_size')) * 0.95, 64 * 1024 * 1024);
if ($file_config->allowed_chunk_size > 4 * 1048576)
{
$file_config->allowed_chunk_size = floor($file_config->allowed_chunk_size / 1048576) * 1048576;
}
else
{
$file_config->allowed_chunk_size = floor($file_config->allowed_chunk_size / 65536) * 65536;
}
// Do not allow chunked uploads in IE < 10, Android browser, and Opera
$browser = Rhymix\Framework\UA::getBrowserInfo();
if (($browser->browser === 'IE' && version_compare($browser->version, '10', '<')) || $browser->browser === 'Android' || $browser->browser === 'Opera')
{
$file_config->allowed_filesize = min(FileHandler::returnBytes(ini_get('upload_max_filesize')), FileHandler::returnBytes(ini_get('post_max_size')));
$file_config->allowed_chunk_size = 0;
}
// Do not allow chunked uploads in XpressEditor.
if (starts_with($option->skin, 'xpresseditor'))
{
$file_config->allowed_filesize = min(FileHandler::returnBytes(ini_get('upload_max_filesize')), FileHandler::returnBytes(ini_get('post_max_size')));
$file_config->allowed_chunk_size = 0;
}
Context::set('file_config',$file_config);
// Configure upload status such as file size

View file

@ -55,8 +55,9 @@
jQuery(function($){
// uploader
var setting = {
maxFileSize: {$file_config->allowed_filesize},
limitMultiFileUploadSize: {$file_config->allowed_filesize}
maxFileSize: {$logged_info->is_admin === 'Y' ? 0 : $file_config->allowed_filesize},
maxChunkSize: {$file_config->allowed_chunk_size},
singleFileUploads: true
};
$container = $('#xefu-container-{$editor_sequence}');
$container.data('instance',$container.xeUploader(setting));

View file

@ -134,6 +134,7 @@ class fileAdminController extends file
else $file_config->download_grant = $download_grant;
//관리자가 허용한 첨부파일의 사이즈가 php.ini의 값보다 큰지 확인하기 - by ovclas
/*
$userFileAllowSize = FileHandler::returnbytes($file_config->allowed_filesize.'M');
$userAttachAllowSize = FileHandler::returnbytes($file_config->allowed_attach_size.'M');
$iniPostMaxSize = FileHandler::returnbytes(ini_get('post_max_size'));
@ -142,7 +143,15 @@ class fileAdminController extends file
if($userFileAllowSize > $iniMinSzie || $userAttachAllowSize > $iniMinSzie)
return new Object(-1, 'input size over than config in php.ini');
*/
if (PHP_INT_SIZE < 8)
{
if ($file_config->allowed_filesize > 2047 || $file_config->allowed_attach_size > 2047)
{
return new Object(-1, 'msg_32bit_max_2047mb');
}
}
$oModuleController = getController('module');
for($i=0;$i<count($module_srl);$i++)
{

View file

@ -29,21 +29,112 @@ class fileController extends file
$file_info = $_FILES['Filedata'];
// An error appears if not a normally uploaded file
if(!is_uploaded_file($file_info['tmp_name'])) exit();
if(!$file_info || !is_uploaded_file($file_info['tmp_name'])) exit();
// Basic variables setting
$oFileModel = getModel('file');
$editor_sequence = Context::get('editor_sequence');
$upload_target_srl = intval(Context::get('uploadTargetSrl'));
if(!$upload_target_srl) $upload_target_srl = intval(Context::get('upload_target_srl'));
$module_srl = $this->module_srl;
// Exit a session if there is neither upload permission nor information
if(!$_SESSION['upload_info'][$editor_sequence]->enabled) exit();
// Extract from session information if upload_target_srl is not specified
if(!$upload_target_srl) $upload_target_srl = $_SESSION['upload_info'][$editor_sequence]->upload_target_srl;
// Create if upload_target_srl is not defined in the session information
if(!$upload_target_srl) $_SESSION['upload_info'][$editor_sequence]->upload_target_srl = $upload_target_srl = getNextSequence();
if(!$_SESSION['upload_info'][$editor_sequence]->enabled)
{
return new Object(-1, 'msg_not_permitted');
}
// Get upload_target_srl
$upload_target_srl = intval(Context::get('uploadTargetSrl')) ?: intval(Context::get('upload_target_srl'));
if (!$upload_target_srl)
{
$upload_target_srl = $_SESSION['upload_info'][$editor_sequence]->upload_target_srl;
}
if (!$upload_target_srl)
{
$upload_target_srl = getNextSequence();
$_SESSION['upload_info'][$editor_sequence]->upload_target_srl = $upload_target_srl;
}
// Handle chunking
if (preg_match('!^bytes (\d+)-(\d+)/(\d+)$!', $_SERVER['HTTP_CONTENT_RANGE'], $matches))
{
// Check basic sanity
$chunk_start = intval($matches[1]);
$chunk_size = ($matches[2] - $matches[1]) + 1;
$total_size = intval($matches[3]);
if ($chunk_start < 0 || $chunk_size < 0 || $total_size < 0 || $chunk_start + $chunk_size > $total_size || $chunk_size != $file_info['size'])
{
return new Object(-1, 'msg_upload_invalid_chunk');
}
$this->add('chunk_current_size', $chunk_size);
$this->add('chunk_uploaded_size', $chunk_start);
// Check existing chunks
$nonce = Context::get('nonce');
$temp_key = hash_hmac('sha1', sprintf('%d:%d:%d:%s:%s', $editor_sequence, $upload_target_srl, $module_srl, $file_info['name'], $nonce), config('crypto.authentication_key'));
$temp_filename = RX_BASEDIR . 'files/attach/chunks/' . $temp_key;
if ($chunk_start == 0 && Rhymix\Framework\Storage::isFile($temp_filename))
{
Rhymix\Framework\Storage::delete($temp_filename);
$this->add('chunk_status', 11);
return new Object(-1, 'msg_upload_invalid_chunk');
}
if ($chunk_start != 0 && (!Rhymix\Framework\Storage::isFile($temp_filename) || Rhymix\Framework\Storage::getSize($temp_filename) != $chunk_start))
{
Rhymix\Framework\Storage::delete($temp_filename);
$this->add('chunk_status', 12);
return new Object(-1, 'msg_upload_invalid_chunk');
}
// Check size limit
$is_admin = (Context::get('logged_info')->is_admin === 'Y');
if (!$is_admin)
{
$module_config = getModel('file')->getFileConfig($module_srl);
$allowed_attach_size = $module_config->allowed_attach_size * 1024 * 1024;
$allowed_filesize = $module_config->allowed_filesize * 1024 * 1024;
if ($total_size > $allowed_filesize)
{
$this->add('chunk_status', 21);
return new Object(-1, 'msg_exceeds_limit_size');
}
$output = executeQuery('file.getAttachedFileSize', (object)array('upload_target_srl' => $upload_target_srl));
if (intval($output->data->attached_size) + $total_size > $allowed_attach_size)
{
$this->add('chunk_status', 22);
return new Object(-1, 'msg_exceeds_limit_size');
}
}
// Append to chunk
$fp = fopen($file_info['tmp_name'], 'r');
$success = Rhymix\Framework\Storage::write($temp_filename, $fp, 'a');
if ($success && Rhymix\Framework\Storage::getSize($temp_filename) == $chunk_start + $chunk_size)
{
$this->add('chunk_status', 0);
$this->add('chunk_uploaded_size', $chunk_start + $chunk_size);
if ($chunk_start + $chunk_size == $total_size)
{
$file_info['tmp_name'] = $temp_filename;
$file_info['size'] = Rhymix\Framework\Storage::getSize($temp_filename);
}
else
{
return new Object();
}
}
else
{
Rhymix\Framework\Storage::delete($temp_filename);
$this->add('chunk_status', 40);
return new Object(-1, 'msg_upload_invalid_chunk');
}
}
else
{
$this->add('chunk_status', -1);
}
// Save the file
$output = $this->insertFile($file_info, $module_srl, $upload_target_srl);
Context::setResponseMethod('JSON');
@ -722,26 +813,27 @@ class fileController extends file
// Set upload path by checking if the attachement is an image or other kinds of file
if(preg_match("/\.(jpe?g|gif|png|wm[va]|mpe?g|avi|swf|flv|mp[1-4]|as[fx]|wav|midi?|moo?v|qt|r[am]{1,2}|m4v)$/i", $file_info['name']))
{
$path = sprintf("./files/attach/images/%s/%s", $module_srl,getNumberingPath($upload_target_srl,3));
$path = RX_BASEDIR . sprintf("files/attach/images/%s/%s", $module_srl,getNumberingPath($upload_target_srl,3));
// special character to '_'
// change to random file name. because window php bug. window php is not recognize unicode character file name - by cherryfilter
$ext = substr(strrchr($file_info['name'],'.'),1);
//$_filename = preg_replace('/[#$&*?+%"\']/', '_', $file_info['name']);
$_filename = Rhymix\Framework\Security::getRandom(32, 'hex') . '.' . $ext;
$filename = $path.$_filename;
$idx = 1;
$filename = $path . Rhymix\Framework\Security::getRandom(32, 'hex') . '.' . $ext;
while(file_exists($filename))
{
$filename = $path.preg_replace('/\.([a-z0-9]+)$/i','_'.$idx.'.$1',$_filename);
$idx++;
$filename = $path . Rhymix\Framework\Security::getRandom(32, 'hex') . '.' . $ext;
}
$direct_download = 'Y';
}
else
{
$path = sprintf("./files/attach/binaries/%s/%s", $module_srl, getNumberingPath($upload_target_srl,3));
$path = RX_BASEDIR . sprintf("files/attach/binaries/%s/%s", $module_srl, getNumberingPath($upload_target_srl,3));
$filename = $path . Rhymix\Framework\Security::getRandom(32, 'hex');
while(file_exists($filename))
{
$filename = $path . Rhymix\Framework\Security::getRandom(32, 'hex');
}
$direct_download = 'N';
}
@ -757,18 +849,34 @@ class fileController extends file
@copy($file_info['tmp_name'], $filename);
if(!file_exists($filename))
{
$filename = $path . Rhymix\Framework\Security::getRandom(32, 'hex') . '.' . $ext;
@copy($file_info['tmp_name'], $filename);
if(!file_exists($filename))
{
return new Object(-1,'msg_file_upload_error');
}
}
}
elseif(starts_with(RX_BASEDIR . 'files/attach/chunks/', $file_info['tmp_name']))
{
if (!Rhymix\Framework\Storage::move($file_info['tmp_name'], $filename))
{
if (!Rhymix\Framework\Storage::move($file_info['tmp_name'], $filename))
{
return new Object(-1,'msg_file_upload_error');
}
}
}
else
{
if(!@move_uploaded_file($file_info['tmp_name'], $filename))
{
$filename = $path . Rhymix\Framework\Security::getRandom(32, 'hex') . '.' . $ext;
if(!@move_uploaded_file($file_info['tmp_name'], $filename)) return new Object(-1,'msg_file_upload_error');
if(!@move_uploaded_file($file_info['tmp_name'], $filename))
{
return new Object(-1,'msg_file_upload_error');
}
}
}
// Get member information
$oMemberModel = getModel('member');
$member_srl = $oMemberModel->getLoggedMemberSrl();
@ -877,11 +985,11 @@ class fileController extends file
$output = executeQuery('file.deleteFile', $args);
if(!$output->toBool()) return $output;
// Call a trigger (after)
ModuleHandler::triggerCall('file.deleteFile', 'after', $trigger_obj);
// If successfully deleted, remove the file
FileHandler::removeFile($uploaded_filename);
// Call a trigger (after)
ModuleHandler::triggerCall('file.deleteFile', 'after', $trigger_obj);
}
$oDocumentController->updateUploaedCount($documentSrlList);

View file

@ -175,6 +175,7 @@ class fileModel extends file
if(!$config->allow_outlink) $config->allow_outlink = 'Y';
if(!$config->download_grant) $config->download_grant = array();
/*
$size = ini_get('upload_max_filesize');
$unit = strtolower($size[strlen($size) - 1]);
$size = (float)$size;
@ -189,6 +190,7 @@ class fileModel extends file
{
$config->allowed_attach_size = $size;
}
*/
return $config;
}
@ -286,11 +288,13 @@ class fileModel extends file
if($logged_info->is_admin == 'Y')
{
/*
$iniPostMaxSize = FileHandler::returnbytes(ini_get('post_max_size'));
$iniUploadMaxSize = FileHandler::returnbytes(ini_get('upload_max_filesize'));
$size = min($iniPostMaxSize, $iniUploadMaxSize) / 1048576;
$file_config->allowed_attach_size = $size;
$file_config->allowed_filesize = $size;
*/
$file_config->allowed_filetypes = '*.*';
}
return $file_config;
@ -305,6 +309,15 @@ class fileModel extends file
function getUploadStatus($attached_size = 0)
{
$file_config = $this->getUploadConfig();
if (Context::get('allow_chunks') === 'Y')
{
$allowed_filesize = $file_config->allowed_filesize * 1024 * 1024;
}
else
{
$allowed_filesize = min(FileHandler::returnBytes(ini_get('upload_max_filesize')), FileHandler::returnBytes(ini_get('post_max_size')));
}
// Display upload status
$upload_status = sprintf(
'%s : %s/ %s<br /> %s : %s (%s : %s)',
@ -312,7 +325,7 @@ class fileModel extends file
FileHandler::filesize($attached_size),
FileHandler::filesize($file_config->allowed_attach_size*1024*1024),
lang('allowed_filesize'),
FileHandler::filesize($file_config->allowed_filesize*1024*1024),
FileHandler::filesize($allowed_filesize),
lang('allowed_filetypes'),
$file_config->allowed_filetypes
);

View file

@ -18,8 +18,9 @@ $lang->enable_download_group = 'Downloadable Groups';
$lang->about_allow_outlink = 'You can block external links according to referers(except media files like *.wmv and *.mp3).';
$lang->about_allow_outlink_format = 'These formats will always be allowed. Please use comma(,) for multiple input. eg)hwp,doc,zip,pdf';
$lang->about_allow_outlink_site = 'These websites will alyways be allowed. Please use new line for multiple input. ex)https://www.rhymix.org/';
$lang->about_allowed_filesize = 'You can assign file size limit for each file.(Exclude administrators)';
$lang->about_allowed_attach_size = 'You can assign file size limit for each document.(Exclude administrators)';
$lang->about_allowed_filesize = 'You can limit the size of each attached file. Administrators are exempt.';
$lang->about_allowed_attach_size = 'You can limit the total size of all attached files in one document. Administrators are exempt.';
$lang->about_allowed_size_limits = 'The file size will be limited to the value set in php.ini (%sB) in IE9 and below and older Android browsers.';
$lang->about_allowed_filetypes = 'To allow an extension, use "*.[extention]". To allow multiple extensions, use ";" between each extension. ex) *.* or *.jpg;*.gif; ';
$lang->cmd_delete_checked_file = 'Delete Selected Item(s)';
$lang->cmd_move_to_document = 'Move to Document';
@ -44,6 +45,8 @@ $lang->file_search_target_list['isvalid'] = 'Status';
$lang->msg_not_allowed_outlink = 'It is not allowed to download files from sites other than this.';
$lang->msg_not_permitted_create = 'Failed to create a file or directory.';
$lang->msg_file_upload_error = 'An error has occurred during uploading.';
$lang->msg_upload_invalid_chunk = 'An error has occurred during chunked uploading.';
$lang->msg_32bit_max_2047mb = 'On 32-bit servers, the file size limit cannot exceed 2047MB.';
$lang->no_files = 'No Files';
$lang->file_manager = 'Manage selected files';
$lang->selected_file = 'Selected files';

View file

@ -18,8 +18,9 @@ $lang->enable_download_group = '다운로드 가능 그룹';
$lang->about_allow_outlink = '리퍼러에 따라 파일 외부 링크를 차단할 수 있습니다.(*.wmv, *.mp3등 미디어 파일 제외)';
$lang->about_allow_outlink_format = '파일 외부 링크 설정에 상관없이 허용하는 파일 확장자입니다. 여러 개 입력 시에 쉼표(,)을 이용해서 구분해주세요. 예)hwp,doc,zip,pdf';
$lang->about_allow_outlink_site = '파일 외부 링크 설정에 상관없이 허용하는 사이트 주소입니다. 여러 개 입력 시에 줄을 바꿔서 구분해주세요. 예)https://www.rhymix.org/';
$lang->about_allowed_filesize = '하나의 파일에 대해 최고 용량을 지정할 수 있습니다.(관리자는 제외)';
$lang->about_allowed_attach_size = '하나의 문서에 첨부할 수 있는 최고 용량을 지정할 수 있습니다.(관리자는 제외)';
$lang->about_allowed_filesize = '각 파일의 용량을 제한할 수 있습니다. 관리자에게는 적용되지 않습니다.';
$lang->about_allowed_attach_size = '하나의 문서에 첨부할 수 있는 최대 용량을 제한할 수 있습니다. 관리자에게는 적용되지 않습니다.';
$lang->about_allowed_size_limits = 'IE9 이하, 구버전 안드로이드 등에서는 php.ini에서 지정한 %sB로 제한됩니다.';
$lang->about_allowed_filetypes = '"*.확장자"로 지정할 수 있고 ";" 으로 여러 개 지정이 가능합니다. 예) *.* or *.jpg;*.gif;';
$lang->cmd_delete_checked_file = '선택항목 삭제';
$lang->cmd_move_to_document = '문서로 이동';
@ -45,6 +46,8 @@ $lang->file_search_target_list['isvalid'] = '상태';
$lang->msg_not_allowed_outlink = '외부링크에서 다운로드할 수 없습니다.';
$lang->msg_not_permitted_create = '파일 또는 디렉터리를 생성할 수 없습니다.';
$lang->msg_file_upload_error = '파일 업로드 중 에러가 발생하였습니다.';
$lang->msg_upload_invalid_chunk = '분할 업로드 처리 중 오류가 발생하였습니다.';
$lang->msg_32bit_max_2047mb = '32비트 서버에서는 파일 크기 제한이 2047MB를 초과할 수 없습니다.';
$lang->no_files = '파일이 없습니다.';
$lang->file_manager = '선택한 파일 관리';
$lang->selected_file = '선택한 파일';

View file

@ -10,23 +10,22 @@
<div class="x_control-group">
<label for="allowed_filesize" class="x_control-label">{$lang->allowed_filesize}</label>
<div class="x_controls">
<input type="number" min="0" name="allowed_filesize" id="allowed_filesize" value="{$file_config->allowed_filesize}" size="3" /> MB
<p class="x_help-inline">{$lang->about_allowed_filesize}</p>
<input type="number" min="0" name="allowed_filesize" id="allowed_filesize" value="{$file_config->allowed_filesize}" size="7" style="min-width:80px" /> MB
<p class="x_help-block">{$lang->about_allowed_filesize}<br />{sprintf($lang->about_allowed_size_limits, ini_get('upload_max_filesize'))}</p>
</div>
</div>
<div class="x_control-group">
<label for="allowed_attach_size" class="x_control-label">{$lang->allowed_attach_size}</label>
<div class="x_controls">
<input type="number" min="0" name="allowed_attach_size" id="allowed_attach_size" value="{$file_config->allowed_attach_size}" size="3" /> MB
/ {ini_get('upload_max_filesize')}
<p class="x_help-inline">{$lang->about_allowed_attach_size}</p>
<input type="number" min="0" name="allowed_attach_size" id="allowed_attach_size" value="{$file_config->allowed_attach_size}" size="7" style="min-width:80px" /> MB
<p class="x_help-block">{$lang->about_allowed_attach_size}<br />{sprintf($lang->about_allowed_size_limits, ini_get('upload_max_filesize'))}</p>
</div>
</div>
<div class="x_control-group">
<label for="allowed_filetypes" class="x_control-label">{$lang->allowed_filetypes}</label>
<div class="x_controls">
<input type="text" name="allowed_filetypes" id="allowed_filetypes" value="{$file_config->allowed_filetypes}" />
<p class="x_help-inline">{$lang->about_allowed_filetypes}</p>
<p class="x_help-block">{$lang->about_allowed_filetypes}</p>
</div>
</div>
<div class="x_control-group">