diff --git a/include/ajax.draft.php b/include/ajax.draft.php index e24be81efc5ef46f44527bbb2333bf7cdb3a859f..41fde2be24ff955b793f1fbf773b9d8372616c0b 100644 --- a/include/ajax.draft.php +++ b/include/ajax.draft.php @@ -67,6 +67,8 @@ class DraftAjaxAPI extends AjaxController { } function _uploadInlineImage($draft) { + global $cfg; + if (!isset($_POST['data']) && !isset($_FILES['file'])) Http::response(422, "File not included properly"); @@ -76,9 +78,26 @@ class DraftAjaxAPI extends AjaxController { $_FILES['image'][$k] = array($v); unset($_FILES['file']); - $file = AttachmentFile::format($_FILES['image'], true); + $file = AttachmentFile::format($_FILES['image']); # TODO: Detect unacceptable attachment extension # TODO: Verify content-type and check file-content to ensure image + $type = $file[0]['type']; + if (strpos($file[0]['type'], 'image/') !== 0) + return Http::response(403, + JsonDataEncoder::encode(array( + 'error' => 'File type is not allowed', + )) + ); + + # TODO: Verify file size is acceptable + if ($file[0]['size'] > $cfg->getMaxFileSize()) + return Http::response(403, + JsonDataEncoder::encode(array( + 'error' => 'File is too large', + )) + ); + + if (!($ids = $draft->attachments->upload($file))) { if ($file[0]['error']) { return Http::response(403, diff --git a/include/ajax.forms.php b/include/ajax.forms.php index da6bb6560724604e4603925ec12dd1fbfd971315..070606298089eb337009a6203dcddd97dcf3d6c6 100644 --- a/include/ajax.forms.php +++ b/include/ajax.forms.php @@ -92,14 +92,14 @@ class DynamicFormsAjaxAPI extends AjaxController { Http::response(400, 'Upload to a non file-field'); return JsonDataEncoder::encode( - array('id'=>$impl->upload()) + array('id'=>$impl->ajaxUpload()) ); } function attach() { $field = new FileUploadField(); return JsonDataEncoder::encode( - array('id'=>$field->upload()) + array('id'=>$field->ajaxUpload(true)) ); } } diff --git a/include/api.tickets.php b/include/api.tickets.php index ae0450a2d869c68bd2ccfcc1bd7a427acbd72458..47d978a498aac405381d4f5659f2b8072d092da6 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -60,30 +60,33 @@ class TicketApiController extends ApiController { if(!parent::validate($data, $format, $strict) && $strict) $this->exerr(400, __('Unexpected or invalid data received')); - //Nuke attachments IF API files are not allowed. - if(!$ost->getConfig()->allowAPIAttachments()) + // Use the settings on the thread entry on the ticket details + // form to validate the attachments in the email + $tform = TicketForm::objects()->one()->getForm(); + $messageField = $tform->getField('message'); + $fileField = $messageField->getWidget()->getAttachments(); + + // Nuke attachments IF API files are not allowed. + if (!$messageField->isAttachmentsEnabled()) $data['attachments'] = array(); //Validate attachments: Do error checking... soft fail - set the error and pass on the request. - if($data['attachments'] && is_array($data['attachments'])) { - foreach($data['attachments'] as &$attachment) { - if(!$ost->isFileTypeAllowed($attachment)) - $attachment['error'] = sprintf(__('Invalid file type (ext) for %s'),Format::htmlchars($attachment['name'])); - elseif ($attachment['encoding'] && !strcasecmp($attachment['encoding'], 'base64')) { - if(!($attachment['data'] = base64_decode($attachment['data'], true))) - $attachment['error'] = sprintf(__('%s: Poorly encoded base64 data'), Format::htmlchars($attachment['name'])); + if ($data['attachments'] && is_array($data['attachments'])) { + foreach($data['attachments'] as &$file) { + if ($file['encoding'] && !strcasecmp($file['encoding'], 'base64')) { + if(!($file['data'] = base64_decode($file['data'], true))) + $file['error'] = sprintf(__('%s: Poorly encoded base64 data'), + Format::htmlchars($file['name'])); } - if (!$attachment['error'] - && ($size = $ost->getConfig()->getMaxFileSize()) - && ($fsize = $attachment['size'] ?: strlen($attachment['data'])) - && $fsize > $size) { - $attachment['error'] = sprintf('File %s (%s) is too big. Maximum of %s allowed', - Format::htmlchars($attachment['name']), - Format::file_size($fsize), - Format::file_size($size)); + // Validate and save immediately + try { + $file['id'] = $fileField->uploadAttachment($file); + } + catch (FileUploadError $ex) { + $file['error'] = $file['name'] . ': ' . $ex->getMessage(); } } - unset($attachment); + unset($file); } return true; diff --git a/include/class.file.php b/include/class.file.php index 702936c7d5204caa2ab40aa1115947ae7aff9d04..15f4ea17a0f8dc294672bfa4d695a7a12dc1ff09 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -534,9 +534,8 @@ class AttachmentFile { /* Method formats http based $_FILE uploads - plus basic validation. - @restrict - make sure file type & size are allowed. */ - function format($files, $restrict=false) { + function format($files) { global $ost; if(!$files || !is_array($files)) @@ -562,16 +561,6 @@ class AttachmentFile { $file['error'] = 'File upload error #'.$file['error']; elseif(!$file['tmp_name'] || !is_uploaded_file($file['tmp_name'])) $file['error'] = 'Invalid or bad upload POST'; - elseif($restrict) { // make sure file type & size are allowed. - if(!$ost->isFileTypeAllowed($file)) - $file['error'] = 'Invalid file type for '.Format::htmlchars($file['name']); - elseif($ost->getConfig()->getMaxFileSize() - && $file['size']>$ost->getConfig()->getMaxFileSize()) - $file['error'] = sprintf('File %s (%s) is too big. Maximum of %s allowed', - Format::htmlchars($file['name']), - Format::file_size($file['size']), - Format::file_size($ost->getConfig()->getMaxFileSize())); - } } unset($file); diff --git a/include/class.forms.php b/include/class.forms.php index 790a48be9c5681c34ef2d7654e42915e253e0eab..b994c738faa1b7b3989e0c0fccd800542823da53 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -997,12 +997,17 @@ class ThreadEntryField extends FormField { 'label'=>__('Enable Attachments'), 'default'=>$cfg->allowOnlineAttachments(), 'configuration'=>array( - 'desc'=>__('Enables attachments submitted with the issue details'), + 'desc'=>__('Enables attachments on tickets, regardless of channel'), ), )), ) + $attachments->getConfigurationOptions(); } + + function isAttachmentsEnabled() { + $config = $this->getConfiguration(); + return $config['attachments']; + } } class PriorityField extends ChoiceField { @@ -1278,7 +1283,10 @@ class FileUploadField extends FormField { ); } - function upload() { + /** + * Called from the ajax handler for async uploads via web clients. + */ + function ajaxUpload($bypass=false) { $config = $this->getConfiguration(); $files = AttachmentFile::format($_FILES['upload'], @@ -1289,8 +1297,12 @@ class FileUploadField extends FormField { $file = array_shift($files); $file['name'] = urldecode($file['name']); - // TODO: Check allowed type / size. - // Return HTTP/413, 415, 417 or similar + if (!$bypass && !$this->isValidFileType($file['name'])) + Http::response(415, 'File type is not allowed'); + + $config = $this->getConfiguration(); + if (!$bypass && $file['size'] > $config['size']) + Http::response(413, 'File is too large'); if (!($id = AttachmentFile::upload($file))) Http::response(500, 'Unable to store file: '. $file['error']); @@ -1298,6 +1310,65 @@ class FileUploadField extends FormField { return $id; } + /** + * Called from FileUploadWidget::getValue() when manual upload is used + * for browsers which do not support the HTML5 way of uploading async. + */ + function uploadFile($file) { + if (!$this->isValidFileType($file['name'])) + throw new FileUploadError(__('File type is not allowed')); + + $config = $this->getConfiguration(); + if ($file['size'] > $config['size']) + throw new FileUploadError(__('File size is too large')); + + return AttachmentFile::upload($file); + } + + /** + * Called from API and email routines and such to handle attachments + * sent other than via web upload + */ + function uploadAttachment(&$file) { + if (!$this->isValidFileType($file['name'])) + throw new FileUploadError(__('File type is not allowed')); + + if (is_callable($file['data'])) + $file['data'] = $file['data'](); + if (!isset($file['size'])) { + // bootstrap.php include a compat version of mb_strlen + if (extension_loaded('mbstring')) + $file['size'] = mb_strlen($file['data'], '8bit'); + else + $file['size'] = strlen($file['data']); + } + + $config = $this->getConfiguration(); + if ($file['size'] > $config['size']) + throw new FileUploadError(__('File size is too large')); + + if (!$id = AttachmentFile::save($file)) + throw new FileUploadError(__('Unable to save file')); + + return $id; + } + + function isValidFileType($name, $type=false) { + $config = $this->getConfiguration(); + + // Return true if all file types are allowed (.*) + if (strpos($config['extensions'], '.*') || !$config['extensions']) + return true; + + $allowed = array_map('trim', explode(',', strtolower($config['extensions']))); + + $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); + + //TODO: Check MIME type - file ext. shouldn't be solely trusted. + + return ($ext && is_array($allowed) && in_array(".$ext", $allowed)); + } + function getFiles() { if (!isset($this->attachments) && ($a = $this->getAnswer()) && ($e = $a->getEntry()) && ($e->get('id')) @@ -1785,8 +1856,10 @@ class FileUploadWidget extends Widget { // Handle manual uploads (IE<10) if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES[$this->name])) { foreach (AttachmentFile::format($_FILES[$this->name]) as $file) { - if ($id = AttachmentFile::upload($file)) - $ids[] = $id; + try { + $ids[] = $this->field->uploadFile($file); + } + catch (FileUploadError $ex) {} } return array_merge($ids, parent::getValue() ?: array()); } @@ -1798,4 +1871,6 @@ class FileUploadWidget extends Widget { } } +class FileUploadError extends Exception {} + ?> diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 105b5438d4e4c410264681554ba6ede5591efc04..45758c75c052057061c354e77e2c5faef639e4ff 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -665,8 +665,14 @@ class MailFetcher { $errors=array(); $seen = false; + // Use the settings on the thread entry on the ticket details + // form to validate the attachments in the email + $tform = TicketForm::objects()->one()->getForm(); + $messageField = $tform->getField('message'); + $fileField = $messageField->getWidget()->getAttachments(); + // Fetch attachments if any. - if($ost->getConfig()->allowEmailAttachments()) { + if ($messageField->isAttachmentsEnabled()) { // Include TNEF attachments in the attachments list if ($this->tnef) { foreach ($this->tnef->attachments as $at) { @@ -680,15 +686,11 @@ class MailFetcher { } } $vars['attachments'] = array(); - foreach($attachments as $a ) { + + foreach ($attachments as $a) { $file = array('name' => $a['name'], 'type' => $a['type']); - //Check the file type - if(!$ost->isFileTypeAllowed($file)) { - $file['error'] = sprintf(_S('Invalid file type (ext) for %s'), - Format::htmlchars($file['name'])); - } - elseif (@$a['data'] instanceof TnefAttachment) { + if (@$a['data'] instanceof TnefAttachment) { $file['data'] = $a['data']->getData(); } else { @@ -701,6 +703,15 @@ class MailFetcher { } // Include the Content-Id if specified (for inline images) $file['cid'] = isset($a['cid']) ? $a['cid'] : false; + + // Validate and save immediately + try { + $file['id'] = $fileField->uploadAttachment($file); + } + catch (FileUploadError $ex) { + $file['error'] = $file['name'] . ': ' . $ex->getMessage(); + } + $vars['attachments'][] = $file; } } diff --git a/include/class.osticket.php b/include/class.osticket.php index e3901f25d34b1748db66f4a2fa416e9c786c5a87..59dc66c6c83de78b4191fb2178ebf78b141ba9f7 100644 --- a/include/class.osticket.php +++ b/include/class.osticket.php @@ -133,24 +133,6 @@ class osTicket { return ($token && !strcasecmp($token, $this->getLinkToken())); } - function isFileTypeAllowed($file, $mimeType='') { - - if(!$file || !($allowedFileTypes=$this->getConfig()->getAllowedFileTypes())) - return false; - - //Return true if all file types are allowed (.*) - if(trim($allowedFileTypes)=='.*') return true; - - $allowed = array_map('trim', explode(',', strtolower($allowedFileTypes))); - $filename = is_array($file)?$file['name']:$file; - - $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - - //TODO: Check MIME type - file ext. shouldn't be solely trusted. - - return ($ext && is_array($allowed) && in_array(".$ext", $allowed)); - } - /* Replace Template Variables */ function replaceTemplateVariables($input, $vars=array()) { diff --git a/include/class.thread.php b/include/class.thread.php index f99c0f45f3e22e96f20331c1c9dc27e09d285712..f7751f78e7f9d6b2a66fee694cdeb943f321ee86 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -539,7 +539,11 @@ Class ThreadEntry { */ function saveAttachment(&$file) { - if(!($fileId=is_numeric($file)?$file:AttachmentFile::save($file))) + if (is_numeric($file)) + $fileId = $file; + elseif (is_array($file) && isset($file['id'])) + $fileId = $file['id']; + elseif (!($fileId = AttachmentFile::save($file))) return 0; $inline = is_array($file) && @$file['inline'];