diff --git a/ajax.php b/ajax.php index 60fca4337ec3df74e71df4037b49bdbfc917ac89..bfa481a20aed7cf8c1d6134ff835a46584dcbb0e 100644 --- a/ajax.php +++ b/ajax.php @@ -36,7 +36,9 @@ $dispatcher = patterns('', url_post('^(?P<namespace>[\w.]+)$', 'createDraftClient') )), url('^/form/', patterns('ajax.forms.php:DynamicFormsAjaxAPI', - url_get('^help-topic/(?P<id>\d+)$', 'getClientFormsForHelpTopic') + url_get('^help-topic/(?P<id>\d+)$', 'getClientFormsForHelpTopic'), + url_post('^upload/(\d+)?$', 'upload'), + url_post('^upload/(\w+)?$', 'attach') )), url('^/i18n/(?P<lang>[\w_]+)/', patterns('ajax.i18n.php:i18nAjaxAPI', url_get('(?P<tag>\w+)$', 'getLanguageFile') diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css index c88bba906cdef9b040661312900e7f63b0015f10..e50724e19283d088aceb3acfd64cf4feb51acc8a 100644 --- a/assets/default/css/theme.css +++ b/assets/default/css/theme.css @@ -703,25 +703,6 @@ label.required { border: 1px solid #aaa; background: #fff; } -#reply .attachments .uploads div { - display: inline-block; - padding-right: 20px; -} -#reply .file { - display: inline-block; - padding-left: 20px; - margin-right: 20px; - background: url('../images/icons/file.gif') 0 50% no-repeat; -} -.uploads { - display: inline-block; - padding-right: 20px; -} -.uploads label { - padding: 3px; - padding-right: 10px; - width: auto !important; -} /* Ticket icons */ .Icon { width: auto; diff --git a/css/filedrop.css b/css/filedrop.css new file mode 100644 index 0000000000000000000000000000000000000000..d6b42fa25920ea84d5cb18f34155f0a01a16ccc1 --- /dev/null +++ b/css/filedrop.css @@ -0,0 +1,179 @@ +.filedrop { + padding-bottom: 10px; +} +.filedrop .dropzone { + border: 2px dashed rgba(0, 0, 0, 0.2); + padding: 8px; + border-radius: 5px; + background-color: rgba(0, 0, 0, 0.05); + color: #999; +} +.filedrop .dropzone a { + color: rgba(24, 78, 129, 0.7); +} +.filedrop .files:not(:empty) { + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 5px 5px 0 0; + padding: 5px; +} +.filedrop .files:not(:empty) + .dropzone { + border-top: none; + border-radius: 0 0 5px 5px; +} +.filedrop .files .file { + display: block; + padding: 5px 10px 5px 20px; + margin: 0; + border-radius: 5px; +} +.filedrop .files .file:hover { + background-color: rgba(0, 0, 0, 0.05); +} +.filedrop .files .file .filesize { + margin-left: 1em; + color: #999; +} +.filedrop .files .file .upload-rate { + margin-right: 10px; + color: #aaa; +} +.filedrop .files .file .trash { + cursor: pointer; +} +.filedrop .progress { + margin-top: 5px; +} +.filedrop .cancel { + cursor: pointer; +} +.filedrop .preview { + width: auto; + height: auto; + max-width: 60px; + max-height: 40px; + display: inline-block; + float: left; + padding-right: 10px; +} +.redactor_box + .filedrop .dropzone, +.redactor_box + div > .filedrop .dropzone, +.redactor_box + div > .filedrop .files { + border-top-width: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.tooltip-preview, +.tooltip-preview img { + max-width: 300px; + max-height: 300px; +} + +/* Bootstrap 3.2 progress-bar */ +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + height: 10px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); +} +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #428bca; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); +} +.progress-bar:not(.active) { + -webkit-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} +.progress-striped .progress-bar, +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .progress-bar, +.progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar[aria-valuenow="1"], +.progress-bar[aria-valuenow="2"] { + min-width: 30px; +} +.progress-bar[aria-valuenow="0"] { + min-width: 30px; + color: #777; + background-color: transparent; + background-image: none; + -webkit-box-shadow: none; + box-shadow: none; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} + diff --git a/include/ajax.config.php b/include/ajax.config.php index 74ee115f08c1082fbb2013cafd0fb2d2c7ddd84b..1cff41842b82d4d8c7c08fde9a4222561810c7e2 100644 --- a/include/ajax.config.php +++ b/include/ajax.config.php @@ -27,10 +27,8 @@ class ConfigAjaxAPI extends AjaxController { $config=array( 'lock_time' => ($cfg->getLockTime()*3600), - 'max_file_uploads'=> (int) $cfg->getStaffMaxFileUploads(), 'html_thread' => (bool) $cfg->isHtmlThreadEnabled(), 'date_format' => ($cfg->getDateFormat()), - 'allow_attachments' => (bool) $cfg->allowAttachments(), 'lang' => $lang, 'short_lang' => $sl, ); @@ -44,10 +42,6 @@ class ConfigAjaxAPI extends AjaxController { list($sl, $locale) = explode('_', $lang); $config=array( - 'allow_attachments' => (bool) $cfg->allowOnlineAttachments(), - 'file_types' => $cfg->getAllowedFileTypes(), - 'max_file_size' => (int) $cfg->getMaxFileSize(), - 'max_file_uploads'=> (int) $cfg->getClientMaxFileUploads(), 'html_thread' => (bool) $cfg->isHtmlThreadEnabled(), 'lang' => $lang, 'short_lang' => $sl, 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 84b2cb3d8da8302fa1ed1fd0ad9600ea8b8fcf4a..8044fafc32c7b1d9e77ee93f341d14903627d5f4 100644 --- a/include/ajax.forms.php +++ b/include/ajax.forms.php @@ -2,6 +2,7 @@ require_once(INCLUDE_DIR . 'class.topic.php'); require_once(INCLUDE_DIR . 'class.dynamic_forms.php'); +require_once(INCLUDE_DIR . 'class.forms.php'); class DynamicFormsAjaxAPI extends AjaxController { function getForm($form_id) { @@ -38,8 +39,10 @@ class DynamicFormsAjaxAPI extends AjaxController { function saveFieldConfiguration($field_id) { $field = DynamicFormField::lookup($field_id); - if (!$field->setConfiguration()) - return (include STAFFINC_DIR . 'templates/dynamic-field-config.tmpl.php'); + if (!$field->setConfiguration()) { + include STAFFINC_DIR . 'templates/dynamic-field-config.tmpl.php'; + return; + } else $field->save(); Http::response(201, 'Field successfully updated'); @@ -74,12 +77,34 @@ class DynamicFormsAjaxAPI extends AjaxController { if (!$list || !($item = $list->getItem( (int) $item_id))) Http::response(404, 'No such list item'); - if (!$item->setConfiguration()) - return (include STAFFINC_DIR . 'templates/list-item-properties.tmpl.php'); + if (!$item->setConfiguration()) { + include STAFFINC_DIR . 'templates/list-item-properties.tmpl.php'; + return; + } else $item->save(); Http::response(201, 'Successfully updated record'); } + + function upload($id) { + if (!$field = DynamicFormField::lookup($id)) + Http::response(400, 'No such field'); + + $impl = $field->getImpl(); + if (!$impl instanceof FileUploadField) + Http::response(400, 'Upload to a non file-field'); + + return JsonDataEncoder::encode( + array('id'=>$impl->ajaxUpload()) + ); + } + + function attach() { + $field = new FileUploadField(); + return JsonDataEncoder::encode( + 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.attachment.php b/include/class.attachment.php index 937d09edd5346d331170752077e1b00f9a354e51..2e3b8c55e303ff74996a538352bb9fb46c674fd8 100644 --- a/include/class.attachment.php +++ b/include/class.attachment.php @@ -167,6 +167,8 @@ class GenericAttachments { .' AND a.object_id='.db_input($this->getId()); if(($res=db_query($sql)) && db_num_rows($res)) { while($rec=db_fetch_array($res)) { + $rec['download'] = AttachmentFile::getDownloadForIdAndKey( + $rec['id'], $rec['key']); $this->attachments[] = $rec; } } diff --git a/include/class.config.php b/include/class.config.php index 78284e49daa52abb910a4dbb5d94b73522c74ed5..8a7d5eebdb6d53a6d23d5667d543ded34ad8b5ca 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -152,9 +152,6 @@ class OsticketConfig extends Config { 'pw_reset_window' => 30, 'enable_html_thread' => true, 'allow_attachments' => true, - 'allow_email_attachments' => true, - 'allow_online_attachments' => true, - 'allow_online_attachments_onlogin' => false, 'name_format' => 'full', # First Last 'auto_claim_tickets'=> true, 'system_language' => 'en_US', @@ -809,28 +806,10 @@ class OsticketConfig extends Config { return ($this->get('allow_attachments')); } - function allowOnlineAttachments() { - return ($this->allowAttachments() && $this->get('allow_online_attachments')); - } - - function allowAttachmentsOnlogin() { - return ($this->allowOnlineAttachments() && $this->get('allow_online_attachments_onlogin')); - } - - function allowEmailAttachments() { - return ($this->allowAttachments() && $this->get('allow_email_attachments')); - } - function getSystemLanguage() { return $this->get('system_language'); } - //TODO: change db field to allow_api_attachments - which will include email/json/xml attachments - // terminology changed on the UI - function allowAPIAttachments() { - return $this->allowEmailAttachments(); - } - /* Needed by upgrader on 1.6 and older releases upgrade - not not remove */ function getUploadDir() { return $this->get('upload_dir'); @@ -959,27 +938,6 @@ class OsticketConfig extends Config { $errors['enable_captcha']=__('PNG support is required for Image Captcha'); } - if($vars['allow_attachments']) { - - if(!ini_get('file_uploads')) - $errors['err']=__('The "file_uploads" directive is disabled in php.ini'); - - if(!is_numeric($vars['max_file_size'])) - $errors['max_file_size']=__('Maximum file size required'); - - if(!$vars['allowed_filetypes']) - $errors['allowed_filetypes']=__('Allowed file extentions required'); - - if(!($maxfileuploads=ini_get('max_file_uploads'))) - $maxfileuploads=DEFAULT_MAX_FILE_UPLOADS; - - if(!$vars['max_user_file_uploads'] || $vars['max_user_file_uploads']>$maxfileuploads) - $errors['max_user_file_uploads']=sprintf(__('Invalid selection. Must be less than %d'),$maxfileuploads); - - if(!$vars['max_staff_file_uploads'] || $vars['max_staff_file_uploads']>$maxfileuploads) - $errors['max_staff_file_uploads']=sprintf(__('Invalid selection. Must be less than %d'),$maxfileuploads); - } - if ($vars['default_help_topic'] && ($T = Topic::lookup($vars['default_help_topic'])) && !$T->isActive()) { @@ -1012,15 +970,8 @@ class OsticketConfig extends Config { 'hide_staff_name'=>isset($vars['hide_staff_name'])?1:0, 'enable_html_thread'=>isset($vars['enable_html_thread'])?1:0, 'allow_client_updates'=>isset($vars['allow_client_updates'])?1:0, - 'allow_attachments'=>isset($vars['allow_attachments'])?1:0, - 'allowed_filetypes'=>strtolower(preg_replace("/\n\r|\r\n|\n|\r/", '',trim($vars['allowed_filetypes']))), 'max_file_size'=>$vars['max_file_size'], - 'max_user_file_uploads'=>$vars['max_user_file_uploads'], - 'max_staff_file_uploads'=>$vars['max_staff_file_uploads'], 'email_attachments'=>isset($vars['email_attachments'])?1:0, - 'allow_email_attachments'=>isset($vars['allow_email_attachments'])?1:0, - 'allow_online_attachments'=>isset($vars['allow_online_attachments'])?1:0, - 'allow_online_attachments_onlogin'=>isset($vars['allow_online_attachments_onlogin'])?1:0, )); } diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index d36a56aa4e8db580e24bcd8ea9a2a843ba6d2a15..452e22bf41653150a29354eec7b54f983f9cecd4 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -810,6 +810,9 @@ class DynamicFormEntry extends VerySimpleModel { && in_array($field->get('name'), array('name'))) continue; + // Set the entry ID here so that $field->getClean() can use the + // entry-id if necessary + $a->set('entry_id', $this->get('id')); $val = $field->to_database($field->getClean()); if (is_array($val)) { $a->set('value', $val[0]); @@ -817,7 +820,6 @@ class DynamicFormEntry extends VerySimpleModel { } else $a->set('value', $val); - $a->set('entry_id', $this->get('id')); // Don't save answers for presentation-only fields if ($field->hasData() && !$field->isPresentationOnly()) $a->save(); diff --git a/include/class.faq.php b/include/class.faq.php index 78d221c21b1e7ec2ea267da70401aa7ce57ac612..c2a1958917b834d8a74d92488e8b333547532059 100644 --- a/include/class.faq.php +++ b/include/class.faq.php @@ -171,7 +171,7 @@ class FAQ { $this->updateTopics($vars['topics']); //Delete removed attachments. - $keepers = $vars['files']?$vars['files']:array(); + $keepers = $vars['files']; if(($attachments = $this->attachments->getSeparates())) { foreach($attachments as $file) { if($file['id'] && !in_array($file['id'], $keepers)) @@ -179,9 +179,8 @@ class FAQ { } } - //Upload new attachments IF any. - if($_FILES['attachments'] && ($files=AttachmentFile::format($_FILES['attachments']))) - $this->attachments->upload($files); + // Upload new attachments IF any. + $this->attachments->upload($keepers); // Inline images (attached to the draft) $this->attachments->deleteInlines(); diff --git a/include/class.file.php b/include/class.file.php index 203ce5534356d1a295fec5cfd131212d17aa74de..15f4ea17a0f8dc294672bfa4d695a7a12dc1ff09 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -107,13 +107,17 @@ class AttachmentFile { return $this->ht['created']; } + static function getDownloadForIdAndKey($id, $key) { + return strtolower($key . md5($id.session_id().strtolower($key))); + } + + /** * Retrieve a signature that can be sent to scp/file.php?h= in order to * download this file */ function getDownloadHash() { - return strtolower($this->getKey() - . md5($this->getId().session_id().strtolower($this->getKey()))); + return self::getDownloadForIdAndKey($this->getId(), $this->getKey()); } function open() { @@ -530,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)) @@ -558,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); @@ -587,7 +580,7 @@ class AttachmentFile { .'SELECT file_id FROM '.TICKET_ATTACHMENT_TABLE .' UNION ' .'SELECT file_id FROM '.ATTACHMENT_TABLE - .") AND `ft` = 'T'"; + .") AND `ft` = 'T' AND TIMESTAMPDIFF(DAY, `created`, CURRENT_TIMESTAMP) > 1"; if (!($res = db_query($sql))) return false; diff --git a/include/class.forms.php b/include/class.forms.php index 2f597eb0757008ade7abe3a7a624561db47dd420..27c6eee2a183a355ba6e1e5dc13453c7fe77b2aa 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -59,6 +59,7 @@ class Form { function getTitle() { return $this->title; } function getInstructions() { return $this->instructions; } function getSource() { return $this->_source; } + function setSource($source) { $this->_source = $source; } /** * Validate the form and indicate if there no errors. @@ -107,6 +108,40 @@ class Form { else include(CLIENTINC_DIR . 'templates/dynamic-form.tmpl.php'); } + + function getMedia() { + static $dedup = array(); + + foreach ($this->getFields() as $f) { + if (($M = $f->getMedia()) && is_array($M)) { + foreach ($M as $type=>$files) { + foreach ($files as $url) { + $key = strtolower($type.$url); + if (isset($dedup[$key])) + continue; + + self::emitMedia($url, $type); + + $dedup[$key] = true; + } + } + } + } + } + + static function emitMedia($url, $type) { + if ($url[0] == '/') + $url = ROOT_PATH . substr($url, 1); + + switch (strtolower($type)) { + case 'css': ?> + <link rel="stylesheet" type="text/css" href="<?php echo $url; ?>"/><?php + break; + case 'js': ?> + <script type="text/javascript" src="<?php echo $url; ?>"></script><?php + break; + } + } } require_once(INCLUDE_DIR . "class.json.php"); @@ -139,6 +174,7 @@ class FormField { 'phone' => array( /* @trans */ 'Phone Number', 'PhoneField'), 'bool' => array( /* @trans */ 'Checkbox', 'BooleanField'), 'choices' => array( /* @trans */ 'Choices', 'ChoiceField'), + 'files' => array( /* @trans */ 'File Upload', 'FileUploadField'), 'break' => array( /* @trans */ 'Section Break', 'SectionBreakField'), ), ); @@ -181,6 +217,9 @@ class FormField { function get($what) { return $this->ht[$what]; } + function set($field, $value) { + $this->ht[$field] = $value; + } /** * getClean @@ -386,7 +425,7 @@ class FormField { return substr(md5( session_id() . '-field-id-'.$this->get('id')), -16); else - return $this->get('id'); + return $this->get('name') ?: $this->get('id'); } function setForm($form) { @@ -410,13 +449,18 @@ class FormField { } function render($mode=null) { - $this->getWidget()->render($mode); + return $this->getWidget()->render($mode); } function renderExtras($mode=null) { return; } + function getMedia() { + $widget = $this->getWidget(); + return $widget::$media; + } + function getConfigurationOptions() { return array(); } @@ -946,10 +990,32 @@ class ThreadEntryField extends FormField { function isPresentationOnly() { return true; } - function renderExtras($mode=null) { - if ($mode == 'client') - // TODO: Pass errors arrar into showAttachments - $this->getWidget()->showAttachments(); + + function getConfigurationOptions() { + global $cfg; + + $attachments = new FileUploadField(); + $fileupload_config = $attachments->getConfigurationOptions(); + $fileupload_config['extensions']->set('default', $cfg->getAllowedFileTypes()); + return array( + 'attachments' => new BooleanField(array( + 'label'=>__('Enable Attachments'), + 'default'=>$cfg->allowAttachments(), + 'configuration'=>array( + 'desc'=>__('Enables attachments on tickets, regardless of channel'), + ), + 'validators' => function($self, $value) { + if (!ini_get('file_uploads')) + $self->addError(__('The "file_uploads" directive is disabled in php.ini')); + } + )), + ) + + $fileupload_config; + } + + function isAttachmentsEnabled() { + $config = $this->getConfiguration(); + return $config['attachments']; } } @@ -1172,7 +1238,273 @@ FormField::addFieldTypes('Dynamic Fields', function() { ); }); +class FileUploadField extends FormField { + static $widget = 'FileUploadWidget'; + + protected $attachments; + + function getConfigurationOptions() { + // Compute size selections + $sizes = array('262144' => '— '.__('Small').' —'); + $next = 512 << 10; + $max = strtoupper(ini_get('upload_max_filesize')); + $limit = (int) $max; + if (!$limit) $limit = 2 << 20; # 2M default value + elseif (strpos($max, 'K')) $limit <<= 10; + elseif (strpos($max, 'M')) $limit <<= 20; + elseif (strpos($max, 'G')) $limit <<= 30; + while ($next <= $limit) { + // Select the closest, larger value (in case the + // current value is between two) + $sizes[$next] = Format::file_size($next); + $next *= 2; + } + // Add extra option if top-limit in php.ini doesn't fall + // at a power of two + if ($next < $limit * 2) + $sizes[$limit] = Format::file_size($limit); + + // Load file types + $_types = YamlDataParser::load(INCLUDE_DIR . '/config/filetype.yaml'); + $types = array(); + foreach ($_types as $type=>$info) { + $types[$type] = $info['description']; + } + + global $cfg; + return array( + 'size' => new ChoiceField(array( + 'label'=>__('Maximum File Size'), + 'hint'=>__('Choose maximum size of a single file uploaded to this field'), + 'default'=>$cfg->getMaxFileSize(), + 'choices'=>$sizes + )), + 'mimetypes' => new ChoiceField(array( + 'label'=>__('Restrict by File Type'), + 'hint'=>__('Optionally, choose acceptable file types.'), + 'required'=>false, + 'choices'=>$types, + 'configuration'=>array('multiselect'=>true,'prompt'=>__('No restrictions')) + )), + 'extensions' => new TextareaField(array( + 'label'=>__('Additional File Type Filters'), + 'hint'=>__('Optionally, enter comma-separated list of additional file types, by extension. (e.g .doc, .pdf).'), + 'configuration'=>array('html'=>false, 'rows'=>2), + )), + 'max' => new TextboxField(array( + 'label'=>__('Maximum Files'), + 'hint'=>__('Users cannot upload more than this many files.'), + 'default'=>false, + 'required'=>false, + 'validator'=>'number', + 'configuration'=>array('size'=>8, 'length'=>4, 'placeholder'=>__('No limit')), + )) + ); + } + + /** + * Called from the ajax handler for async uploads via web clients. + */ + function ajaxUpload($bypass=false) { + $config = $this->getConfiguration(); + + $files = AttachmentFile::format($_FILES['upload'], + // For numeric fields assume configuration exists + !is_numeric($this->get('id'))); + if (count($files) != 1) + Http::response(400, 'Send one file at a time'); + $file = array_shift($files); + $file['name'] = urldecode($file['name']); + + if (!$bypass && !$this->isValidFileType($file['name'], $file['type'])) + 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']); + + 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'], $file['type'])) + 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'], $file['type'])) + 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(); + + // Check MIME type - file ext. shouldn't be solely trusted. + if ($type && $config['__mimetypes'] + && in_array($type, $config['__mimetypes'])) + return true; + + // Return true if all file types are allowed (.*) + if (!$config['__extensions'] || in_array('.*', $config['__extensions'])) + return true; + + $allowed = $config['__extensions']; + $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); + + return ($ext && is_array($allowed) && in_array(".$ext", $allowed)); + } + + function getFiles() { + if (!isset($this->attachments) && ($a = $this->getAnswer()) + && ($e = $a->getEntry()) && ($e->get('id')) + ) { + $this->attachments = new GenericAttachments( + // Combine the field and entry ids to make the key + sprintf('%u', crc32('E'.$this->get('id').$e->get('id'))), + 'E'); + } + return $this->attachments ? $this->attachments->getAll() : array(); + } + + function getConfiguration() { + $config = parent::getConfiguration(); + $_types = YamlDataParser::load(INCLUDE_DIR . '/config/filetype.yaml'); + $mimetypes = array(); + $extensions = array(); + if (isset($config['mimetypes']) && is_array($config['mimetypes'])) { + foreach ($config['mimetypes'] as $type=>$desc) { + foreach ($_types[$type]['types'] as $mime=>$exts) { + $mimetypes[$mime] = true; + foreach ($exts as $ext) + $extensions['.'.$ext] = true; + } + } + } + if (strpos($config['extensions'], '.*') !== false) + $config['extensions'] = ''; + + if (is_string($config['extensions'])) { + foreach (preg_split('/\s+/', str_replace(',',' ', $config['extensions'])) as $ext) { + if (!$ext) { + continue; + } + elseif (strpos($ext, '/')) { + $mimetypes[$ext] = true; + } + else { + if ($ext[0] != '.') + $ext = '.' . $ext; + // Add this to the MIME types list so it can be exported to + // the @accept attribute + if (!isset($extensions[$ext])) + $mimetypes[$ext] = true; + + $extensions[$ext] = true; + } + } + $config['__extensions'] = array_keys($extensions); + } + elseif (is_array($config['extensions'])) { + $config['__extensions'] = $config['extensions']; + } + + // 'mimetypes' is the array represented from the user interface, + // '__mimetypes' is a complete list of supported MIME types. + $config['__mimetypes'] = array_keys($mimetypes); + return $config; + } + + // When the field is saved to database, encode the ID listing as a json + // array. Then, inspect the difference between the files actually + // attached to this field + function to_database($value) { + $this->getFiles(); + if (isset($this->attachments)) { + $ids = array(); + // Handle deletes + foreach ($this->attachments->getAll() as $f) { + if (!in_array($f['id'], $value)) + $this->attachments->delete($f['id']); + else + $ids[] = $f['id']; + } + // Handle new files + foreach ($value as $id) { + if (!in_array($id, $ids)) + $this->attachments->upload($id); + } + } + return JsonDataEncoder::encode($value); + } + + function parse($value) { + // Values in the database should be integer file-ids + return array_map(function($e) { return (int) $e; }, + $value ?: array()); + } + + function to_php($value) { + return JsonDataParser::decode($value); + } + + function display($value) { + $links = array(); + foreach ($this->getFiles() as $f) { + $hash = strtolower($f['key'] + . md5($f['id'].session_id().strtolower($f['key']))); + $links[] = sprintf('<a class="no-pjax" href="file.php?h=%s">%s</a>', + $hash, Format::htmlchars($f['name'])); + } + return implode('<br/>', $links); + } + + function toString($value) { + $files = array(); + foreach ($this->getFiles() as $f) { + $files[] = $f['name']; + } + return implode(', ', $files); + } +} + class Widget { + static $media = null; function __construct($field) { $this->field = $field; @@ -1508,29 +1840,112 @@ class ThreadEntryWidget extends Widget { cols="21" rows="8" style="width:80%;"><?php echo Format::htmlchars($this->value); ?></textarea> <?php + $config = $this->field->getConfiguration(); + if (!$config['attachments']) + return; + + $attachments = $this->getAttachments($config); + print $attachments->render($client); + foreach ($attachments->getMedia() as $type=>$urls) { + foreach ($urls as $url) + Form::emitMedia($url, $type); + } } - function showAttachments($errors=array()) { - global $cfg, $thisclient; - - if(($cfg->allowOnlineAttachments() - && !$cfg->allowAttachmentsOnlogin()) - || ($cfg->allowAttachmentsOnlogin() - && ($thisclient && $thisclient->isValid()))) { ?> - <div class="clear"></div> - <hr/> - <div><strong style="padding-right:1em;vertical-align:top"><?php - echo __('Attachments'); ?>: </strong> - <div style="display:inline-block"> - <div class="uploads" style="display:block"></div> - <input type="file" class="multifile" name="attachments[]" id="attachments" size="30" value="" /> - </div> - <font class="error"> <?php echo $errors['attachments']; ?></font> - </div> - <hr/> - <?php + function getAttachments($config=false) { + if (!$config) + $config = $this->field->getConfiguration(); + + return new FileUploadField(array( + 'id'=>'attach', + 'name'=>'attach:' . $this->field->get('id'), + 'configuration'=>$config) + ); + } +} + +class FileUploadWidget extends Widget { + static $media = array( + 'css' => array( + '/css/filedrop.css', + ), + ); + + function render($how) { + $config = $this->field->getConfiguration(); + $name = $this->field->getFormName(); + $id = substr(md5(spl_object_hash($this)), 10); + $attachments = $this->field->getFiles(); + $mimetypes = array_filter($config['__mimetypes'], + function($t) { return strpos($t, '/') !== false; } + ); + $files = array(); + foreach ($this->value ?: array() as $fid) { + $found = false; + foreach ($attachments as $f) { + if ($f['id'] == $fid) { + $files[] = $f; + $found = true; + break; + } + } + if (!$found && ($file = AttachmentFile::lookup($fid))) { + $files[] = array( + 'id' => $file->getId(), + 'name' => $file->getName(), + 'type' => $file->getType(), + 'size' => $file->getSize(), + ); + } + } + ?><div id="<?php echo $id; + ?>" class="filedrop"><div class="files"></div> + <div class="dropzone"><i class="icon-upload"></i> + Drop files here or <a href="#" class="manual">choose + them</a> + <input type="file" class="multifile" multiple id="file-<?php echo $id; ?>" style="display:none;" + accept="<?php echo implode(',', $config['__mimetypes']); ?>"/> + </div></div> + <script type="text/javascript"> + $(function(){$('#<?php echo $id; ?> .dropzone').filedropbox({ + url: 'ajax.php/form/upload/<?php echo $this->field->get('id') ?>', + link: $('#<?php echo $id; ?>').find('a.manual'), + paramname: 'upload[]', + fallback_id: 'file-<?php echo $id; ?>', + allowedfileextensions: <?php echo JsonDataEncoder::encode( + $config['__extensions']); ?>, + allowedfiletypes: <?php echo JsonDataEncoder::encode( + $mimetypes); ?>, + maxfiles: <?php echo $config['max'] ?: 20; ?>, + maxfilesize: <?php echo ($config['size'] ?: 1048576) / 1048576; ?>, + name: '<?php echo $name; ?>[]', + files: <?php echo JsonDataEncoder::encode($files); ?> + });}); + </script> +<?php + } + + function getValue() { + $data = $this->field->getSource(); + $ids = array(); + // Handle manual uploads (IE<10) + if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES[$this->name])) { + foreach (AttachmentFile::format($_FILES[$this->name]) as $file) { + try { + $ids[] = $this->field->uploadFile($file); + } + catch (FileUploadError $ex) {} + } + return array_merge($ids, parent::getValue() ?: array()); } + // If no value was sent, assume an empty list + elseif ($data && is_array($data) && !isset($data[$this->name])) + return array(); + + return parent::getValue(); } } +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.mailparse.php b/include/class.mailparse.php index efcff356c753a89672f6d2937dedac59a60ce6d6..c2d0cac78cd13567695111233879c60f9c2da6f2 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -672,8 +672,7 @@ class EmailDataParser { $data['reply-to-name'] = trim($replyto->personal, " \t\n\r\0\x0B\x22"); } - if($cfg && $cfg->allowEmailAttachments()) - $data['attachments'] = $parser->getAttachments(); + $data['attachments'] = $parser->getAttachments(); return $data; } 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']; diff --git a/include/class.ticket.php b/include/class.ticket.php index acded0c81c769d753360626775c0d797e9653982..311e098b5d5edcd8f36e08ccd456153f72cec270 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -2681,7 +2681,6 @@ class Ticket { } //post the message. - unset($vars['cannedattachments']); //Ticket::open() might have it set as part of open & respond. $vars['title'] = $vars['subject']; //Use the initial subject as title of the post. $vars['userId'] = $ticket->getUserId(); $message = $ticket->postMessage($vars , $origin, false); @@ -2773,7 +2772,12 @@ class Ticket { if (!$thisstaff->canAssignTickets()) unset($vars['assignId']); - if(!($ticket=Ticket::create($vars, $errors, 'staff', false))) + $create_vars = $vars; + $tform = TicketForm::objects()->one()->getForm($create_vars); + $create_vars['cannedattachments'] + = $tform->getField('message')->getWidget()->getAttachments()->getClean(); + + if(!($ticket=Ticket::create($create_vars, $errors, 'staff', false))) return false; $vars['msgId']=$ticket->getLastMsgId(); @@ -2782,11 +2786,9 @@ class Ticket { $response = null; if($vars['response'] && $thisstaff->canPostReply()) { - // unpack any uploaded files into vars. - if ($_FILES['attachments']) - $vars['files'] = AttachmentFile::format($_FILES['attachments']); - $vars['response'] = $ticket->replaceVars($vars['response']); + // $vars['cannedatachments'] contains the attachments placed on + // the response form. if(($response=$ticket->postReply($vars, $errors, false))) { //Only state supported is closed on response if(isset($vars['ticket_state']) && $thisstaff->canCloseTickets()) diff --git a/include/client/header.inc.php b/include/client/header.inc.php index 2d06fe5075bb5a2a9b4b167bf89a72dfaa6c69f7..97085a7cbb462b71ca86ebe74d042836ca2e09f2 100644 --- a/include/client/header.inc.php +++ b/include/client/header.inc.php @@ -35,8 +35,8 @@ if (($lang = Internationalization::getCurrentLanguage()) <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/rtl.css"/> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.8.3.min.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script> - <script src="<?php echo ROOT_PATH; ?>js/jquery.multifile.js"></script> <script src="<?php echo ROOT_PATH; ?>js/osticket.js"></script> + <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/filedrop.field.js"></script> <script src="<?php echo ROOT_PATH; ?>scp/js/bootstrap-typeahead.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-osticket.js"></script> diff --git a/include/client/templates/dynamic-form.tmpl.php b/include/client/templates/dynamic-form.tmpl.php index c8e7090ff7c6de45686ee27aa1095e9c440ec5cc..57f1aa2951aed93e9e50cf6d6868c7e9f7c15041 100644 --- a/include/client/templates/dynamic-form.tmpl.php +++ b/include/client/templates/dynamic-form.tmpl.php @@ -5,6 +5,8 @@ ?> <tr><td colspan="2"><hr /> <div class="form-header" style="margin-bottom:0.5em"> + <?php print ($form instanceof DynamicFormEntry) + ? $form->getForm()->getMedia() : $form->getMedia(); ?> <h3><?php echo Format::htmlchars($form->getTitle()); ?></h3> <em><?php echo Format::htmlchars($form->getInstructions()); ?></em> </div> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 8e547913a10b73e7efac108fd81d141365f17858..8a6c2c4ee7d3b049a2e77e9c1ff1f3124cf40753 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -181,24 +181,15 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { data-draft-namespace="ticket.client" data-draft-object-id="<?php echo $ticket->getId(); ?>" class="richtext ifhtml draft"><?php echo $info['message']; ?></textarea> - </td> - </tr> <?php - if($cfg->allowOnlineAttachments()) { ?> - <tr> - <td width="160"> - <label for="attachment"><?php echo __('Attachments');?>:</label> - </td> - <td width="640" id="reply_form_attachments" class="attachments"> - <div class="uploads"> - </div> - <div class="file_input"> - <input class="multifile" type="file" name="attachments[]" size="30" value="" /> - </div> - </td> - </tr> + if ($messageField->isAttachmentsEnabled()) { ?> +<?php + print $attachments->render(true); +?> <?php } ?> + </td> + </tr> </table> <p style="padding-left:165px;"> <input type="submit" value="<?php echo __('Post Reply');?>"> diff --git a/include/config/filetype.yaml b/include/config/filetype.yaml new file mode 100644 index 0000000000000000000000000000000000000000..dafd40109f0ece9aca82de1835373b36f652bed3 --- /dev/null +++ b/include/config/filetype.yaml @@ -0,0 +1,129 @@ +--- +image: + description: Images + types: + 'image/bmp': ['bmp'] + 'image/gif': ['gif'] + 'image/jpeg': ['jpeg', 'jpg'] + 'image/png': ['png'] + 'image/svg+xml': ['svg'] + 'image/tiff': ['tiff'] + 'image/vnd.adobe.photoshop': ['psd'] + 'image/vnd.microsoft.icon': ['ico'] + 'image/x-ico': ['ico'] + 'application/postscript': ['eps'] +audio: + description: Audio and Music + types: + 'audio/aiff': [] + 'audio/mpeg': ['mp3'] + 'audio/mp4': ['m4a', 'm4r', 'm4p'] + 'audio/ogg': ['ogg'] + 'audio/vorbis', + 'audio/vnd.wav': ['wav'] + 'audio/wav': ['wav'] + 'audio/x-midi': ['mid', 'midi'] +text: + description: Text Documents + types: + 'text/css': ['css'] + 'text/html': ['htm', 'html'] + 'text/javascript': ['js'] + 'text/plain': ['txt'] + 'text/xml': ['xml'] + 'application/json': ['json'] + 'application/javascript': ['js'] +office: + description: Common Office Documents + types: + # Microsoft Office + 'application/msword': ['doc'] + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['docx'] + 'application/vnd.ms-word.document.macroEnabled.12': ['docm'] + 'application/vnd.ms-excel': ['xls'] + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['xlsx'] + 'application/vnd.ms-excel.sheet.macroEnabled.12': ['xlsm'] + 'application/vnd.ms-excel.sheet.binary.macroEnabled.12': ['xlsb'] + 'application/vnd.ms-powerpoint': ['ppt'] + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['pptx'] + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow': ['ppsx'] + 'application/vnd.ms-powerpoint.presentation.macroEnabled.12': ['pptm'] + 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12': ['ppsm'] + 'application/vnd.ms-access': ['mdb', 'accdb'] + 'application/vnd.ms-project': [] + 'application/msonenote': [] + 'application/vnd.ms-publisher': [] + 'application/rtf': ['rtf'] + 'application/vnd.ms-works': [] + + # iWork + 'application/vnd.apple.keynote': ['keynote'] + 'application/vnd.apple.pages': ['pages'] + 'application/vnd.apple.numbers': ['numbers'] + + # OpenOffice + 'application/vnd.oasis.opendocument.text': [] + 'application/vnd.oasis.opendocument.text-web': [] + 'application/vnd.oasis.opendocument.text-master': [] + 'application/vnd.oasis.opendocument.graphics': [] + 'application/vnd.oasis.opendocument.presentation': [] + 'application/vnd.oasis.opendocument.spreadsheet': [] + 'application/vnd.oasis.opendocument.chart': [] + 'application/vnd.oasis.opendocument.formula': [] + 'application/vnd.oasis.opendocument.database': [] + 'application/vnd.oasis.opendocument.image': [] + 'application/vnd.openofficeorg.extension': [] + + # Other office + 'application/wordperfect': [] + 'application/vnd.kde.karbon': [] + 'application/vnd.kde.kchart': [] + 'application/vnd.kde.kformula': [] + 'application/vnd.kde.kivio': [] + 'application/vnd.kde.kontour': [] + 'application/vnd.kde.kpresenter': [] + 'application/vnd.kde.kspread': [] + 'application/vnd.kde.kword': [] + + # Creative / Common + 'application/pdf': ['pdf'] + '.csv': ['csv'] + 'application/illustrator': ['ai'] + 'application/x-director': [] + 'application/x-indesign': [] + + # Interchange + 'text/vcard': [] + + # Other + 'image/x-dwg': ['dwg'] + 'image/vnd.dwg': ['dwg'] + 'image/vnd.dxf': ['dxf'] + 'application/x-autocad': [] + 'application/x-mathcad': [] + 'application/x-msmoney': [] + + 'application/x-latex': ['tex'] +video: + description: Video Files + types: + 'video/avi': ['avi'] + 'video/mpeg': ['mpg','mpeg'] + 'video/mp4': ['mp4'] + 'video/ogg': ['ogg'] + 'video/quicktime': [] + 'video/webm': [] + 'video/x-ms-asf': [] + 'video/x-ms-wmv': [] + 'application/x-dvi': ['dvi'] + 'application/x-shockwave-flash': ['swf'] +archive: + description: Archives + types: + 'application/tar': ['tar'] + 'application/gzip': ['gz'] + 'application/x-lha': [] + 'application/rar': ['rar'] + 'application/x-compress': ['z'] + 'application/zip': ['zip'] + 'application/x-7z-compressed': ['7z'] diff --git a/include/i18n/en_US/help/tips/settings.ticket.yaml b/include/i18n/en_US/help/tips/settings.ticket.yaml index 5baf7e720592be83eacd5256d1b9234b53f4c9de..d3c1da5cb9f1f7eb160d4d6279993469ecbb555c 100644 --- a/include/i18n/en_US/help/tips/settings.ticket.yaml +++ b/include/i18n/en_US/help/tips/settings.ticket.yaml @@ -128,40 +128,23 @@ enable_html_ticket_thread: If enabled, this will permit the use of rich text formatting between Clients and Agents. -attachments: - title: Attachments +ticket_attachment_settings: + title: Ticket Thread Attachments content: > + Configure settings for files attached to the <span + class="doc-desc-title">issue details</span> field. These settings + are used for all new tickets and new messages regardless of the + source channel (web portal, email, api, etc.). -allow_attachments: - title: Allow Attachments - content: > - -emailed_api_attachments: - title: Emailed/API Attachments - content: > - -online_web_attachments: - title: Online/Web Attachments - content: > - -max_user_file_uploads: - title: Max. User File Uploads - content: > - -max_staff_file_uploads: - title: Max. Staff File Uploads - content: > - -maximum_file_size: +max_file_size: title: Maximum File Size content: > + Choose a maximum file size for attachments uploaded by agents. This + includes canned attachments, knowledge base articles, and + attachments to ticket replies. ticket_response_files: title: Ticket Response Files content: > If enabled, any attachments an Agent may attach to a ticket response will be also included in the email to the User. - -accepted_file_types: - title: Accepted File Types - content: > diff --git a/include/staff/cannedresponse.inc.php b/include/staff/cannedresponse.inc.php index e3cb5f84d6f82255845df47fcba1096aa43c31e8..6be5a63b1e7b2834971715c939e1e3073af0d78c 100644 --- a/include/staff/cannedresponse.inc.php +++ b/include/staff/cannedresponse.inc.php @@ -87,31 +87,20 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); data-draft-object-id="<?php if (isset($canned)) echo $canned->getId(); ?>" style="width:98%;" class="richtext draft"><?php echo $info['response']; ?></textarea> - <br><br> - <div><b><?php echo __('Canned Attachments'); ?></b> <?php echo __('(optional)'); ?> - <i class="help-tip icon-question-sign" href="#canned_attachments"></i> - <font class="error"><?php echo $errors['files']; ?></font></div> - <?php - if($canned && ($files=$canned->attachments->getSeparates())) { - echo '<div id="canned_attachments"><span class="faded">'.__('Uncheck to delete the attachment on submit').'</span><br>'; - foreach($files as $file) { - $hash=$file['key'].md5($file['id'].session_id().strtolower($file['key'])); - echo sprintf('<label><input type="checkbox" name="files[]" id="f%d" value="%d" checked="checked"> - <a href="file.php?h=%s">%s</a> </label> ', - $file['id'], $file['id'], $hash, $file['name']); - } - echo '</div><br>'; - - } - //Hardcoded limit... TODO: add a setting on admin panel - what happens on tickets page?? - if(count($files)<10) { - ?> - <div> - <input type="file" name="attachments[]" value=""/> + <div><h3><?php echo __('Canned Attachments'); ?> <?php echo __('(optional)'); ?> + <i class="help-tip icon-question-sign" href="#canned_attachments"></i></h3> + <div class="error"><?php echo $errors['files']; ?></div> </div> <?php - }?> - <div class="faded"><?php echo __('You can upload up to 10 attachments per canned response.');?></div> + $attachments = $canned_form->getField('attachments'); + if ($canned && ($files=$canned->attachments->getSeparates())) { + $ids = array(); + foreach ($files as $f) + $ids[] = $f['id']; + $attachments->value = $ids; + } + print $attachments->render(); ?> + <br/> </td> </tr> <tr> diff --git a/include/staff/dynamic-form.inc.php b/include/staff/dynamic-form.inc.php index b160e2c253f9b993701ad18c7ad651bfcd88a358..e0bf6a4552b68b761ff0d78efb3fad9502acf4fa 100644 --- a/include/staff/dynamic-form.inc.php +++ b/include/staff/dynamic-form.inc.php @@ -277,17 +277,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </div> <script type="text/javascript"> -$(function() { - var $this = $('#popup-loading').hide(); - $(document).ajaxStart( function(event) { - console.log(1,event); - var $h1 = $this.find('h1'); - $this.show(); - $h1.css({'margin-top':$this.height()/3-$h1.height()/3}); // show Loading Div - }).ajaxStop ( function(){ - $this.hide(); // hide loading div - }); -}); $('form.manage-form').on('submit.inline', function(e) { var formObj = this, deleted = $('input.delete-box:checked', this); if (deleted.length) { diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php index e3f8dcfc9b391b4fc6b6846bc34db0b65beb7fa9..4fcea8138b348ae8bdd24ddd3316cc4e940826ab 100644 --- a/include/staff/faq.inc.php +++ b/include/staff/faq.inc.php @@ -96,24 +96,20 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </tr> <tr> <td colspan=2> - <div><b><?php echo __('Attachments');?></b> (<?php echo __('optional');?>) <font class="error"> <?php echo $errors['files']; ?></font></div> + <div><h3><?php echo __('Attachments');?> + <span class="faded">(<?php echo __('optional');?>)</span></h3> + <div class="error"><?php echo $errors['files']; ?></div> + </div> <?php - if($faq && ($files=$faq->attachments->getSeparates())) { - echo '<div class="faq_attachments"><span class="faded">'.__('Uncheck to delete the attachment on submit').'</span><br>'; - foreach($files as $file) { - $hash=$file['key'].md5($file['id'].session_id().strtolower($file['key'])); - echo sprintf('<label><input type="checkbox" name="files[]" id="f%d" value="%d" checked="checked"> - <a href="file.php?h=%s">%s</a> </label> ', - $file['id'], $file['id'], $hash, $file['name']); - } - echo '</div><br>'; + $attachments = $faq_form->getField('attachments'); + if ($faq && ($files=$faq->attachments->getSeparates())) { + $ids = array(); + foreach ($files as $f) + $ids[] = $f['id']; + $attachments->value = $ids; } - ?> - <div class="faded"><?php echo __('Select files to upload.');?></div> - <div class="uploads"></div> - <div class="file_input"> - <input type="file" class="multifile" name="attachments[]" size="30" value="" /> - </div> + print $attachments->render(); ?> + <br/> </td> </tr> <?php diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php index 08fc55dcc6438ca437bac75f9a52814c097a1fb0..238f6585386e6fe8ab4e83b7310ca08bca87971f 100644 --- a/include/staff/header.inc.php +++ b/include/staff/header.inc.php @@ -20,14 +20,14 @@ if (($lang = Internationalization::getCurrentLanguage()) <![endif]--> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.8.3.min.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script> + <script type="text/javascript" src="./js/scp.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.pjax.js"></script> - <script type="text/javascript" src="../js/jquery.multifile.js"></script> + <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/filedrop.field.js"></script> <script type="text/javascript" src="./js/tips.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-osticket.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-fonts.js"></script> <script type="text/javascript" src="./js/bootstrap-typeahead.js"></script> - <script type="text/javascript" src="./js/scp.js"></script> <link rel="stylesheet" href="<?php echo ROOT_PATH ?>css/thread.css" media="all"> <link rel="stylesheet" href="./css/scp.css" media="all"> <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/redactor.css" media="screen"> diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php index 9f4b306735d14061a8dc1cfde5848a3da98047fa..eea65c8bb7186430cb3a17d0e4628240d0b8c9e2 100644 --- a/include/staff/settings-tickets.inc.php +++ b/include/staff/settings-tickets.inc.php @@ -214,61 +214,20 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) </th> </tr> <tr> - <td width="180"><?php echo __('Allow Attachments');?>:</td> + <td width="180"><?php echo __('Ticket Attachment Settings');?>:</td> <td> - <input type="checkbox" name="allow_attachments" <?php echo - $config['allow_attachments']?'checked="checked"':''; ?>> <b><?php echo __('Allow Attachments'); ?></b> - <em>(<?php echo __('Global Setting'); ?>)</em> - <font class="error"> <?php echo $errors['allow_attachments']; ?></font> - </td> - </tr> - <tr> - <td width="180"><?php echo __('Emailed/API Attachments');?>:</td> - <td> - <input type="checkbox" name="allow_email_attachments" <?php echo $config['allow_email_attachments']?'checked="checked"':''; ?>> <?php echo __('Accept emailed files');?> - <font class="error"> <?php echo $errors['allow_email_attachments']; ?></font> - </td> - </tr> - <tr> - <td width="180"><?php echo __('Online/Web Attachments');?>:</td> - <td> - <input type="checkbox" name="allow_online_attachments" <?php echo $config['allow_online_attachments']?'checked="checked"':''; ?> > - <?php echo __('Allow web upload');?> - <input type="checkbox" name="allow_online_attachments_onlogin" <?php echo $config['allow_online_attachments_onlogin'] ?'checked="checked"':''; ?> > - <?php echo __('Limit to authenticated users only. <em>(User must be logged in to upload files)</em>');?> - <font class="error"> <?php echo $errors['allow_online_attachments']; ?></font> - </td> - </tr> - <tr> - <td><?php echo __('Maximum User File Uploads');?>:</td> - <td> - <select name="max_user_file_uploads"> - <?php - for($i = 1; $i <=$maxfileuploads; $i++) { - ?> - <option <?php echo $config['max_user_file_uploads']==$i?'selected="selected"':''; ?> value="<?php echo $i; ?>"> - <?php echo sprintf(_N('%d file', '%d files', $i), $i); ?></option> - <?php - } ?> - </select> - <em><?php echo __('(Number of files the user is allowed to upload simultaneously)');?></em> - <font class="error"> <?php echo $errors['max_user_file_uploads']; ?></font> - </td> - </tr> - <tr> - <td><?php echo __('Maximum Agent File Uploads');?>:</td> - <td> - <select name="max_staff_file_uploads"> - <?php - for($i = 1; $i <=$maxfileuploads; $i++) { - ?> - <option <?php echo $config['max_staff_file_uploads']==$i?'selected="selected"':''; ?> value="<?php echo $i; ?>"> - <?php echo sprintf(_N('%d file', '%d files', $i), $i); ?></option> - <?php - } ?> - </select> - <em><?php echo __('(Number of files an agent is allowed to upload simultaneously)');?></em> - <font class="error"> <?php echo $errors['max_staff_file_uploads']; ?></font> +<?php + $tform = TicketForm::objects()->one()->getForm(); + $f = $tform->getField('message'); +?> + <a class="action-button field-config" style="float:none;overflow:inherit" + href="#ajax.php/form/field-config/<?php + echo $f->get('id'); ?>" + onclick="javascript: + $.dialog($(this).attr('href').substr(1), [201]); + return false; + "><i class="icon-edit"></i> <?php echo __('Config'); ?></a> + <i class="help-tip icon-question-sign" href="#ticket_attachment_settings"></i> </td> </tr> <tr> @@ -305,7 +264,8 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) } ?> </select> - <font class="error"> <?php echo $errors['max_file_size']; ?></font> + <i class="help-tip icon-question-sign" href="#max_file_size"></i> + <div class="error"><?php echo $errors['max_file_size']; ?></div> </td> </tr> <tr> @@ -330,18 +290,6 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) </td> </tr> <?php } ?> - <tr> - <th colspan="2"> - <em><strong><?php echo __('Accepted File Types');?></strong>: <?php echo __('Limit the type of files users are allowed to submit.');?> - <font class="error"> <?php echo $errors['allowed_filetypes']; ?></font></em> - </th> - </tr> - <tr> - <td colspan="2"> - <em><?php echo __('Enter allowed file extensions separated by a comma. e.g .doc, .pdf. To accept all files enter wildcard <b><i>.*</i></b> i.e dotStar (NOT Recommended).');?></em><br> - <textarea name="allowed_filetypes" cols="21" rows="4" style="width: 65%;" wrap="hard" ><?php echo $config['allowed_filetypes']; ?></textarea> - </td> - </tr> </tbody> </table> <p style="padding-left:250px;"> diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php index 98a2776742b6a51cf232f91e0456c1eea253b525..79ebe981bad0798a851d2e61285be0f38613c5d3 100644 --- a/include/staff/templates/dynamic-form.tmpl.php +++ b/include/staff/templates/dynamic-form.tmpl.php @@ -7,6 +7,8 @@ if (isset($options['entry']) && $options['mode'] == 'edit' ) return; +print $form->getMedia(); + if (isset($options['entry']) && $options['mode'] == 'edit') { ?> <tbody> <?php } ?> diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index 6a9b533ade9ccb692f05f24127d4e7f5fef78529..d677070ed7588a759d75e0fb248ea1412f5e4a10 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -302,34 +302,13 @@ if ($_POST) placeholder="<?php echo __('Initial response for the ticket'); ?>" name="response" id="response" cols="21" rows="8" style="width:80%;"><?php echo $info['response']; ?></textarea> - <table border="0" cellspacing="0" cellpadding="2" width="100%"> - <?php - if($cfg->allowAttachments()) { ?> - <tr><td width="100" valign="top"><?php echo __('Attachments');?>:</td> - <td> - <div class="canned_attachments"> - <?php - if($info['cannedattachments']) { - foreach($info['cannedattachments'] as $k=>$id) { - if(!($file=AttachmentFile::lookup($id))) continue; - $hash=$file->getKey().md5($file->getId().session_id().$file->getKey()); - echo sprintf('<label><input type="checkbox" name="cannedattachments[]" - id="f%d" value="%d" checked="checked" - <a href="file.php?h=%s">%s</a> </label> ', - $file->getId(), $file->getId() , $hash, $file->getName()); - } - } - ?> - </div> - <div class="uploads"></div> - <div class="file_input"> - <input type="file" class="multifile" name="attachments[]" size="30" value="" /> - </div> - </td> - </tr> - <?php - } ?> + <div class="attachments"> +<?php +print $response_form->getField('attachments')->render(); +?> + </div> + <table border="0" cellspacing="0" cellpadding="2" width="100%"> <tr> <td width="100"><?php echo __('Ticket Status');?>:</td> <td> @@ -430,9 +409,11 @@ $(function() { // Popup user lookup on the initial page load (not post) if we don't have a // user selected if (!$_POST && !$user) {?> - $.userLookup('ajax.php/users/lookup/form', function (user) { + setTimeout(function() { + $.userLookup('ajax.php/users/lookup/form', function (user) { window.location.href = window.location.href+'&uid='+user.id; - }); + }); + }, 100); <?php } ?> }); diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 2b6bc0477cfb915c4b4426aa399d2174600a0bfb..5da914258ea548bf4d18a46372bb06db13786b18 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -577,26 +577,13 @@ $tcount+= $ticket->getNumNotes(); rows="9" wrap="soft" class="richtext ifhtml draft draft-delete"><?php echo $info['response']; ?></textarea> + <div id="reply_form_attachments" class="attachments"> +<?php +print $response_form->getField('attachments')->render(); +?> + </div> </td> </tr> - <?php - if($cfg->allowAttachments()) { ?> - <tr> - <td width="120" style="vertical-align:top"> - <label for="attachment"><?php echo __('Attachments');?>:</label> - </td> - <td id="reply_form_attachments" class="attachments"> - <div class="canned_attachments"> - </div> - <div class="uploads"> - </div> - <div class="file_input"> - <input type="file" class="multifile" name="attachments[]" size="30" value="" /> - </div> - </td> - </tr> - <?php - }?> <tr> <td width="120"> <label for="signature" class="left"><?php echo __('Signature');?>:</label> @@ -693,26 +680,18 @@ $tcount+= $ticket->getNumNotes(); class="richtext ifhtml draft draft-delete"><?php echo $info['note']; ?></textarea> <span class="error"><?php echo $errors['note']; ?></span> - <br> </td> </tr> - <?php - if($cfg->allowAttachments()) { ?> <tr> <td width="120"> <label for="attachment"><?php echo __('Attachments');?>:</label> </td> <td class="attachments"> - <div class="uploads"> - </div> - <div class="file_input"> - <input type="file" class="multifile" name="attachments[]" size="30" value="" /> - </div> +<?php +print $note_form->getField('attachments')->render(); +?> </td> </tr> - <?php - } - ?> <tr><td colspan="2"> </td></tr> <tr> <td width="120"> diff --git a/js/filedrop.field.js b/js/filedrop.field.js new file mode 100644 index 0000000000000000000000000000000000000000..3bea172b3b5958d7c43f65fbe15adc49ebc7f6d4 --- /dev/null +++ b/js/filedrop.field.js @@ -0,0 +1,813 @@ +!function($) { + "use strict"; + + var FileDropbox = function(element, options) { + this.$element = $(element); + this.uploads = []; + + var events = { + uploadStarted: $.proxy(this.uploadStarted, this), + uploadFinished: $.proxy(this.uploadFinished, this), + progressUpdated: $.proxy(this.progressUpdated, this), + speedUpdated: $.proxy(this.speedUpdated, this), + dragOver: $.proxy(this.dragOver, this), + drop: $.proxy(this.drop, this), + beforeSend: $.proxy(this.beforeSend, this), + beforeEach: $.proxy(this.beforeEach, this), + error: $.proxy(this.handleError, this), + globalProgressUpdated: $.proxy(this.lockSubmit, this) + }; + + this.options = $.extend({}, $.fn.filedropbox.defaults, events, options); + this.$element.filedrop(this.options); + if (this.options.shim) { + $('input[type=file]', this.$element).attr('name', this.options.name) + .addClass('shim').css('display', 'inline-block').show(); + $('a.manual', this.$element).hide(); + } + (this.options.files || []).forEach($.proxy(this.addNode, this)); + }; + + FileDropbox.prototype = { + drop: function(e) { + this.$element.removeAttr('style'); + }, + dragOver: function(box, e) { + this.$element.css('background-color', 'rgba(0, 0, 0, 0.3)'); + }, + beforeEach: function (file) { + if (this.options.maxfiles && this.uploads.length >= this.options.maxfiles) { + // This file is not allowed to be added to the list. It's over the + // limit + this.handleError('TooManyFiles', file); + return false; + } + var node = this.addNode(file).data('file', file); + node.find('.progress').show(); + node.find('.progress-bar').width('100%').addClass('progress-bar-striped active'); + node.find('.trash').hide(); + }, + beforeSend: function (file, i, reader) { + this.uploads.some(function(e) { + if (e.data('file') == file) { + if (file.type.indexOf('image/') === 0 && file.size < 1e6) { + e.find('.preview').attr('src', 'data:' + file.type + ';base64,' + + btoa(reader.result)).tooltip({items:'img', + tooltipClass: 'tooltip-preview', + content:function(){ return $(this).clone().wrap('<div>'); }}); + } + return true; + } + }); + }, + speedUpdated: function(i, file, speed) { + var that = this; + this.uploads.some(function(e) { + if (e.data('file') == file) { + e.find('.upload-rate').text(that.fileSize(speed * 1024)+'/s'); + return true; + } + }); + }, + progressUpdated: function(i, file, value) { + this.uploads.some(function(e) { + if (e.data('file') == file) { + e.find('.progress').show(); + e.find('.progress-bar') + .width(value + '%') + .attr({'aria-valuenow': value}) + .removeClass('progress-bar-striped active'); + if (value > 99) + e.find('.progress-bar').addClass('progress-bar-striped active'); + return true; + } + }); + }, + uploadStarted: function(i, file, n, xhr) { + var that = this; + this.uploads.some(function(e) { + if (e.data('file') == file) { + e.data('xhr', xhr); + e.find('.cancel').show(); + that.progressUpdated(i, file, 0); + that.lockSubmit(1); + return true; + } + }); + }, + uploadFinished: function(i, file, json, time, xhr) { + var that = this; + this.uploads.some(function(e) { + if (e.data('file') == file) { + if (!json || !json.id) + return e.remove(); + e.find('[name="'+that.options.name+'"]').val(json.id); + e.data('fileId', json.id); + e.find('.progress-bar') + .width('100%') + .attr({'aria-valuenow': 100}); + e.find('.trash').show(); + e.find('.upload-rate').hide(); + e.find('.cancel').hide(); + setTimeout(function() { e.find('.progress').hide(); }, 600); + return true; + } + }); + }, + fileSize: function(size) { + var sizes = ['k','M','G','T'], + suffix = ''; + while (size > 900) { + size /= 1024; + suffix = sizes.shift(); + } + return (suffix ? size.toPrecision(3) + suffix : size) + 'B'; + }, + addNode: function(file) { + // Check if the file is already in the list of files for this dropbox + var already_added = false; + this.uploads.some(function(e) { + if (file.id && e.data('fileId') == file.id) { + already_added = true; + return true; + } + }); + if (already_added) + return; + + var filenode = $('<div class="file"></div>'); + filenode + .append($('<div class="filetype"></div>').addClass()) + .append($('<img class="preview" />')) + .append($('<div class="filename"></div>') + .append($('<span class="filesize"></span>').text( + this.fileSize(parseInt(file.size)) + )) + .append($('<div class="pull-right cancel"></div>') + .append($('<i class="icon-remove"></i>') + .attr('title', __('Cancel')) + ) + .click($.proxy(this.cancelUpload, this, filenode)) + .hide() + ) + .append($('<div class="upload-rate pull-right"></div>')) + ).append($('<div class="progress"></div>') + .append($('<div class="progress-bar"></div>')) + .attr({'aria-valuemin':0,'aria-valuemax':100}) + .hide()) + .append($('<input type="hidden"/>').attr('name', this.options.name) + .val(file.id)) + .append($('<div class="clear"></div>')); + if (this.options.deletable) { + filenode.prepend($('<span><i class="icon-trash"></i></span>') + .addClass('trash pull-right') + .click($.proxy(this.deleteNode, this, filenode)) + ); + } + if (file.id) + filenode.data('fileId', file.id); + if (file.download) + filenode.find('.filename').prepend( + $('<a class="no-pjax" target="_blank"></a>').text(file.name) + .attr('href', 'file.php?h='+escape(file.download)) + ); + else + filenode.find('.filename').prepend(document.createTextNode(file.name)); + this.$element.parent().find('.files').append(filenode); + this.uploads.push(filenode); + return filenode; + }, + deleteNode: function(filenode, e) { + if (!e || confirm(__('You sure?'))) { + var i = this.uploads.indexOf(filenode); + if (i !== -1) + this.uploads.splice(i,1); + filenode.slideUp('fast', function() { this.remove(); }); + } + }, + cancelUpload: function(node) { + if (node.data('xhr')) { + node.data('xhr').abort(); + return this.deleteNode(node, false); + } + }, + handleError: function(err, i, file, status) { + var message = $.fn.filedropbox.messages[err]; + if (file instanceof File) { + message = '<b>' + file.name + '</b><br/>' + message; + } + $.sysAlert(__('File Upload Error'), message); + }, + lockSubmit: function(total_progress) { + var submit = this.$element.closest('form').find('input[type=submit]'), + $submit = $(submit); + if (0 < total_progress && total_progress < 100) { + if (!$submit.data('original')) { + $submit.data('original', $submit.val()); + } + $submit.val(__('Uploading ...')).prop('disabled', true); + } + else if ($submit.data('original')) { + $submit.val($submit.data('original')).prop('disabled', false); + } + } + }; + + $.fn.filedropbox = function ( option ) { + return this.each(function () { + var $this = $(this), + data = $this.data('dropbox'), + options = typeof option == 'object' && option; + if (!data) $this.data('dropbox', (data = new FileDropbox(this, options))); + if (typeof option == 'string') data[option](); + }); + }; + + $.fn.filedropbox.defaults = { + files: [], + deletable: true, + shim: !window.FileReader, + queuefiles: 4 + }; + + $.fn.filedropbox.messages = { + 'BrowserNotSupported': __('Your browser is not supported'), + 'TooManyFiles': __('You are trying to upload too many files'), + 'FileTooLarge': __('File is too large'), + 'FileTypeNotAllowed': __('This type of file is not allowed'), + 'FileExtensionNotAllowed': __('This type of file is not allowed'), + 'NotFound': __('Could not find or read this file'), + 'NotReadable': __('Could not find or read this file'), + 'AbortError': __('Could not find or read this file') + }; + + $.fn.filedropbox.Constructor = FileDropbox; + +}(jQuery); + +/* + * Default text - jQuery plugin for html5 dragging files from desktop to browser + * + * Author: Weixi Yen + * + * Email: [Firstname][Lastname]@gmail.com + * + * Copyright (c) 2010 Resopollution + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * Project home: + * http://www.github.com/weixiyen/jquery-filedrop + * + * Version: 0.1.0 + * + * Features: + * Allows sending of extra parameters with file. + * Works with Firefox 3.6+ + * Future-compliant with HTML5 spec (will work with Webkit browsers and IE9) + * Usage: + * See README at project homepage + * + */ +;(function($) { + + jQuery.event.props.push("dataTransfer"); + + var default_opts = { + fallback_id: '', + link: false, + url: '', + refresh: 1000, + paramname: 'userfile', + requestType: 'POST', // just in case you want to use another HTTP verb + allowedfileextensions:[], + allowedfiletypes:[], + maxfiles: 25, // Ignored if queuefiles is set > 0 + maxfilesize: 1, // MB file size limit + queuefiles: 0, // Max files before queueing (for large volume uploads) + queuewait: 200, // Queue wait time if full + data: {}, + headers: {}, + drop: empty, + dragStart: empty, + dragEnter: empty, + dragOver: empty, + dragLeave: empty, + docEnter: empty, + docOver: empty, + docLeave: empty, + beforeEach: empty, + afterAll: empty, + rename: empty, + error: function(err, file, i, status) { + alert(err); + }, + uploadStarted: empty, + uploadFinished: empty, + progressUpdated: empty, + globalProgressUpdated: empty, + speedUpdated: empty + }, + errors = ["BrowserNotSupported", "TooManyFiles", "FileTooLarge", "FileTypeNotAllowed", "NotFound", "NotReadable", "AbortError", "ReadError", "FileExtensionNotAllowed"]; + + $.fn.filedrop = function(options) { + var opts = $.extend({}, default_opts, options), + global_progress = [], + doc_leave_timer, stop_loop = false, + files_count = 0, + files; + + if (window.FileReader) + $('#' + opts.fallback_id).css({ + display: 'none', + width: 0, + height: 0 + }); + + this.on('drop', drop).on('dragstart', opts.dragStart).on('dragenter', dragEnter).on('dragover', dragOver).on('dragleave', dragLeave); + $(document).on('drop', docDrop).on('dragenter', docEnter).on('dragover', docOver).on('dragleave', docLeave); + + (opts.link || this).on('click', function(e){ + $('#' + opts.fallback_id).trigger(e); + return false; + }); + + $('#' + opts.fallback_id).change(function(e) { + opts.drop(e); + files = e.target.files; + files_count = files.length; + upload(); + }); + + function drop(e) { + if( opts.drop.call(this, e) === false ) return false; + if(!e.dataTransfer) + return; + files = e.dataTransfer.files; + if (files === null || files === undefined || files.length === 0) { + opts.error(errors[0]); + return false; + } + files_count = files.length; + upload(); + e.preventDefault(); + return false; + } + + function getBuilder(filename, filedata, mime, boundary) { + var dashdash = '--', + crlf = '\r\n', + builder = '', + paramname = opts.paramname; + + if (opts.data) { + var params = $.param(opts.data).replace(/\+/g, '%20').split(/&/); + + $.each(params, function() { + var pair = this.split("=", 2), + name = decodeURIComponent(pair[0]), + val = decodeURIComponent(pair[1]); + + if (pair.length !== 2) { + return; + } + + builder += dashdash; + builder += boundary; + builder += crlf; + builder += 'Content-Disposition: form-data; name="' + name + '"'; + builder += crlf; + builder += crlf; + builder += val; + builder += crlf; + }); + } + + if (jQuery.isFunction(paramname)){ + paramname = paramname(filename); + } + + builder += dashdash; + builder += boundary; + builder += crlf; + builder += 'Content-Disposition: form-data; name="' + (paramname||"") + '"'; + builder += '; filename="' + encodeURIComponent(filename) + '"'; + builder += crlf; + + builder += 'Content-Type: ' + mime; + builder += crlf; + builder += crlf; + + builder += filedata; + builder += crlf; + + builder += dashdash; + builder += boundary; + builder += dashdash; + builder += crlf; + return builder; + } + + function progress(e) { + if (e.lengthComputable) { + var percentage = ((e.loaded * 100) / e.total).toFixed(1); + if (this.currentProgress != percentage) { + + this.currentProgress = percentage; + opts.progressUpdated(this.index, this.file, this.currentProgress); + + global_progress[this.global_progress_index] = this.currentProgress; + globalProgress(); + + var elapsed = new Date().getTime(); + var diffTime = elapsed - this.currentStart; + if (diffTime >= opts.refresh) { + var diffData = e.loaded - this.startData; + var speed = diffData / diffTime; // KB per second + opts.speedUpdated(this.index, this.file, speed); + this.startData = e.loaded; + this.currentStart = elapsed; + } + } + } + } + + function abort(e) { + global_progress[this.global_progress_index] = 100; + globalProgress(); + } + + function globalProgress() { + if (global_progress.length === 0) { + return; + } + + var total = 0, index; + for (index in global_progress) { + if(global_progress.hasOwnProperty(index)) { + total = total + global_progress[index]; + } + } + + opts.globalProgressUpdated(Math.round(total / global_progress.length)); + } + + // Respond to an upload + function upload() { + stop_loop = false; + + if (!files) { + opts.error(errors[0]); + return false; + } + + if (opts.allowedfiletypes.push && opts.allowedfiletypes.length) { + for(var fileIndex = files.length;fileIndex--;) { + if(!files[fileIndex].type || $.inArray(files[fileIndex].type, opts.allowedfiletypes) < 0) { + opts.error(errors[3], files[fileIndex], fileIndex); + return false; + } + } + } + + if (opts.allowedfileextensions.push && opts.allowedfileextensions.length) { + for(var fileIndex = files.length;fileIndex--;) { + var allowedextension = false; + for (i=0;i<opts.allowedfileextensions.length;i++){ + if (files[fileIndex].name.substr(files[fileIndex].name.length-opts.allowedfileextensions[i].length).toLowerCase() + == opts.allowedfileextensions[i].toLowerCase() + ) { + allowedextension = true; + } + } + if (!allowedextension){ + opts.error(errors[8], files[fileIndex], fileIndex); + return false; + } + } + } + + var filesDone = 0, + filesRejected = 0; + + if (files_count > opts.maxfiles && opts.queuefiles === 0) { + opts.error(errors[1]); + return false; + } + + // Define queues to manage upload process + var workQueue = []; + var processingQueue = []; + var doneQueue = []; + + // Add everything to the workQueue + for (var i = 0; i < files_count; i++) { + workQueue.push(i); + } + + // Helper function to enable pause of processing to wait + // for in process queue to complete + var pause = function(timeout) { + setTimeout(process, timeout); + return; + }; + + // Process an upload, recursive + var process = function() { + + var fileIndex; + + if (stop_loop) { + return false; + } + + // Check to see if are in queue mode + if (opts.queuefiles > 0 && processingQueue.length >= opts.queuefiles) { + return pause(opts.queuewait); + } else { + // Take first thing off work queue + fileIndex = workQueue[0]; + workQueue.splice(0, 1); + + // Add to processing queue + processingQueue.push(fileIndex); + } + + try { + if (beforeEach(files[fileIndex]) !== false) { + if (fileIndex === files_count) { + return; + } + var reader = new FileReader(), + max_file_size = 1048576 * opts.maxfilesize; + + reader.index = fileIndex; + if (files[fileIndex].size > max_file_size) { + opts.error(errors[2], files[fileIndex], fileIndex); + // Remove from queue + processingQueue.forEach(function(value, key) { + if (value === fileIndex) { + processingQueue.splice(key, 1); + } + }); + filesRejected++; + return true; + } + + reader.onerror = function(e) { + switch(e.target.error.code) { + case e.target.error.NOT_FOUND_ERR: + opts.error(errors[4], files[fileIndex], fileIndex); + return false; + case e.target.error.NOT_READABLE_ERR: + opts.error(errors[5], files[fileIndex], fileIndex); + return false; + case e.target.error.ABORT_ERR: + opts.error(errors[6], files[fileIndex], fileIndex); + return false; + default: + opts.error(errors[7], files[fileIndex], fileIndex); + return false; + }; + }; + + reader.onloadend = function(e) { + if (!opts.beforeSend + || false !== opts.beforeSend(files[fileIndex], fileIndex, e.target)) + return send(e); + }; + + reader.readAsBinaryString(files[fileIndex]); + + } else { + filesRejected++; + } + } catch (err) { + // Remove from queue + processingQueue.forEach(function(value, key) { + if (value === fileIndex) { + processingQueue.splice(key, 1); + } + }); + opts.error(errors[0], files[fileIndex], fileIndex, err); + return false; + } + + // If we still have work to do, + if (workQueue.length > 0) { + process(); + } + }; + + var send = function(e) { + + var fileIndex = (e.srcElement || e.target).index; + + // Sometimes the index is not attached to the + // event object. Find it by size. Hack for sure. + if (e.target.index === undefined) { + e.target.index = getIndexBySize(e.total); + } + + var xhr = new XMLHttpRequest(), + upload = xhr.upload, + file = files[e.target.index], + index = e.target.index, + start_time = new Date().getTime(), + boundary = '------multipartformboundary' + (new Date()).getTime(), + global_progress_index = global_progress.length, + builder, + newName = rename(file.name), + mime = file.type; + + if (opts.withCredentials) { + xhr.withCredentials = opts.withCredentials; + } + + var data = e.target.result; + if (typeof newName === "string") { + builder = getBuilder(newName, data, mime, boundary); + } else { + builder = getBuilder(file.name, data, mime, boundary); + } + + upload.index = index; + upload.file = file; + upload.downloadStartTime = start_time; + upload.currentStart = start_time; + upload.currentProgress = 0; + upload.global_progress_index = global_progress_index; + upload.startData = 0; + upload.addEventListener("progress", progress, false); + upload.addEventListener("abort", abort, false); + + // Allow url to be a method + if (jQuery.isFunction(opts.url)) { + xhr.open(opts.requestType, opts.url(), true); + } else { + xhr.open(opts.requestType, opts.url, true); + } + + xhr.setRequestHeader('content-type', 'multipart/form-data; boundary=' + boundary); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + + // Add headers + $.each(opts.headers, function(k, v) { + xhr.setRequestHeader(k, v); + }); + + xhr.sendAsBinary(builder); + + global_progress[global_progress_index] = 0; + globalProgress(); + + opts.uploadStarted(index, file, files_count, xhr); + + xhr.onload = function() { + var serverResponse = null; + + if (xhr.responseText) { + try { + serverResponse = jQuery.parseJSON(xhr.responseText); + } + catch (e) { + serverResponse = xhr.responseText; + } + } + + var now = new Date().getTime(), + timeDiff = now - start_time, + result = opts.uploadFinished(index, file, serverResponse, timeDiff, xhr); + filesDone++; + + // Remove from processing queue + processingQueue.forEach(function(value, key) { + if (value === fileIndex) { + processingQueue.splice(key, 1); + } + }); + + // Add to donequeue + doneQueue.push(fileIndex); + + // Make sure the global progress is updated + global_progress[global_progress_index] = 100; + globalProgress(); + + if (filesDone === (files_count - filesRejected)) { + afterAll(); + } + if (result === false) { + stop_loop = true; + } + + + // Pass any errors to the error option + if (xhr.status < 200 || xhr.status > 299) { + opts.error(xhr.statusText, file, fileIndex, xhr.status); + } + }; + }; + + // Initiate the processing loop + process(); + } + + function getIndexBySize(size) { + for (var i = 0; i < files_count; i++) { + if (files[i].size === size) { + return i; + } + } + + return undefined; + } + + function rename(name) { + return opts.rename(name); + } + + function beforeEach(file) { + return opts.beforeEach(file); + } + + function afterAll() { + return opts.afterAll(); + } + + function dragEnter(e) { + clearTimeout(doc_leave_timer); + e.preventDefault(); + opts.dragEnter.call(this, e); + } + + function dragOver(e) { + clearTimeout(doc_leave_timer); + e.preventDefault(); + opts.docOver.call(this, e); + opts.dragOver.call(this, e); + } + + function dragLeave(e) { + clearTimeout(doc_leave_timer); + opts.dragLeave.call(this, e); + e.stopPropagation(); + } + + function docDrop(e) { + e.preventDefault(); + opts.docLeave.call(this, e); + return false; + } + + function docEnter(e) { + clearTimeout(doc_leave_timer); + e.preventDefault(); + opts.docEnter.call(this, e); + return false; + } + + function docOver(e) { + clearTimeout(doc_leave_timer); + e.preventDefault(); + opts.docOver.call(this, e); + return false; + } + + function docLeave(e) { + doc_leave_timer = setTimeout((function(_this) { + return function() { + opts.docLeave.call(_this, e); + }; + })(this), 200); + } + + return this; + }; + + function empty() {} + + try { + if (XMLHttpRequest.prototype.sendAsBinary) { + return; + } + XMLHttpRequest.prototype.sendAsBinary = function(datastr) { + function byteValue(x) { + return x.charCodeAt(0) & 0xff; + } + var ords = Array.prototype.map.call(datastr, byteValue); + var ui8a = new Uint8Array(ords); + + // Not pretty: Chrome 22 deprecated sending ArrayBuffer, moving instead + // to sending ArrayBufferView. Sadly, no proper way to detect this + // functionality has been discovered. Happily, Chrome 22 also introduced + // the base ArrayBufferView class, not present in Chrome 21. + if ('ArrayBufferView' in window) + this.send(ui8a); + else + this.send(ui8a.buffer); + }; + } catch (e) {} + +})(jQuery); diff --git a/js/osticket.js b/js/osticket.js index ae166d4e3a2d9dadb8747639ed99f388491a7174..099169067145379c95429d28a08c3ed26df982a3 100644 --- a/js/osticket.js +++ b/js/osticket.js @@ -94,20 +94,6 @@ $(document).ready(function(){ } })(); - /* Multifile uploads */ - var elems = $('.multifile'); - if (elems.exists()) { - /* Get config settings from the backend */ - getConfig().then(function(c) { - elems.multifile({ - container: '.uploads', - max_uploads: c.max_file_uploads || 1, - file_types: c.file_types || ".*", - max_file_size: c.max_file_size || 0 - }); - }); - } - $.translate_format = function(str) { var translation = { 'd':'dd', diff --git a/open.php b/open.php index d7aa3cb986c58b2242b19ca09241635823824c08..2311bc2bc57759bbcd812cdb75ace610fc504439 100644 --- a/open.php +++ b/open.php @@ -29,8 +29,11 @@ if ($_POST) { $errors['captcha']=__('Invalid - try again!'); } - if (!$errors && $cfg->allowOnlineAttachments() && $_FILES['attachments']) - $vars['files'] = AttachmentFile::format($_FILES['attachments'], true); + $tform = TicketForm::objects()->one()->getForm($vars); + $messageField = $tform->getField('message'); + $attachments = $messageField->getWidget()->getAttachments(); + if (!$errors && $messageField->isAttachmentsEnabled()) + $vars['cannedattachments'] = $attachments->getClean(); // Drop the draft.. If there are validation errors, the content // submitted will be displayed back to the user diff --git a/scp/ajax.php b/scp/ajax.php index cdeec76d9bb1f7ef5b5e3ae3b86fb009b47e4fe9..7cfe289d568964f6fd8fb22e5b4f349460d793ab 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -55,7 +55,9 @@ $dispatcher = patterns('', url_get('^help-topic/(?P<id>\d+)$', 'getFormsForHelpTopic'), url_get('^field-config/(?P<id>\d+)$', 'getFieldConfiguration'), url_post('^field-config/(?P<id>\d+)$', 'saveFieldConfiguration'), - url_delete('^answer/(?P<entry>\d+)/(?P<field>\d+)$', 'deleteAnswer') + url_delete('^answer/(?P<entry>\d+)/(?P<field>\d+)$', 'deleteAnswer'), + url_post('^upload/(\d+)?$', 'upload'), + url_post('^upload/(\w+)?$', 'attach') )), url('^/list/', patterns('ajax.forms.php:DynamicFormsAjaxAPI', url_get('^(?P<list>\w+)/item/(?P<id>\d+)/properties$', 'getListItemProperties'), diff --git a/scp/canned.php b/scp/canned.php index b9a1ea3c346b251dc98154e57012718271ba63a4..4a5447525d5dc6c995641ada7dabe379b0e35f20 100644 --- a/scp/canned.php +++ b/scp/canned.php @@ -28,6 +28,13 @@ $canned=null; if($_REQUEST['id'] && !($canned=Canned::lookup($_REQUEST['id']))) $errors['err']=sprintf(__('%s: Unknown or invalid ID.'), __('canned response')); +$canned_form = new Form(array( + 'attachments' => new FileUploadField(array('id'=>'attach', + 'configuration'=>array('extensions'=>false, + 'size'=>$cfg->getMaxFileSize()) + )), +)); + if($_POST && $thisstaff->canManageCannedResponses()) { switch(strtolower($_POST['do'])) { case 'update': @@ -38,7 +45,7 @@ if($_POST && $thisstaff->canManageCannedResponses()) { __('this canned response')); //Delete removed attachments. //XXX: files[] shouldn't be changed under any circumstances. - $keepers = $_POST['files']?$_POST['files']:array(); + $keepers = $canned_form->getField('attachments')->getClean(); $attachments = $canned->attachments->getSeparates(); //current list of attachments. foreach($attachments as $k=>$file) { if($file['id'] && !in_array($file['id'], $keepers)) { @@ -46,8 +53,8 @@ if($_POST && $thisstaff->canManageCannedResponses()) { } } //Upload NEW attachments IF ANY - TODO: validate attachment types?? - if($_FILES['attachments'] && ($files=AttachmentFile::format($_FILES['attachments']))) - $canned->attachments->upload($files); + if ($keepers) + $canned->attachments->upload($attachments); // Attach inline attachments from the editor if (isset($_POST['draft_id']) @@ -77,8 +84,9 @@ if($_POST && $thisstaff->canManageCannedResponses()) { $msg=sprintf(__('Successfully added %s'), Format::htmlchars($_POST['title'])); $_REQUEST['a']=null; //Upload attachments - if($_FILES['attachments'] && ($c=Canned::lookup($id)) && ($files=AttachmentFile::format($_FILES['attachments']))) - $c->attachments->upload($files); + $keepers = $canned_form->getField('attachments')->getClean(); + if ($keepers && ($c=Canned::lookup($id))) + $c->attachments->upload($keepers); // Attach inline attachments from the editor if (isset($_POST['draft_id']) @@ -169,5 +177,6 @@ $ost->addExtraHeader('<meta name="tip-namespace" content="' . $tip_namespace . ' "$('#content').data('tipNamespace', '".$tip_namespace."');"); require(STAFFINC_DIR.'header.inc.php'); require(STAFFINC_DIR.$page); +print $canned_form->getMedia(); include(STAFFINC_DIR.'footer.inc.php'); ?> diff --git a/scp/css/scp.css b/scp/css/scp.css index 27b80dfa0938eb7fdf3eaabb048f9269805d5a87..30c7dcb42c7ffea790b0ef27b0e963e75079ef88 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1444,7 +1444,7 @@ time { } .dialog.draggable h3:hover { - cursor: crosshair; + cursor: move; } #advanced-search fieldset.span6 { diff --git a/scp/faq.php b/scp/faq.php index 47c61203db0cf752c07896ef78f4c2659602894b..2a3c2e95116e2b6da074df0e0655366d581ddec3 100644 --- a/scp/faq.php +++ b/scp/faq.php @@ -23,8 +23,16 @@ if($_REQUEST['id'] && !($faq=FAQ::lookup($_REQUEST['id']))) if($_REQUEST['cid'] && !$faq && !($category=Category::lookup($_REQUEST['cid']))) $errors['err']=sprintf(__('%s: Unknown or invalid'), __('FAQ category')); +$faq_form = new Form(array( + 'attachments' => new FileUploadField(array('id'=>'attach', + 'configuration'=>array('extensions'=>false, + 'size'=>$cfg->getMaxFileSize()) + )), +)); + if($_POST): $errors=array(); + $_POST['files'] = $faq_form->getField('attachments')->getClean(); switch(strtolower($_POST['do'])) { case 'create': case 'add': @@ -108,5 +116,6 @@ $ost->addExtraHeader('<meta name="tip-namespace" content="' . $tip_namespace . ' "$('#content').data('tipNamespace', '".$tip_namespace."');"); require_once(STAFFINC_DIR.'header.inc.php'); require_once(STAFFINC_DIR.$inc); +print $faq_form->getMedia(); require_once(STAFFINC_DIR.'footer.inc.php'); ?> diff --git a/scp/js/scp.js b/scp/js/scp.js index dcd8b80f5915d98e4da6136b9b7c35ffc10d2629..e6e36147e5d093d7526e76b9170c1baef1c5cc16 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -93,12 +93,12 @@ var scp_prep = function() { $('.dialog#confirm-action').delegate('input.confirm', 'click.confirm', function(e) { e.preventDefault(); $('.dialog#confirm-action').hide(); - $('#overlay').hide(); + $.toggleOverlay(false); $('input#action', formObj).val(action); formObj.submit(); return false; }); - $('#overlay').show(); + $.toggleOverlay(true); $('.dialog#confirm-action .confirm-action').hide(); $('.dialog#confirm-action p#'+this.name+'-confirm') .show() @@ -115,7 +115,7 @@ var scp_prep = function() { var action = $(this).attr('href').substr(1, $(this).attr('href').length); $('input#action', $dialog).val(action); - $('#overlay').show(); + $.toggleOverlay(true); $('.confirm-action', $dialog).hide(); $('p'+$(this).attr('href')+'-confirm', $dialog) .show() @@ -229,16 +229,12 @@ var scp_prep = function() { redactor.observeStart(); } //Canned attachments. - if(canned.files && $('.canned_attachments',fObj).length) { + var ca = $('.attachments', fObj); + if(canned.files && ca.length) { + var fdb = ca.find('.dropzone').data('dropbox'); $.each(canned.files,function(i, j) { - if(!$('.canned_attachments #f'+j.id,fObj).length) { - var file='<span><label><input type="checkbox" name="cannedattachments[]" value="' + j.id+'" id="f'+j.id+'" checked="checked">'; - file+= ' '+ j.name + '</label>'; - file+= ' (<a target="_blank" class="no-pjax" href="file.php?h=' + j.key + j.hash + '">view</a>) </span>'; - $('.canned_attachments', fObj).append(file); - } - - }); + fdb.addNode(j); + }); } } }) @@ -249,14 +245,6 @@ var scp_prep = function() { /* Get config settings from the backend */ getConfig().then(function(c) { - // Multifile uploads - $('.multifile').multifile({ - container: '.uploads', - max_uploads: c.max_file_uploads || 1, - file_types: c.file_types || ".*", - max_file_size: c.max_file_size || 0 - }); - // Datepicker $('.dp').datepicker({ numberOfMonths: 2, @@ -345,7 +333,7 @@ var scp_prep = function() { $('.dialog').delegate('input.close, a.close', 'click', function(e) { e.preventDefault(); $(this).parents('div.dialog').hide() - $('#overlay').hide(); + $.toggleOverlay(false); return false; }); @@ -365,7 +353,7 @@ var scp_prep = function() { $('#go-advanced').click(function(e) { e.preventDefault(); $('#result-count').html(''); - $('#overlay').show(); + $.toggleOverlay(true); $('#advanced-search').show(); }); @@ -525,7 +513,7 @@ $(document).keydown(function(e) { if (e.keyCode == 27 && !$('#overlay').is(':hidden')) { $('div.dialog').hide(); - $('#overlay').hide(); + $.toggleOverlay(false); e.preventDefault(); e.stopPropagation(); @@ -533,6 +521,20 @@ $(document).keydown(function(e) { } }); +$.toggleOverlay = function (show) { + if (typeof(show) === 'undefined') { + return $.toggleOverlay(!$('#overlay').is(':visible')); + } + if (show) { + $('#overlay').show().fadeIn(); + $('body').css('overflow', 'hidden'); + } + else { + $('#overlay').fadeOut(); + $('body').css('overflow', 'auto'); + } +}; + $.dialog = function (url, codes, cb, options) { options = options||{}; @@ -541,19 +543,18 @@ $.dialog = function (url, codes, cb, options) { var $popup = $('.dialog#popup'); - $('#overlay').show(); + $.toggleOverlay(true); $('div.body', $popup).empty().hide(); $('div#popup-loading', $popup).show() .find('h1').css({'margin-top':function() { return $popup.height()/3-$(this).height()/3}}); $popup.show(); $('div.body', $popup).load(url, function () { $('div#popup-loading', $popup).hide(); - $('#overlay').show(); - $('div.body', $popup).show({ - duration: 0, + $('div.body', $popup).slideDown({ + duration: 300, + queue: false, complete: function() { if (options.onshow) options.onshow(); } }); - $popup.show(); $(document).off('.dialog'); $(document).on('submit.dialog', '.dialog#popup form', function(e) { e.preventDefault(); @@ -568,12 +569,13 @@ $.dialog = function (url, codes, cb, options) { success: function(resp, status, xhr) { if (xhr && xhr.status && codes && $.inArray(xhr.status, codes) != -1) { - $('div.body', $popup).empty(); + $.toggleOverlay(false); $popup.hide(); - $('#overlay').hide(); + $('div.body', $popup).empty(); if(cb) cb(xhr); } else { $('div.body', $popup).html(resp); + $popup.effect('shake'); $('#msg_notice, #msg_error', $popup).delay(5000).slideUp(); } } @@ -591,7 +593,7 @@ $.dialog = function (url, codes, cb, options) { $.sysAlert = function (title, msg, cb) { var $dialog = $('.dialog#alert'); if ($dialog.length) { - $('#overlay').show(); + $.toggleOverlay(true); $('#title', $dialog).html(title); $('#body', $dialog).html(msg); $dialog.show(); @@ -703,23 +705,19 @@ $(document).on('pjax:send', function(event) { $('#overlay').css('background-color','white').fadeIn(); }); +$(document).on('pjax:beforeReplace', function() { + // Close popups + // Close tooltips + $('.tip_box').remove(); + $('.dialog .body').empty().parent().hide(); +}); + $(document).on('pjax:complete', function() { // right $("#loadingbar").width("101%").delay(200).fadeOut(400, function() { $(this).remove(); }); - - $('.tip_box').remove(); - $('.dialog .body').empty().parent().hide(); - $('#overlay').stop(false, true).hide().removeAttr('style'); -}); - -$(document).on('pjax:end', function() { - // Close popups - // Close tooltips - $('.tip_box').remove(); - $('.dialog .body').empty().parent().hide(); - $('#overlay').hide(); + $('#overlay').fadeOut(100).removeAttr('style'); }); // Quick note interface diff --git a/scp/tickets.php b/scp/tickets.php index 7de8a89fb49bc165f769a4e1a4575565c5e6edbb..cd7d5ebfbae570cd656ba4932abfa7fe613e38d2 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -39,6 +39,18 @@ if($_REQUEST['id']) { if ($_REQUEST['uid']) $user = User::lookup($_REQUEST['uid']); +// Configure form for file uploads +$response_form = new Form(array( + 'attachments' => new FileUploadField(array('id'=>'attach', + 'name'=>'attach:response', + 'configuration' => array('extensions'=>''))) +)); +$note_form = new Form(array( + 'attachments' => new FileUploadField(array('id'=>'attach', + 'name'=>'attach:note', + 'configuration' => array('extensions'=>''))) +)); + //At this stage we know the access status. we can process the post. if($_POST && !$errors): @@ -65,8 +77,7 @@ if($_POST && !$errors): //If no error...do the do. $vars = $_POST; - if(!$errors && $_FILES['attachments']) - $vars['files'] = AttachmentFile::format($_FILES['attachments']); + $vars['cannedattachments'] = $response_form->getField('attachments')->getClean(); if(!$errors && ($response=$ticket->postReply($vars, $errors, $_POST['emailreply']))) { $msg = sprintf(__('%s: Reply posted successfully'), @@ -75,6 +86,10 @@ if($_POST && !$errors): $ticket->getId(), $ticket->getNumber())) ); + // Clear attachment list + $response_form->setSource(array()); + $response_form->getField('attachments')->reset(); + // Remove staff's locks TicketLock::removeStaffLocks($thisstaff->getId(), $ticket->getId()); @@ -168,13 +183,18 @@ if($_POST && !$errors): break; case 'postnote': /* Post Internal Note */ $vars = $_POST; - if($_FILES['attachments']) - $vars['files'] = AttachmentFile::format($_FILES['attachments']); + $attachments = $note_form->getField('attachments')->getClean(); + $vars['cannedattachments'] = array_merge( + $vars['cannedattachments'] ?: array(), $attachments); $wasOpen = ($ticket->isOpen()); if(($note=$ticket->postNote($vars, $errors, $thisstaff))) { $msg=__('Internal note posted successfully'); + // Clear attachment list + $note_form->setSource(array()); + $note_form->getField('attachments')->reset(); + if($wasOpen && $ticket->isClosed()) $ticket = null; //Going back to main listing. else @@ -315,12 +335,17 @@ if($_POST && !$errors): $vars = $_POST; $vars['uid'] = $user? $user->getId() : 0; + $vars['cannedattachments'] = $response_form->getField('attachments')->getClean(); + if(($ticket=Ticket::open($vars, $errors))) { $msg=__('Ticket created successfully'); $_REQUEST['a']=null; if (!$ticket->checkStaffAccess($thisstaff) || $ticket->isClosed()) $ticket=null; Draft::deleteForNamespace('ticket.staff%', $thisstaff->getId()); + // Drop files from the response attachments widget + $response_form->setSource(array()); + $response_form->getField('attachments')->reset(); unset($_SESSION[':form-data']); } elseif(!$errors['err']) { $errors['err']=__('Unable to create the ticket. Correct the error(s) and try again'); @@ -465,4 +490,5 @@ if($ticket) { require_once(STAFFINC_DIR.'header.inc.php'); require_once(STAFFINC_DIR.$inc); +print $response_form->getMedia(); require_once(STAFFINC_DIR.'footer.inc.php'); diff --git a/tickets.php b/tickets.php index 4e38384b2b05a877701f72c05d6479873c742bd2..57b7ee0e8ec949c2d99a9ce8c63b2c11f9ba4bd6 100644 --- a/tickets.php +++ b/tickets.php @@ -35,6 +35,10 @@ if($_REQUEST['id']) { if (!$ticket && $thisclient->isGuest()) Http::redirect('view.php'); +$tform = TicketForm::objects()->one(); +$messageField = $tform->getField('message'); +$attachments = $messageField->getWidget()->getAttachments(); + //Process post...depends on $ticket object above. if($_POST && is_object($ticket) && $ticket->getId()): $errors=array(); @@ -75,8 +79,7 @@ if($_POST && is_object($ticket) && $ticket->getId()): 'userId' => $thisclient->getId(), 'poster' => (string) $thisclient->getName(), 'message' => $_POST['message']); - if($cfg->allowOnlineAttachments() && $_FILES['attachments']) - $vars['files'] = AttachmentFile::format($_FILES['attachments'], true); + $vars['cannedattachments'] = $response_form->getField('attachments')->getClean(); if (isset($_POST['draft_id'])) $vars['draft_id'] = $_POST['draft_id']; @@ -85,6 +88,9 @@ if($_POST && is_object($ticket) && $ticket->getId()): // Cleanup drafts for the ticket. If not closed, only clean // for this staff. Else clean all drafts for the ticket. Draft::deleteForNamespace('ticket.client.' . $ticket->getId()); + // Drop attachments + $response_form->getField('attachments')->reset(); + $response_form->setSource(array()); } else { $errors['err']=__('Unable to post the message. Try again'); } @@ -117,5 +123,6 @@ if($ticket && $ticket->checkUserAccess($thisclient)) { } include(CLIENTINC_DIR.'header.inc.php'); include(CLIENTINC_DIR.$inc); +print $tform->getMedia(); include(CLIENTINC_DIR.'footer.inc.php'); ?>