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">&nbsp;<?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)'); ?>
-                &nbsp;<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>&nbsp;&nbsp;</label>&nbsp;',
-                                      $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)'); ?>
+                &nbsp;<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">&nbsp;<?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>&nbsp;&nbsp;</label>&nbsp;',
-                                      $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>
-              &nbsp; <em>(<?php echo __('Global Setting'); ?>)</em>
-                &nbsp;<font class="error">&nbsp;<?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');?>
-                    &nbsp;<font class="error">&nbsp;<?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');?> &nbsp;&nbsp;&nbsp;&nbsp;
-                <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">&nbsp;<?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>
-                &nbsp;<font class="error">&nbsp;<?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>
-                &nbsp;<font class="error">&nbsp;<?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">&nbsp;<?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">&nbsp;<?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>&nbsp;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>&nbsp;&nbsp;</label>&nbsp;',
-                                            $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">&nbsp;</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');
 ?>