diff --git a/.gitignore b/.gitignore
index 3f02437e02e80eb03aa1bdc0a0c90ad51a2a112e..a36f4e07b61ea6bddad624fa51870f5551c69f6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ stage
 # Ignore mPDF temp files
 include/mpdf/ttfontdata
 include/mpdf/tmp
+nbproject/
diff --git a/WHATSNEW.md b/WHATSNEW.md
index 4523347765df7bf305e94ea25973092fae0b4518..61c3aaaaf3b589f35444e7126f6dd3a4768eb772 100644
--- a/WHATSNEW.md
+++ b/WHATSNEW.md
@@ -1,3 +1,39 @@
+osTicket v1.9.6
+===============
+### Enhancements
+  * New Message-Id system allowing for better threading in mail clients (#1549,
+    #1730)
+  * Fix forced session expiration after 24 hours (#1677)
+  * Staff panel logo is customizable (#1718)
+  * Priority fields have a selectable default (instead of system default) (#1732)
+  * Import/Export support for file contents via cli (#1661)
+
+### Improvements
+  * Fix broken links in documentation, thanks @Chefkeks (#1675)
+  * Fix handling of some Redmond-specific character set encoding names (#1698)
+  * Include the users name in the "To" field of outbound email (#1549)
+  * Delete collaborators when deleting tickets (#1709)
+  * Fix regression preventing auto-responses for staff new tickets (#1712)
+  * Fix empty export if ticket details form has multiple priority fields (#1732)
+  * Fix filtering by list item properties in ticket filters (#1741)
+  * Fix missing icon for "add new filter", thanks @Chefkeks (#1735)
+  * Support Firefox v6 - v12 on the file drop widget (#1776)
+  * Show update errors on access templates (#1778)
+  * Allow empty staff login banner on update (#1778)
+  * Fix corruption of text thread bodies for third-party collaborator email
+    posts (#1794)
+  * Add some hidden template variables to pop out content (#1781)
+  * Fix missing validation for user name and email address (#1816, eb8858e)
+  * Turn off search indexing when complete, disable incorrectly implemented
+    work breaking, squelch error 1062 email from search backend (afa9692)
+  * Fix possible out of memory crash in custom forms (#1707, 0440111)
+
+### Performance and Security
+  * Fix generation of random data on Windows® platforms (#1672)
+  * Fix possible DoS and brute force on login pages (#1727)
+  * Fix possible redirect away from HTTPS on client login page, thanks @ldrumm
+    (#1782)
+
 osTicket v1.9.5.1
 =================
 ### Improvements
diff --git a/assets/default/images/poweredby.png b/assets/default/images/poweredby.png
index 9b2915505a144a89e12338f081b30dcd032b4e40..8f3d4821e88a50a6b3137a5fe55bd4ccfd524869 100644
Binary files a/assets/default/images/poweredby.png and b/assets/default/images/poweredby.png differ
diff --git a/include/ajax.config.php b/include/ajax.config.php
index c625d75502b7d734ffe4a01a0adcef6e8af5832c..c263bd4a253993240b27e4557060b1fadc689ae1 100644
--- a/include/ajax.config.php
+++ b/include/ajax.config.php
@@ -21,7 +21,7 @@ class ConfigAjaxAPI extends AjaxController {
 
     //config info UI might need.
     function scp() {
-        global $cfg;
+        global $cfg, $thisstaff;
 
         $lang = Internationalization::getCurrentLanguage();
         $info = Internationalization::getLanguageInfo($lang);
@@ -48,6 +48,7 @@ class ConfigAjaxAPI extends AjaxController {
               'primary_lang_flag' => strtolower($primary_info['flag'] ?: $primary_locale ?: $primary_sl),
               'primary_language' => $primary,
               'secondary_languages' => $cfg->getSecondaryLanguages(),
+              'page_size'       => $thisstaff->getPageLimit(),
         );
         return $this->json_encode($config);
     }
diff --git a/include/ajax.content.php b/include/ajax.content.php
index 8cd1d5bfd9958b9304c0fde5e5b3a60a1fef3fc4..9ea0d7d1431bdf58aac6b2ffada47acb7e4585bd 100644
--- a/include/ajax.content.php
+++ b/include/ajax.content.php
@@ -64,14 +64,15 @@ class ContentAjaxAPI extends AjaxController {
                     <tr><td>%{ticket.create_date}</td><td>'.__('Date created').'</td></tr>
                     <tr><td>%{ticket.due_date}</td><td>'.__('Due date').'</td></tr>
                     <tr><td>%{ticket.close_date}</td><td>'.__('Date closed').'</td></tr>
-                    <tr><td>%{recipient.ticket_link}</td><td>'.__('Auth. token used for auto-login').'</td></tr>
-                    <tr><td>%{ticket.client_link}</td><td>'.__('Client\'s ticket view link').'</td></tr>
-                    <tr><td>%{recipient.ticket_link}</td><td>'.__('Agent\'s ticket view link').'</td></tr>
-                    <tr><td colspan="2" style="padding:5px 0 5px 0;"><em>'.__('Expandable Variables (See Wiki)').'</em></td></tr>
-                    <tr><td>%{ticket.<b>topic</b>}</td><td>'.__('Help topic').'</td></tr>
-                    <tr><td>%{ticket.<b>dept</b>}</td><td>'.__('Department').'</td></tr>
-                    <tr><td>%{ticket.<b>staff</b>}</td><td>'.__('Assigned/closing agent').'</td></tr>
-                    <tr><td>%{ticket.<b>team</b>}</td><td>'.__('Assigned/closing team').'</td></tr>
+                    <tr><td>%{ticket.recipients}</td><td>'.__('List of all recipient names').'</td></tr>
+                    <tr><td nowrap>%{recipient.ticket_link}</td><td>'.__('Auth. token used for auto-login').'<br/>
+                    '.__('Agent\'s ticket view link').'</td></tr>
+                    <tr><td colspan="2" style="padding:5px 0 5px 0;"><em><b>'.__('Expandable Variables').'</b></em></td></tr>
+                    <tr><td>%{ticket.topic}</td><td>'.__('Help topic').'</td></tr>
+                    <tr><td>%{ticket.dept}</td><td>'.__('Department').'</td></tr>
+                    <tr><td>%{ticket.staff}</td><td>'.__('Assigned/closing agent').'</td></tr>
+                    <tr><td>%{ticket.team}</td><td>'.__('Assigned/closing team').'</td></tr>
+                    <tr><td>%{ticket.thread}</td><td>'.__('Ticket Thread').'</td></tr>
                 </table>
             </td>
             <td valign="top">
@@ -89,14 +90,17 @@ class ContentAjaxAPI extends AjaxController {
                 <table width="100%" border="0" cellspacing=1 cellpadding=1>
                     <tr><td colspan="2"><b>'.__('Name Expansion').'</b></td></tr>
                     <tr><td>.first</td><td>'.__('First Name').'</td></tr>
-                    <tr><td>.middle</td><td>'.__('Middle Name(s)').'</td></tr>
                     <tr><td>.last</td><td>'.__('Last Name').'</td></tr>
                     <tr><td>.full</td><td>'.__('First Last').'</td></tr>
-                    <tr><td>.legal</td><td>'.__('First M. Last').'</td></tr>
                     <tr><td>.short</td><td>'.__('First L.').'</td></tr>
-                    <tr><td>.formal</td><td>'.__('Mr. Last').'</td></tr>
                     <tr><td>.shortformal</td><td>'.__('F. Last').'</td></tr>
                     <tr><td>.lastfirst</td><td>'.__('Last, First').'</td></tr>
+                    <tr><td colspan="2" style="padding:5px 0 5px 0;"><em><b>'.__('Ticket Thread expansions').'</b></em></td></tr>
+                    <tr><td>.original</td><td>'.__('Original Message').'</td></tr>
+                    <tr><td>.lastmessage</td><td>'.__('Last Message').'</td></tr>
+                    <tr><td colspan="2" style="padding:5px 0 5px 0;"><em><b>'.__('Thread Entry expansions').'</b></em></td></tr>
+                    <tr><td>.poster</td><td>'.__('Poster').'</td></tr>
+                    <tr><td>.create_date</td><td>'.__('Date created').'</td></tr>
                 </table>
             </td>
         </tr>
@@ -139,12 +143,17 @@ class ContentAjaxAPI extends AjaxController {
 
         $langs = Internationalization::getConfiguredSystemLanguages();
         $translations = $content->getAllTranslations();
-        $info = array();
+        $info = array(
+            'title' => $content->getTitle(),
+            'body' => $content->getBody(),
+        );
         foreach ($translations as $t) {
             if (!($data = $t->getComplex()))
                 continue;
-            $info['title'][$t->lang] = $data['name'];
-            $info['body'][$t->lang] = $data['body'];
+            $info['trans'][$t->lang] = array(
+                'title' => $data['name'],
+                'body' => $data['body'],
+            );
         }
 
         include STAFFINC_DIR . 'templates/content-manage.tmpl.php';
@@ -159,6 +168,8 @@ class ContentAjaxAPI extends AjaxController {
         $langs = $cfg->getSecondaryLanguages();
 
         $content = Page::lookupByType($type, $lang);
+        $info = $content->getHashtable();
+
         include STAFFINC_DIR . 'templates/content-manage.tmpl.php';
     }
 
@@ -167,19 +178,26 @@ class ContentAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
-        elseif (!$_POST['name'] || !$_POST['body'])
-            Http::response(422, 'Please submit name and body');
         elseif (!($content = Page::lookup($id)))
             Http::response(404, 'No such content');
 
+        if (!isset($_POST['body']))
+            $_POST['body'] = '';
+
         $vars = array_merge($content->getHashtable(), $_POST);
         $errors = array();
-        if (!$content->update($vars, $errors)) {
-            if ($errors['err'])
-                Http::response(422, $errors['err']);
-            else
-                Http::response(500, 'Unable to update content: '.print_r($errors, true));
+
+        // Allow empty content for the staff banner
+        if ($content->update($vars, $errors,
+            $content->getType() == 'banner-staff')
+        ) {
+            Http::response(201, 'Have a great day!');
         }
+        if (!$errors['err'])
+            $errors['err'] = __('Correct the error(s) below and try again!');
+        $info = $_POST;
+        $errors = Format::htmlchars($errors);
+        include STAFFINC_DIR . 'templates/content-manage.tmpl.php';
     }
 }
 ?>
diff --git a/include/ajax.sequence.php b/include/ajax.sequence.php
index 37be03269c87483a77e8d99cc7aaf840ffaeae34..299e8c3223d7a1d8262cc40c8919ac44fbde534f 100644
--- a/include/ajax.sequence.php
+++ b/include/ajax.sequence.php
@@ -33,7 +33,7 @@ class SequenceAjaxAPI extends AjaxController {
         elseif (!($sequence = Sequence::lookup($id)))
             Http::response(404, 'No such object');
 
-        return $sequence->current($_GET['format']);
+        return $sequence->current(Format::htmlchars($_GET['format']));
     }
 
     /**
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 9e416bef6a3961dcc549ff8381ed6bc93c2003ce..28ed7d2b4fbfcd6de7af37038c6321d02dc3e628 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -413,7 +413,7 @@ class TicketsAjaxAPI extends AjaxController {
             Http::response(404, 'No such ticket/user');
 
         $errors = array();
-        if($user->updateInfo($_POST, $errors))
+        if($user->updateInfo($_POST, $errors, true))
              Http::response(201, $user->to_json());
 
         $forms = $user->getForms();
diff --git a/include/class.config.php b/include/class.config.php
index 7ddc58855f0b68cb5f56f98ad893c43222e79e85..b7eae5da0e851d57f8766f1dc3ceaf44451b68a2 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -202,6 +202,13 @@ class OsticketConfig extends Config {
     }
 
     function isKnowledgebaseEnabled() {
+        global $thisclient;
+
+        if ($this->get('restrict_kb', false)
+            && (!$thisclient || $thisclient->isGuest())
+        ) {
+            return false;
+        }
         require_once(INCLUDE_DIR.'class.faq.php');
         return ($this->get('enable_kb') && FAQ::countPublishedFAQs());
     }
@@ -1168,11 +1175,16 @@ class OsticketConfig extends Config {
 
     function updateKBSettings($vars, &$errors) {
 
-        if($errors) return false;
+        if ($vars['restrict_kb'] && !$this->isClientRegistrationEnabled())
+            $errors['restrict_kb'] =
+                __('The knowledge base cannot be restricted unless client registration is enabled');
+
+        if ($errors) return false;
 
         return $this->updateAll(array(
             'enable_kb'=>isset($vars['enable_kb'])?1:0,
-               'enable_premade'=>isset($vars['enable_premade'])?1:0,
+            'restrict_kb'=>isset($vars['restrict_kb'])?1:0,
+            'enable_premade'=>isset($vars['enable_premade'])?1:0,
         ));
     }
 
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index 3c8289899b0b036a2dbb4cccf1c9354564d07cce..2efcbec3c2fca60c77f19d49c60a30c7557ee415 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -52,18 +52,17 @@ class DynamicForm extends VerySimpleModel {
     var $_dfields;
 
     function getFields($cache=true) {
-        if (!$cache)
-            $fields = false;
-        else
-            $fields = &$this->_fields;
+        if (!$cache) {
+            $this->_fields = null;
+        }
 
-        if (!$fields) {
-            $fields = new ListObject();
+        if (!$this->_fields) {
+            $this->_fields = new ListObject();
             foreach ($this->getDynamicFields() as $f)
-                $fields->append($f->getImpl($f));
+                $this->_fields->append($f->getImpl($f));
         }
 
-        return $fields;
+        return $this->_fields;
     }
 
     function getDynamicFields() {
@@ -104,13 +103,39 @@ class DynamicForm extends VerySimpleModel {
     function getTitle() { return $this->getLocal('title'); }
     function getInstructions() { return $this->getLocal('instructions'); }
 
+    /**
+     * Drop field errors clean info etc. Useful when replacing the source
+     * content of the form. This is necessary because the field listing is
+     * cached under some circumstances.
+     */
+    function reset() {
+        foreach ($this->getFields() as $f)
+            $f->reset();
+        return $this;
+    }
+
     function getForm($source=false) {
-        if (!$this->_form || $source) {
-            $fields = $this->getFields($this->_has_data);
-            $this->_form = new Form($fields, $source, array(
-                'title'=>$this->getLocal('title'), 'instructions'=>$this->getLocal('instructions')));
+        if ($source)
+            $this->reset();
+        $fields = $this->getFields();
+        $form = new Form($fields, $source, array(
+            'title'=>$this->getLocal('title'), 'instructions'=>$this->getLocal('instructions')));
+        return $form;
+    }
+
+    function addErrors(array $formErrors, $replace=false) {
+        $fields = array();
+        foreach ($this->getFields() as $f)
+            $fields[$f->get('id')] = $f;
+        foreach ($formErrors as $id => $fieldErrors) {
+            if (isset($fields[$id])) {
+                if ($replace)
+                    $fields[$id]->_errors = $fieldErrors;
+                else
+                    foreach ($fieldErrors as $E)
+                        $fields[$id]->addError($E);
+            }
         }
-        return $this->_form;
     }
 
     function isDeletable() {
@@ -414,8 +439,7 @@ class TicketForm extends DynamicForm {
             return;
 
         $f = $answer->getField();
-        $name = $f->get('name') ? $f->get('name')
-            : 'field_'.$f->get('id');
+        $name = $f->get('name') ?: ('field_'.$f->get('id'));
         $fields = sprintf('`%s`=', $name) . db_input(
             implode(',', $answer->getSearchKeys()));
         $sql = 'INSERT INTO `'.TABLE_PREFIX.'ticket__cdata` SET '.$fields
@@ -867,26 +891,40 @@ class DynamicFormEntry extends VerySimpleModel {
     function getInstructions() { return $this->getForm()->getInstructions(); }
 
     function getForm() {
-        if (!isset($this->_form)) {
-            $this->_form = DynamicForm::lookup($this->get('form_id'));
-            if ($this->_form && isset($this->id))
-                $this->_form->data($this);
+        $form = DynamicForm::lookup($this->get('form_id'));
+        if ($form) {
+            if (isset($this->id))
+                $form->data($this);
             if (isset($this->extra)) {
                 $x = JsonDataParser::decode($this->extra) ?: array();
-                $this->_form->disableFields($x['disable'] ?: array());
+                $form->disableFields($x['disable'] ?: array());
             }
+            if ($this->errors())
+                $form->addErrors($this->errors(), true);
         }
-        return $this->_form;
+        return $form;
     }
 
     function getFields() {
         if (!isset($this->_fields)) {
             $this->_fields = array();
+            // Get all dynamic fields associated with the form
+            //  even when stored elsewhere -- important during validation
+            foreach ($this->getForm()->getDynamicFields() as $field) {
+                $field = $field->getImpl($field);
+                if ($field instanceof ThreadEntryField)
+                    continue;
+                $this->_fields[$field->get('id')] = $field;
+            }
+            // Get answers to entries
             foreach ($this->getAnswers() as $a) {
-                $T = $this->_fields[] = $a->getField();
-                $T->setForm($this);
+                if (!($f = $a->getField())) continue;
+                $this->_fields[$f->get('id')] = $f;
             }
         }
+        foreach ($this->_fields as $F)
+            $F->setForm($this);
+
         return $this->_fields;
     }
 
@@ -1057,7 +1095,7 @@ class DynamicFormEntry extends VerySimpleModel {
                 $a->deleted = false;
                 // Add to list of answers
                 $this->_values[] = $a;
-                $this->_fields[] = $fImpl;
+                $this->_fields[$field->get('id')] = $fImpl;
                 $this->_form = null;
 
                 // Omit fields without data and non-storable fields.
@@ -1066,21 +1104,25 @@ class DynamicFormEntry extends VerySimpleModel {
 
                 $a->save();
             }
-            // Sort the form the way it is declared to be sorted
-            if ($this->_fields)
-                usort($this->_fields,
-                    function($a, $b) {
-                        return $a->get('sort') - $b->get('sort');
-                });
+        }
+
+        // Sort the form the way it is declared to be sorted
+        if ($this->_fields) {
+            uasort($this->_fields,
+                function($a, $b) {
+                    return $a->get('sort') - $b->get('sort');
+            });
         }
     }
 
-    function save() {
+    function save($refetch=false) {
         if (count($this->dirty))
             $this->set('updated', new SqlFunction('NOW'));
-        parent::save();
+        if (!parent::save($refetch || count($this->dirty)))
+            return false;
+
         foreach ($this->getFields() as $field) {
-            if (!$field->isStorable())
+            if (!($a = $field->getAnswer()) || !$field->isStorable())
                 continue;
 
             $a = $field->getAnswer();
diff --git a/include/class.export.php b/include/class.export.php
index 288cb68480c0d850f57e91cb81938eae9fc4aa88..3210d362553eff74b002806ad6436aa39f46846a 100644
--- a/include/class.export.php
+++ b/include/class.export.php
@@ -290,6 +290,7 @@ class CsvResultsExporter extends ResultSetExporter {
         if (!$this->output)
              $this->output = fopen('php://output', 'w');
 
+        fputs($this->output, chr(0xEF) . chr(0xBB) . chr(0xBF));
         fputcsv($this->output, $this->getHeaders());
         while ($row=$this->next())
             fputcsv($this->output, $row);
diff --git a/include/class.filter.php b/include/class.filter.php
index 9647481cc3cbb9e4e07c9204bc728e848351be9c..ada35a25994684acda91afb84421b7090adc1206 100644
--- a/include/class.filter.php
+++ b/include/class.filter.php
@@ -438,7 +438,7 @@ class Filter {
 
                 else //for everything-else...we assume it's valid.
                     $rules[]=array('what'=>$vars["rule_w$i"],
-                        'how'=>$vars["rule_h$i"],'val'=>$vars["rule_v$i"]);
+                        'how'=>$vars["rule_h$i"],'val'=>trim($vars["rule_v$i"]));
             }elseif($vars["rule_v$i"]) {
                 $errors["rule_$i"]=__('Incomplete selection');
             }
@@ -919,7 +919,18 @@ class RejectedException extends Exception {
     }
 }
 
-class FilterDataChanged extends Exception {}
+class FilterDataChanged extends Exception {
+    var $data;
+
+    function __construct($data) {
+         parent::__construct('Ticket filter data changed');
+         $this->data = $data;
+    }
+
+    function getData() {
+        return $this->data;
+    }
+}
 
 /**
  * Function: endsWith
diff --git a/include/class.filter_action.php b/include/class.filter_action.php
index f2367bc77b1f63e5a1d5eeb11a6d7b35af3b2013..531b997e3008354a0a1bc84f25d62121f1bdf50c 100644
--- a/include/class.filter_action.php
+++ b/include/class.filter_action.php
@@ -189,11 +189,15 @@ class FA_UseReplyTo extends TriggerAction {
 
     function apply(&$ticket, array $info) {
         $config = $this->getConfiguration();
-        if ($config['enable'] && $info['reply-to']) {
+        $changed = $info['reply-to'] != $ticket['email']
+            || ($info['reply-to-name'] && $ticket['name'] != $info['reply-to-name']);
+        if ($info['reply-to']) {
             $ticket['email'] = $info['reply-to'];
             if ($info['reply-to-name'])
                 $ticket['name'] = $info['reply-to-name'];
         }
+        if ($changed)
+            throw new FilterDataChanged($ticket);
     }
 
     function getConfigurationOptions() {
diff --git a/include/class.format.php b/include/class.format.php
index 7b00d4adb277432b0c1a0801dbba691beb9a2696..61613331e0346da6a16f07f44f6c648e986bce92 100644
--- a/include/class.format.php
+++ b/include/class.format.php
@@ -690,7 +690,7 @@ class Format {
         // Drop leading and trailing whitespace
         $text = trim($text);
 
-        if (class_exists('IntlBreakIterator')) {
+        if (false && class_exists('IntlBreakIterator')) {
             // Split by word boundaries
             if ($tokenizer = IntlBreakIterator::createWordInstance(
                     $lang ?: ($cfg ? $cfg->getPrimaryLanguage() : 'en_US'))
diff --git a/include/class.forms.php b/include/class.forms.php
index 0f89971ec862f548d026beff74155422e94e93bb..d65136a569aac97045ba5e578da18cc9ee883b1e 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -32,7 +32,7 @@ class Form {
         $this->fields = $fields;
         foreach ($fields as $k=>$f) {
             $f->setForm($this);
-            if (!$f->get('name') && $k)
+            if (!$f->get('name') && $k && !is_numeric($k))
                 $f->set('name', $k);
         }
         if (isset($options['title']))
@@ -761,6 +761,17 @@ class FormField {
         return null;
     }
 
+    /**
+     * Indicates if the field provides for searching for something other
+     * than keywords. For instance, textbox fields can have hits by keyword
+     * searches alone, but selection fields should provide the option to
+     * match a specific value or set of values and therefore need to
+     * participate on any search builder.
+     */
+    function hasSpecialSearch() {
+        return true;
+    }
+
     function getConfigurationForm($source=null) {
         if (!$this->_cform) {
             $type = static::getFieldType($this->get('type'));
@@ -869,6 +880,10 @@ class TextboxField extends FormField {
         );
     }
 
+    function hasSpecialSearch() {
+        return false;
+    }
+
     function validateEntry($value) {
         parent::validateEntry($value);
         $config = $this->getConfiguration();
@@ -940,6 +955,10 @@ class TextareaField extends FormField {
         );
     }
 
+    function hasSpecialSearch() {
+        return false;
+    }
+
     function display($value) {
         $config = $this->getConfiguration();
         if ($config['html'])
@@ -992,6 +1011,10 @@ class PhoneField extends FormField {
         );
     }
 
+    function hasSpecialSearch() {
+        return false;
+    }
+
     function validateEntry($value) {
         parent::validateEntry($value);
         $config = $this->getConfiguration();
@@ -1137,7 +1160,7 @@ class ChoiceField extends FormField {
         $config = $this->getConfiguration();
         if (!$config['multiselect'] && is_array($value) && count($value) < 2) {
             reset($value);
-            return key($value);
+            $value = key($value);
         }
         return $value;
     }
@@ -1424,6 +1447,9 @@ class ThreadEntryField extends FormField {
     function isPresentationOnly() {
         return true;
     }
+    function hasSpecialSearch() {
+        return false;
+    }
 
     function getConfigurationOptions() {
         global $cfg;
@@ -1461,8 +1487,8 @@ class ThreadEntryField extends FormField {
 }
 
 class PriorityField extends ChoiceField {
-    function getWidget() {
-        $widget = parent::getWidget();
+    function getWidget($widgetClass=false) {
+        $widget = parent::getWidget($widgetClass);
         if ($widget->value instanceof Priority)
             $widget->value = $widget->value->getId();
         return $widget;
@@ -1472,13 +1498,10 @@ class PriorityField extends ChoiceField {
         return true;
     }
 
-    function getChoices() {
-        global $cfg;
-        $this->ht['default'] = $cfg->getDefaultPriorityId();
-
+    function getChoices($verbose=false) {
         $sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE
               .' ORDER BY priority_urgency DESC';
-        $choices = array();
+        $choices = array('' => '— '.__('Default').' —');
         if (!($res = db_query($sql)))
             return $choices;
 
@@ -1496,7 +1519,10 @@ class PriorityField extends ChoiceField {
             reset($id);
             $id = key($id);
         }
-        return Priority::lookup($id);
+        elseif ($id === false)
+            $id = $value;
+        if ($id)
+            return Priority::lookup($id);
     }
 
     function to_database($prio) {
@@ -1515,14 +1541,31 @@ class PriorityField extends ChoiceField {
     }
 
     function getConfigurationOptions() {
+        $choices = $this->getChoices();
+        $choices[''] = __('System Default');
         return array(
             'prompt' => new TextboxField(array(
                 'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
                 'hint'=>__('Leading text shown before a value is selected'),
                 'configuration'=>array('size'=>40, 'length'=>40),
             )),
+            'default' => new ChoiceField(array(
+                'id'=>3, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
+                'choices' => $choices,
+                'hint'=>__('Default selection for this field'),
+                'configuration'=>array('size'=>20, 'length'=>40),
+            )),
         );
     }
+
+    function getConfiguration() {
+        global $cfg;
+
+        $config = parent::getConfiguration();
+        if (!isset($config['default']))
+            $config['default'] = $cfg->getDefaultPriorityId();
+        return $config;
+    }
 }
 FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
     return array(
@@ -1696,7 +1739,7 @@ class TicketStateField extends ChoiceField {
         return false;
     }
 
-    function getChoices() {
+    function getChoices($verbose=false) {
         static $_choices;
 
         if (!isset($_choices)) {
@@ -1780,7 +1823,7 @@ class TicketFlagField extends ChoiceField {
         return true;
     }
 
-    function getChoices() {
+    function getChoices($verbose=false) {
         $this->ht['default'] =  '';
 
         if (!$this->_choices) {
@@ -1878,6 +1921,10 @@ class FileUploadField extends FormField {
         );
     }
 
+    function hasSpecialSearch() {
+        return false;
+    }
+
     /**
      * Called from the ajax handler for async uploads via web clients.
      */
@@ -2448,9 +2495,12 @@ class ChoicesWidget extends Widget {
     }
 
     function getValue() {
-        $value = parent::getValue();
 
-        if (!$value) return null;
+        if (!($value = parent::getValue()))
+            return null;
+
+        if ($value && !is_array($value))
+            $value = array($value);
 
         // Assume multiselect
         $values = array();
@@ -2461,6 +2511,7 @@ class ChoicesWidget extends Widget {
                     $values[$v] = $choices[$v];
             }
         }
+
         return $values;
     }
 
@@ -2493,8 +2544,11 @@ class CheckboxWidget extends Widget {
 
     function getValue() {
         $data = $this->field->getSource();
-        if (count($data))
+        if (count($data)) {
+            if (!isset($data[$this->name]))
+                return false;
             return @in_array($this->field->get('id'), $data[$this->name]);
+        }
         return parent::getValue();
     }
 
diff --git a/include/class.mailer.php b/include/class.mailer.php
index b5d759cb43801c662bac5a96049cf0a402984772..4c822f725f9b80ace27d7a42b67508cbbe57bd52 100644
--- a/include/class.mailer.php
+++ b/include/class.mailer.php
@@ -349,6 +349,7 @@ class Mailer {
         }
 
         // Make the best effort to add In-Reply-To and References headers
+        $reply_tag = $mid_token = '';
         if (isset($options['thread'])
             && $options['thread'] instanceof ThreadEntry
         ) {
@@ -367,6 +368,12 @@ class Mailer {
                     'References' => $parent->getEmailReferences(),
                 );
             }
+
+            // Configure the reply tag and embedded message id token
+            $mid_token = $options['thread']->asMessageId($to);
+            if ($cfg && $cfg->stripQuotedReply()
+                    && (!isset($options['reply-tag']) || $options['reply-tag']))
+                $reply_tag = $cfg->getReplySeparator() . '<br/><br/>';
         }
 
         // Use Mail_mime default initially
@@ -395,13 +402,12 @@ class Mailer {
         // body
         $isHtml = true;
         if (!(isset($options['text']) && $options['text'])) {
-            $tag = '';
-            if ($cfg && $cfg->stripQuotedReply()
-                    && (!isset($options['reply-tag']) || $options['reply-tag']))
-                $tag = '<div>'.$cfg->getReplySeparator() . '<br/><br/></div>';
             // Embed the data-mid in such a way that it should be included
             // in a response
-            $message = "<div data-mid=\"$messageId\">{$tag}{$message}</div>";
+            if ($reply_tag || $mid_token) {
+                $message = "<div style=\"display:none\"
+                    data-mid=\"$mid_token\">$reply_tag</div>$message";
+            }
             $txtbody = rtrim(Format::html2text($message, 90, false))
                 . ($messageId ? "\nRef-Mid: $messageId\n" : '');
             $mime->setTXTBody($txtbody);
diff --git a/include/class.nav.php b/include/class.nav.php
index 92d08996a2bcd6d2076955021b36fc570c2e7dda..6dee08171b05fbf4c7fb7c6eb7658980b89c641a 100644
--- a/include/class.nav.php
+++ b/include/class.nav.php
@@ -116,7 +116,7 @@ class StaffNav {
         if(!$this->tabs) {
             $this->tabs = array();
             $this->tabs['dashboard'] = array(
-                'desc'=>__('Dashboard'),'href'=>'dashboard.php','title'=>__('Agent Dashboard')
+                'desc'=>__('Dashboard'),'href'=>'dashboard.php','title'=>__('Agent Dashboard'), "class"=>"no-pjax"
             );
             if ($thisstaff->getRole()->hasPerm(User::PERM_DIRECTORY)) {
                 $this->tabs['users'] = array(
diff --git a/include/class.page.php b/include/class.page.php
index e55df9d728484beb9fa0661a6e6bce5688e91ab1..1aaa8ce3d8c8e0f934bd95e15f402e58fd74763b 100644
--- a/include/class.page.php
+++ b/include/class.page.php
@@ -242,7 +242,7 @@ class Page extends VerySimpleModel {
         }
     }
 
-    function update($vars, &$errors) {
+    function update($vars, &$errors, $allowempty=false) {
 
         //Cleanup.
         $vars['name']=Format::striptags(trim($vars['name']));
@@ -264,7 +264,7 @@ class Page extends VerySimpleModel {
         elseif(($pid=self::getIdByName($vars['name'])) && $pid!=$this->getId())
             $errors['name'] = __('Name already exists');
 
-        if(!$vars['body'])
+        if(!$vars['body'] && !$allowempty)
             $errors['body'] = __('Page body is required');
 
         if($errors) return false;
diff --git a/include/class.search.php b/include/class.search.php
index 355feb0725ce8b0fea9d9f5e1ba46c269dc312e0..6bbc7a3657a53e61399812d0ff5b182e44c1c30f 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -223,19 +223,37 @@ class SearchInterface {
     }
 }
 
+require_once(INCLUDE_DIR.'class.config.php');
+class MySqlSearchConfig extends Config {
+    var $table = CONFIG_TABLE;
+
+    function __construct() {
+        parent::Config("mysqlsearch");
+    }
+}
+
 class MysqlSearchBackend extends SearchBackend {
     static $id = 'mysql';
     static $BATCH_SIZE = 30;
 
     // Only index 20 batches per cron run
     var $max_batches = 60;
+    var $_reindexed = 0;
 
     function __construct() {
         $this->SEARCH_TABLE = TABLE_PREFIX . '_search';
     }
 
+    function getConfig() {
+        if (!isset($this->config))
+            $this->config = new MySqlSearchConfig();
+        return $this->config;
+    }
+
+
     function bootstrap() {
-        Signal::connect('cron', array($this, 'IndexOldStuff'));
+        if ($this->getConfig()->get('reindex', true))
+            Signal::connect('cron', array($this, 'IndexOldStuff'));
     }
 
     function update($model, $id, $content, $new=false, $attrs=array()) {
@@ -497,7 +515,10 @@ class MysqlSearchBackend extends SearchBackend {
         // FILES ------------------------------------
 
         // Flush non-full batch of records
-        $this->__index(null, true);
+        if (!$this->_reindexed) {
+            // Stop rebuilding the index
+            $this->getConfig()->set('reindex', 0);
+        }
     }
 
     function __index($record, $force_flush=false) {
@@ -517,9 +538,10 @@ class MysqlSearchBackend extends SearchBackend {
 
         $sql = 'INSERT INTO `'.TABLE_PREFIX.'_search` (`object_type`, `object_id`, `title`, `content`)
             VALUES '.implode(',', $queue);
-        if (!db_query($sql) || count($queue) != db_affected_rows())
+        if (!db_query($sql, false) || count($queue) != db_affected_rows())
             throw new Exception('Unable to index content');
 
+        $this->_reindexed += count($queue);
         $queue = array();
 
         if (!--$this->max_batches)
diff --git a/include/class.thread.php b/include/class.thread.php
index 72f0d9a197b3c49fb23ef60ba1d1f8ae2554a176..b537436d58a2c6bc6fc727f7321f1996e3cd5861 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -234,7 +234,13 @@ class Thread extends VerySimpleModel {
             //XXX: Are we potentially leaking the email address to
             // collaborators?
             // Try not to destroy the format of the body
-            $body->prepend(sprintf('Received From: %s', $mailinfo['email']));
+            $header = sprintf("Received From: %s <%s>\n\n", $mailinfo['name'],
+                $mailinfo['email']);
+            if ($body instanceof HtmlThreadBody)
+                $header = nl2br(Format::htmlchars($header));
+            // Add the banner to the top of the message
+            if ($body instanceof ThreadBody)
+                $body->prepend($header);
             $vars['message'] = $body;
             $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner?
             $vars['origin'] = 'Email';
@@ -510,6 +516,33 @@ class ThreadEntry extends VerySimpleModel {
         return $references;
     }
 
+    /**
+     * Retrieve a list of all the recients of this message if the message
+     * was received via email.
+     *
+     * Returns:
+     * (array<RFC_822>) list of recipients parsed with the Mail/RFC822
+     * address parsing utility. Returns an empty array if the message was
+     * not received via email.
+     */
+    function getAllEmailRecipients() {
+        $headers = self::getEmailHeaderArray();
+        $recipients = array();
+        if (!$headers)
+            return $recipients;
+
+        foreach (array('To', 'Cc') as $H) {
+            if (!isset($headers[$H]))
+                continue;
+
+            if (!($all = Mail_Parse::parseAddressList($headers[$H])))
+                continue;
+
+            $recipients = array_merge($recipients, $all);
+        }
+        return $recipients;
+    }
+
     function getUIDFromEmailReference($ref) {
 
         $info = unpack('Vtid/Vuid',
@@ -979,7 +1012,7 @@ class ThreadEntry extends VerySimpleModel {
             return false;
 
         // Compute the value to be compared from $mails (which used to be in
-        // ThreadEntry::asMessageId()
+        // ThreadEntry::asMessageId() (#nolint)
         $domain = md5($ost->getConfig()->getURL());
         $ticket = $entry->getThread()->getObject();
         if (!$ticket instanceof Ticket)
@@ -1280,6 +1313,14 @@ class ThreadEntryBody /* extends SplString */ {
         return $this->display('html');
     }
 
+    function prepend($what) {
+        $this->body = $what . $this->body;
+    }
+
+    function append($what) {
+        $this->body .= $what;
+    }
+
     function asVar() {
         // Email template, assume HTML
         return $this->display('email');
diff --git a/include/class.ticket.php b/include/class.ticket.php
index c5f364de9b756bbbb9e9db142ae1a4bbc3b3514e..cfc167ae9e9311783649cea00304b9c8d39fa259 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -1211,6 +1211,7 @@ implements RestrictedAccess, Threadable {
                 $sql.=', closed=NOW(), lastupdate=NOW(), duedate=NULL ';
                 if ($thisstaff && $set_closing_agent)
                     $sql.=', staff_id='.db_input($thisstaff->getId());
+                $this->clearOverdue();
 
                 $ecb = function($t) {
                     $t->reload();
@@ -1475,15 +1476,24 @@ implements RestrictedAccess, Threadable {
             return;
 
         //Who posted the entry?
-        $uid = 0;
+        $skip = array();
         if ($entry instanceof Message) {
             $poster = $entry->getUser();
             // Skip the person who sent in the message
-            $uid = $entry->getUserId();
+            $skip[$entry->getUserId()] = 1;
+            // Skip all the other recipients of the message
+            foreach ($entry->getAllEmailRecipients() as $R) {
+                foreach ($recipients as $R2) {
+                    if ($R2->getEmail() == ($R->mailbox.'@'.$R->hostname)) {
+                        $skip[$R2->getUserId()] = true;
+                        break;
+                    }
+                }
+            }
         } else {
             $poster = $entry->getStaff();
             // Skip the ticket owner
-            $uid = $this->getUserId();
+            $skip[$this->getUserId()] = 1;
         }
 
         $vars = array_merge($vars, array(
@@ -1498,7 +1508,10 @@ implements RestrictedAccess, Threadable {
         $options = array('inreplyto' => $entry->getEmailMessageId(),
                          'thread' => $entry);
         foreach ($recipients as $recipient) {
-            if ($uid == $recipient->getUserId()) continue;
+            // Skip folks who have already been included on this part of
+            // the conversation
+            if (isset($skip[$recipient->getUserId()]))
+                continue;
             $notice = $this->replaceVars($msg, array('recipient' => $recipient));
             $email->send($recipient, $notice['subj'], $notice['body'], $attachments,
                 $options);
@@ -2091,8 +2104,7 @@ implements RestrictedAccess, Threadable {
                 $recipients[]=$this->getLastRespondent();
 
             //Assigned staff if any...could be the last respondent
-
-            if ($this->isAssigned()) {
+            if ($cfg->alertAssignedONNewMessage() && $this->isAssigned()) {
                 if ($staff = $this->getStaff())
                     $recipients[] = $staff;
                 elseif ($team = $this->getTeam())
@@ -2196,6 +2208,9 @@ implements RestrictedAccess, Threadable {
         if(!$vars['staffId'] && $thisstaff)
             $vars['staffId'] = $thisstaff->getId();
 
+        if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
+            $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
+
         if(!($response = $this->getThread()->addResponse($vars, $errors)))
             return null;
 
@@ -2319,6 +2334,8 @@ implements RestrictedAccess, Threadable {
         elseif (!isset($vars['poster'])) {
             $vars['poster'] = 'SYSTEM';
         }
+        if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
+            $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
 
         if(!($note=$this->getThread()->addNote($vars, $errors)))
             return null;
@@ -2450,6 +2467,11 @@ implements RestrictedAccess, Threadable {
 
         $this->deleteDrafts();
 
+        $sql = 'DELETE FROM '.TICKET_TABLE.'__cdata WHERE `ticket_id`='
+            .db_input($this->getId());
+        // If the CDATA table doesn't exist, that's not an error
+        db_query($sql, false);
+
         // Log delete
         $log = sprintf(__('Ticket #%1$s deleted by %2$s'),
                 $this->getNumber(),
@@ -2778,7 +2800,7 @@ implements RestrictedAccess, Threadable {
         }
         catch (FilterDataChanged $ex) {
             // Don't pass user recursively, assume the user has changed
-            return self::filterTicketData($origin, $vars, $forms);
+            return self::filterTicketData($origin, $ex->getData(), $forms);
         }
         return $vars;
     }
diff --git a/include/class.user.php b/include/class.user.php
index 0ad21e055023ae751ebe044f54cb78068c1d9ad3..194b659a34777039b1484928c745f415310ce1bb 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -378,7 +378,7 @@ class User extends UserModel {
                     }
                 }
 
-                $this->_forms[] = $cd->getForm();
+                $this->_forms[] = $cd;
             }
         }
 
@@ -541,15 +541,15 @@ class User extends UserModel {
     function updateInfo($vars, &$errors, $staff=false) {
 
         $valid = true;
-        $forms = $this->getDynamicData();
+        $forms = $this->getForms($vars);
         foreach ($forms as $cd) {
             $cd->setSource($vars);
             if ($staff && !$cd->isValidForStaff())
                 $valid = false;
-            elseif (!$cd->isValidForClient())
+            elseif (!$staff && !$cd->isValidForClient())
                 $valid = false;
-            elseif ($cd->get('type') == 'U'
-                        && ($form= $cd->getForm())
+            elseif (($form= $cd->getForm())
+                        && $form->get('type') == 'U'
                         && ($f=$form->getField('email'))
                         && $f->getClean()
                         && ($u=User::lookup(array('emails__address'=>$f->getClean())))
@@ -562,7 +562,7 @@ class User extends UserModel {
         if (!$valid)
             return false;
 
-        foreach ($this->getDynamicData() as $cd) {
+        foreach ($forms as $cd) {
             if (($f=$cd->getForm()) && $f->get('type') == 'U') {
                 if (($name = $f->getField('name'))) {
                     $this->name = $name->getClean();
diff --git a/include/client/login.inc.php b/include/client/login.inc.php
index 2b688ee2de4b9f69b545d4ed89dbc57ab26170ba..5c6413f9b212dbc6d199859f3d2cdf70dccb129b 100644
--- a/include/client/login.inc.php
+++ b/include/client/login.inc.php
@@ -56,7 +56,7 @@ if ($cfg && $cfg->isClientRegistrationEnabled()) {
 <?php } ?>
     <div>
     <b><?php echo __("I'm an agent"); ?></b> —
-    <a href="<?php echo ROOT_PATH; ?>scp"><?php echo __('sign in here'); ?></a>
+    <a href="<?php echo ROOT_PATH; ?>scp/"><?php echo __('sign in here'); ?></a>
     </div>
     </div>
 </div>
diff --git a/include/i18n/en_US/help/tips/settings.autoresponder.yaml b/include/i18n/en_US/help/tips/settings.autoresponder.yaml
index 35faf7ae2f1558dd8b193c207fd56c41b2bf5a8f..c77da28ec34c30ca405a44dd15ce6fc7760f0389 100644
--- a/include/i18n/en_US/help/tips/settings.autoresponder.yaml
+++ b/include/i18n/en_US/help/tips/settings.autoresponder.yaml
@@ -58,4 +58,4 @@ overlimit_notice:
         href: /scp/templates.php?default_for=ticket.overlimit
 
       - title: Set <em>Maximum Open Tickets</em>
-        href: /scp/settings?t=tickets
+        href: /scp/settings.php?t=tickets
diff --git a/include/i18n/en_US/help/tips/settings.kb.yaml b/include/i18n/en_US/help/tips/settings.kb.yaml
index 12f4954725fd56d5befbda7ab26276487f7d1746..702dac9d573a5bf3094321069db57c20dc511331 100644
--- a/include/i18n/en_US/help/tips/settings.kb.yaml
+++ b/include/i18n/en_US/help/tips/settings.kb.yaml
@@ -28,6 +28,15 @@ knowledge_base_status:
       - title: Manage Knowledge Base
         href: /scp/kb.php
 
+restrict_kb:
+    title: Resctrict Access to the Knowledge Base
+    content: >
+        Enable this setting to prevent unregistered users from accessing
+        your knowledge base articles on the client interface.
+    links:
+      - title: Access Control Settings
+        href: /scp/settings.php?t=access
+
 canned_responses:
     title: Canned Responses
     content: >
diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php
index 443c51c00e525c74bf82569d63d79636a4ac8b75..7941d6fa4631a866d6aafdc3b23f0e557a7b1f96 100644
--- a/include/staff/departments.inc.php
+++ b/include/staff/departments.inc.php
@@ -56,7 +56,7 @@ $showing = $pageNav->showing().' '._N('department', 'departments', $count);
             <th width="7px">&nbsp;</th>
             <th width="200"><a <?php echo $name_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
             <th width="80"><a  <?php echo $type_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=type"><?php echo __('Type');?></a></th>
-            <th width="70"><a  <?php echo $members_sort; ?>href="departments.php?<?php echo $qstr; ?>&sort=members"><?php echo __('Members');?></a></th>
+            <th width="70"><a  <?php echo $users_sort; ?>href="departments.php?<?php echo $qstr; ?>&sort=users"><?php echo __('Agents');?></a></th>
             <th width="300"><a  <?php echo $email_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=email"><?php echo __('Email Address');?></a></th>
             <th width="180"><a  <?php echo $manager_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=manager"><?php echo __('Manager');?></a></th>
         </tr>
diff --git a/include/staff/filters.inc.php b/include/staff/filters.inc.php
index ce55919ca6274bd0e039cb641d5da886616731a6..8e1c4946b60d30f41907aebf9ea78fcca6dd32d9 100644
--- a/include/staff/filters.inc.php
+++ b/include/staff/filters.inc.php
@@ -49,7 +49,7 @@ else
  <h2><?php echo __('Ticket Filters');?></h2>
 </div>
 <div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
- <b><a href="filters.php?a=add" class="Icon newEmailFilter"><?php echo __('Add New Filter');?></a></b></div>
+ <b><a href="filters.php?a=add" class="Icon newTicketFilter"><?php echo __('Add New Filter');?></a></b></div>
 <div class="clear"></div>
 <form action="filters.php" method="POST" name="filters">
  <?php csrf_token(); ?>
diff --git a/include/staff/pwreset.login.php b/include/staff/pwreset.login.php
index 29670e28f6a1deed29d69fd37887c9f326dd48f7..54d57b62ca32c2f904c2ab75528d098cb59fcdea 100644
--- a/include/staff/pwreset.login.php
+++ b/include/staff/pwreset.login.php
@@ -5,7 +5,10 @@ $info = ($_POST)?Format::htmlchars($_POST):array();
 ?>
 
 <div id="loginBox">
-    <h1 id="logo"><a href="index.php">osTicket <?php echo __('Agent Password Reset'); ?></a></h1>
+    <h1 id="logo"><a href="index.php">
+        <span class="valign-helper"></span>
+        <img src="logo.php?login" alt="osTicket :: <?php echo __('Agent Password Reset');?>" />
+    </a></h1>
     <h3><?php echo Format::htmlchars($msg); ?></h3>
 
     <form action="pwreset.php" method="post">
diff --git a/include/staff/pwreset.php b/include/staff/pwreset.php
index 22157a36cb563a50d9421ec88eef736dbb14dd76..93f8e9bb1ae7800eb9f17e98420e71a96ac52562 100644
--- a/include/staff/pwreset.php
+++ b/include/staff/pwreset.php
@@ -5,7 +5,10 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
 ?>
 
 <div id="loginBox">
-    <h1 id="logo"><a href="index.php">osTicket <?php echo __('Agent Password Reset'); ?></a></h1>
+    <h1 id="logo"><a href="index.php">
+        <span class="valign-helper"></span>
+        <img src="logo.php?login" alt="osTicket :: <?php echo __('Agent Password Reset');?>" />
+    </a></h1>
     <h3><?php echo Format::htmlchars($msg); ?></h3>
     <form action="pwreset.php" method="post">
         <?php csrf_token(); ?>
diff --git a/include/staff/pwreset.sent.php b/include/staff/pwreset.sent.php
index 2825c0c584872354e48bd58010fdd770bb268563..b0a46d8d1ff7fa58e756b6b33833960b5992e8d2 100644
--- a/include/staff/pwreset.sent.php
+++ b/include/staff/pwreset.sent.php
@@ -5,7 +5,10 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
 ?>
 
 <div id="loginBox">
-    <h1 id="logo"><a href="index.php">osTicket <?php echo __('Agent Password Reset'); ?></a></h1>
+    <h1 id="logo"><a href="index.php">
+        <span class="valign-helper"></span>
+        <img src="logo.php?login" alt="osTicket :: <?php echo __('Agent Password Reset');?>" />
+    </a></h1>
     <h3><?php echo __('A confirmation email has been sent'); ?></h3>
     <h3 style="color:black;"><em><?php echo __(
     'A password reset email was sent to the email on file for your account.  Follow the link in the email to reset your password.'
diff --git a/include/staff/settings-access.inc.php b/include/staff/settings-access.inc.php
index 5b79ae43f44ec71c41f41dc033c9b6f8af95f6b4..5efac16cf87fe95a258cd6fd3b181d31856507eb 100644
--- a/include/staff/settings-access.inc.php
+++ b/include/staff/settings-access.inc.php
@@ -172,9 +172,14 @@ $manage_content = function($title, $content) use ($contents) {
     <i class="icon-file-text pull-left icon-2x" style="color:#bbb;margin:0 -36px"></i>
     <a href="#ajax.php/content/<?php echo $id; ?>/manage"
     onclick="javascript:
-        $.dialog($(this).attr('href').substr(1), 200);
-    return false;">
-<?php
+        $.dialog($(this).attr('href').substr(1), 201);
+    return false;" class="pull-left"><i class="icon-file-text icon-2x"
+        style="color:#bbb;"></i> </a>
+    <span style="display:inline-block;width:90%;padding-left:10px;line-height:1.2em">
+    <a href="#ajax.php/content/<?php echo $id; ?>/manage"
+    onclick="javascript:
+        $.dialog($(this).attr('href').substr(1), 201);
+    return false;"><?php
     echo Format::htmlchars($title); ?></a><br/>
         <span class="faded"><?php
         echo Format::display($notes); ?>
diff --git a/include/staff/settings-kb.inc.php b/include/staff/settings-kb.inc.php
index cfac97418f92f1156c0c6022911dc4aaa3bd0310..941b08e30fed4e3ca89c6023ce472ae4a452288f 100644
--- a/include/staff/settings-kb.inc.php
+++ b/include/staff/settings-kb.inc.php
@@ -16,12 +16,16 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config)
     </thead>
     <tbody>
         <tr>
-            <td width="180"><?php echo __('Knowledge Base Status'); ?>:</td>
+            <td width="180" valign="top"><?php echo __('Knowledge Base Status'); ?>:</td>
             <td>
                 <input type="checkbox" name="enable_kb" value="1" <?php echo $config['enable_kb']?'checked="checked"':''; ?>>
                 <?php echo __('Enable Knowledge Base'); ?>
-                &nbsp;<font class="error">&nbsp;<?php echo $errors['enable_kb']; ?></font>
                 <i class="help-tip icon-question-sign" href="#knowledge_base_status"></i>
+                <div class="error"><?php echo $errors['enable_kb']; ?></div>
+                <input type="checkbox" name="restrict_kb" value="1" <?php echo $config['restrict_kb']?'checked="checked"':''; ?> >
+                <?php echo __('Require Client Login'); ?>
+                <i class="help-tip icon-question-sign" href="#restrict_kb"></i>
+                <div class="error"><?php echo $errors['restrict_kb']; ?></div>
             </td>
         </tr>
         <tr>
diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php
index 1da74cc1aa0648324cc1003ad6003b9ce82454e3..e1e8fb461b01334a541d727c1948c86dab392e19 100644
--- a/include/staff/staff.inc.php
+++ b/include/staff/staff.inc.php
@@ -17,7 +17,7 @@ if($staff && $_REQUEST['a']!='add'){
     $title=__('Add New Agent');
     $action='create';
     $submit_text=__('Add Agent');
-    $passwd_text=__('Temporary password required only for "Local" authenication');
+    $passwd_text=__('Temporary password required only for "Local" authentication');
     //Some defaults for new staff.
     $info['change_passwd']=1;
     $info['welcome_email']=1;
diff --git a/include/staff/templates/content-manage.tmpl.php b/include/staff/templates/content-manage.tmpl.php
index f10927634df62ded7e925d613d931a2347c3f96b..28a016cb71163b256e32a819b8446b832766a60f 100644
--- a/include/staff/templates/content-manage.tmpl.php
+++ b/include/staff/templates/content-manage.tmpl.php
@@ -2,6 +2,11 @@
 <a class="close" href=""><i class="icon-remove-circle"></i></a>
 <hr/>
 
+<?php if ($errors['err']) { ?>
+<div class="error-banner">
+    <?php echo $errors['err']; ?>
+</div>
+<?php } ?>
 <form method="post" action="#content/<?php echo $content->getId(); ?>"
         style="clear:none">
 <?php
@@ -20,29 +25,32 @@ if (count($langs) > 1) { ?>
 } ?>
     <div id="translation-<?php echo $cfg->getPrimaryLanguage(); ?>"
         class="tab_content left-tabs" style="padding:0" lang="<?php echo $cfg->getPrimaryLanguage(); ?>">
+    <div class="error"><?php echo $errors['name']; ?></div>
     <input type="text" style="width: 100%; font-size: 14pt" name="name" value="<?php
-        echo Format::htmlchars($content->getName()); ?>" />
+        echo Format::htmlchars($info['title']); ?>" />
     <div style="margin-top: 5px">
+    <div class="error"><?php echo $errors['body']; ?></div>
     <textarea class="richtext no-bar" name="body"><?php
-    echo Format::htmlchars(Format::viewableImages($content->getBody()));
+    echo Format::htmlchars(Format::viewableImages($info['body']));
 ?></textarea>
     </div>
     </div>
 
 <?php foreach ($langs as $tag=>$nfo) {
         if ($tag == $cfg->getPrimaryLanguage())
-            continue; ?>
+            continue;
+        $trans = $info['trans'][$tag]; ?>
     <div id="translation-<?php echo $tag; ?>" class="tab_content left-tabs"
         style="display:none;padding:0" dir="<?php echo $nfo['direction']; ?>" lang="<?php echo $tag; ?>">
     <input type="text" style="width: 100%; font-size: 14pt"
         name="trans[<?php echo $tag; ?>][title]" value="<?php
-        echo Format::htmlchars($info['title'][$tag]); ?>"
+        echo Format::htmlchars($trans['title']); ?>"
         placeholder="<?php echo __('Title'); ?>" />
     <div style="margin-top: 5px">
     <textarea class="richtext no-bar" data-direction=<?php echo $nfo['direction']; ?>
         placeholder="<?php echo __('Message content'); ?>"
         name="trans[<?php echo $tag; ?>][body]"><?php
-    echo Format::htmlchars(Format::viewableImages($info['body'][$tag]));
+    echo Format::htmlchars(Format::viewableImages($trans['body']));
 ?></textarea>
     </div>
     </div>
diff --git a/include/staff/templates/navigation.tmpl.php b/include/staff/templates/navigation.tmpl.php
index b28ec05261c0c2d669763b66f79258de7bfd01c3..8f0444999c7d0c169acbc599dfbfa69285a2462d 100644
--- a/include/staff/templates/navigation.tmpl.php
+++ b/include/staff/templates/navigation.tmpl.php
@@ -1,7 +1,10 @@
 <?php
 if(($tabs=$nav->getTabs()) && is_array($tabs)){
     foreach($tabs as $name =>$tab) {
-        echo sprintf('<li class="%s"><a href="%s">%s</a>',$tab['active']?'active':'inactive',$tab['href'],$tab['desc']);
+        echo sprintf('<li class="%s %s"><a href="%s">%s</a>',
+            $tab['active'] ? 'active':'inactive',
+            @$tab['class'] ?: '',
+            $tab['href'],$tab['desc']);
         if(!$tab['active'] && ($subnav=$nav->getSubMenu($name))){
             echo "<ul>\n";
             foreach($subnav as $k => $item) {
diff --git a/include/staff/user-view.inc.php b/include/staff/user-view.inc.php
index 9e6fb90faf2f1aeac16daa97a53c5ca0d4325104..eae6c28cb223625efcdb99d813589e1faa19889c 100644
--- a/include/staff/user-view.inc.php
+++ b/include/staff/user-view.inc.php
@@ -110,10 +110,12 @@ if ($thisstaff->getRole()->hasPerm(User::PERM_EDIT)) { ?>
                             if ($org)
                                 echo sprintf('<a href="#users/%d/org" class="user-action">%s</a>',
                                         $user->getId(), $org->getName());
-                            elseif ($thisstaff->getRole()->hasPerm(User::PERM_EDIT)) { ?>
-                                <a href="#users/<?php echo $user->getId(); ?>/org"
-                                class="user-action"><?php echo __('Add Organization'); ?></a>
-<?php                       }
+                            elseif ($thisstaff->getRole()->hasPerm(User::PERM_EDIT)) {
+                                echo sprintf(
+                                    '<a href="#users/%d/org" class="user-action">%s</a>',
+                                    $user->getId(),
+                                    __('Add Organization'));
+                            }
                         ?>
                         </span>
                     </td>
diff --git a/js/filedrop.field.js b/js/filedrop.field.js
index e451028a03fef02fa385f09497178778c125134c..fc99870bdfd5fbe41d7679666344a1bd19ff3f78 100644
--- a/js/filedrop.field.js
+++ b/js/filedrop.field.js
@@ -329,7 +329,8 @@
       globalProgressUpdated: empty,
       speedUpdated: empty
       },
-      errors = ["BrowserNotSupported", "TooManyFiles", "FileTooLarge", "FileTypeNotAllowed", "NotFound", "NotReadable", "AbortError", "ReadError", "FileExtensionNotAllowed"];
+      errors = ["BrowserNotSupported", "TooManyFiles", "FileTooLarge", "FileTypeNotAllowed", "NotFound", "NotReadable", "AbortError", "ReadError", "FileExtensionNotAllowed"],
+      Blob = window.WebKitBlob || window.MozBlob || window.Blob;
 
   $.fn.filedrop = function(options) {
     var opts = $.extend({}, default_opts, options),
@@ -379,8 +380,7 @@
       var dashdash = '--',
           crlf = '\r\n',
           builder = [],
-          paramname = opts.paramname,
-          Blob = window.WebKitBlob || window.Blob;
+          paramname = opts.paramname;
 
       if (opts.data) {
         var params = $.param(opts.data).replace(/\+/g, '%20').split(/&/);
@@ -476,6 +476,10 @@
         opts.error(errors[0]);
         return false;
       }
+      if (typeof Blob === "undefined") {
+        opts.error(errors[0]);
+        return false;
+      }
 
       if (opts.allowedfiletypes.push && opts.allowedfiletypes.length) {
         for(var fileIndex = files.length;fileIndex--;) {
diff --git a/scp/css/login.css b/scp/css/login.css
index aef7b3a789b44c4d55bc0e7df0965cc7f1bc2247..a5a682f08d04b513b643b044f817416988895d56 100644
--- a/scp/css/login.css
+++ b/scp/css/login.css
@@ -79,6 +79,8 @@ h1 {
     height: auto;
     width: auto;
     vertical-align: middle;
+    outline: none;
+    border: none;
 }
 .valign-helper {
     height: 100%;
diff --git a/scp/css/scp.css b/scp/css/scp.css
index c64d284d8fdf27bf2676339fd90845872d03e918..30b384e1436c824a598c231203d9335700c6aaba 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -133,6 +133,8 @@ div#header a {
     height: auto;
     width: auto;
     vertical-align: middle;
+    outline: none;
+    border: none;
 }
 .valign-helper {
     height: 100%;
diff --git a/scp/helptopics.php b/scp/helptopics.php
index adb1eb7c4fcc8c2830fd2b2e3e72b734a0a25d2b..035b04da810e8ebf2dc5187cc180c64c857e9730 100644
--- a/scp/helptopics.php
+++ b/scp/helptopics.php
@@ -89,7 +89,7 @@ if($_POST){
                         ));
                         if ($num > 0) {
                             if($num==$count)
-                                $msg = sprintf(__('Successfully diabled %s'),
+                                $msg = sprintf(__('Successfully disabled %s'),
                                     _N('selected help topic', 'selected help topics', $count));
                             else
                                 $warn = sprintf(__('%1$d of %2$d %3$s disabled'), $num, $count,
@@ -106,7 +106,7 @@ if($_POST){
 
                         if($i && $i==$count)
                             $msg = sprintf(__('Successfully deleted %s'),
-                                _N('selected help topic', 'selected elp topics', $count));
+                                _N('selected help topic', 'selected help topics', $count));
                         elseif($i>0)
                             $warn = sprintf(__('%1$d of %2$d %3$s deleted'), $i, $count,
                                 _N('selected help topic', 'selected help topics', $count));
diff --git a/scp/images/ost-logo.png b/scp/images/ost-logo.png
index 33b09d35aa8dfbc935e85e70ee17bc5c69d0567e..90b4c77640396eb95e26521120557a26a5403181 100644
Binary files a/scp/images/ost-logo.png and b/scp/images/ost-logo.png differ
diff --git a/scp/js/dashboard.inc.js b/scp/js/dashboard.inc.js
index c902e4be2d77352909602d2d72b31758909136a8..683135e62c05b10975e6b79c897f0d33dc5557ec 100644
--- a/scp/js/dashboard.inc.js
+++ b/scp/js/dashboard.inc.js
@@ -1,5 +1,5 @@
 (function ($) {
-    var current_tab;
+    var current_tab = null;
     function refresh(e) {
         $('#line-chart-here').empty();
         $('#line-chart-legend').empty();
@@ -135,7 +135,12 @@
             stop = this.period.value || 'now';
         }
 
+        if (!current_tab)
+            current_tab = $('#tabular-navigation li:first-child a');
+
         var group = current_tab.attr('table-group');
+        var pagesize = 25;
+        getConfig().then(function(c) { if (c.page_size) pagesize = c.page_size; });
         $.ajax({
             method:     'GET',
             dataType:   'json',
@@ -144,7 +149,6 @@
             success:    function(json) {
                 var q = $('<table>').attr({'class':'table table-condensed table-striped'}),
                     h = $('<tr>').appendTo($('<thead>').appendTo(q)),
-                    pagesize = 25,
                     max = [];
                 for (var c in json.columns) {
                     h.append($('<th>').append(json.columns[c]));
@@ -158,7 +162,7 @@
                 }
                 for (var i in json.data) {
                     if (i % pagesize === 0)
-                        b = $('<tbody>').attr({'page':i/pagesize+1}).appendTo(q);
+                        b = $('<tbody>').attr({'page':i/pagesize+1}).addClass('hidden').appendTo(q);
                     row = json.data[i];
                     tr = $('<tr>').appendTo(b);
                     for (var j in row) {
@@ -194,30 +198,31 @@
                         $('<td>').attr('colspan','8').append(
                             'No data for this timeframe found'))).appendTo(q);
                 }
+                $('tbody[page=1]', q).removeClass('hidden');
                 $('#table-here').empty().append(q);
 
                 // ----------------------> Pagination <---------------------
                 function goabs(e) {
-                    $('tbody', q).addClass('hide');
+                    $('tbody', q).addClass('hidden');
                     if (e.target) {
                         page = e.target.text;
-                        $('tbody[page='+page+']', q).removeClass('hide');
+                        $('tbody[page='+page+']', q).removeClass('hidden');
                     } else {
-                        e.removeClass('hide');
+                        e.removeClass('hidden');
                         page = e.attr('page')
                     }
-                    enable_next_prev(page);
+                    return enable_next_prev(page);
                 }
                 function goprev() {
-                    current = $('tbody:not(.hide)', q).attr('page');
+                    current = $('tbody:not(.hidden)', q).attr('page');
                     page = Math.max(1, parseInt(current) - 1);
-                    goabs($('tbody[page='+page+']', q));
+                    return goabs($('tbody[page='+page+']', q));
                 }
                 function gonext() {
-                    current = $('tbody:not(.hide)', q).attr('page');
+                    current = $('tbody:not(.hidden)', q).attr('page');
                     page = Math.min(Math.floor(json.data.length / pagesize) + 1,
                         parseInt(current) + 1);
-                    goabs($('tbody[page='+page+']', q));
+                    return goabs($('tbody[page='+page+']', q));
                 }
                 function enable_next_prev(page) {
                     $('#table-here div.pagination li[page]').removeClass('active');
@@ -229,6 +234,7 @@
                     if (page == Math.floor(json.data.length / pagesize) + 1)
                                     $('#report-page-next').addClass('disabled');
                     else            $('#report-page-next').removeClass('disabled');
+                    return false;
                 }
 
                 var p = $('<ul>')
@@ -254,15 +260,16 @@
                     .appendTo($('<li>')
                     .appendTo(p));
 
-                gonext();
+                goprev();
             }
         });
         return false;
     }
-   
-    $(function() { 
-        $('#timeframe-form').submit(refresh);
+
+    $(function() {
+        var form = $('#timeframe-form');
+        form.submit(refresh);
         //Trigger submit now...init.
-        $('#timeframe-form').submit(); 
-        });
+        form.submit();
+    });
 })(window.jQuery);
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 79d14e3da1ecd8c5f06d16ad2f0630d890666ba7..9e1b5d714419856c0b3ebd434701a5265bc7b8ff 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -386,6 +386,7 @@ var scp_prep = function() {
         var fObj = $(this);
         var elem = $('#advanced-search');
         $('#result-count').html('');
+        fixupDatePickers.call(this);
         $.ajax({
                 url: "ajax.php/tickets/search",
                 data: fObj.serialize(),
@@ -450,18 +451,21 @@ var scp_prep = function() {
 
 $(document).ready(scp_prep);
 $(document).on('pjax:end', scp_prep);
-$(document).on('submit', 'form', function() {
+var fixupDatePickers = function() {
     // Reformat dates
     $('.dp', $(this)).each(function(i, e) {
         var $e = $(e),
             d = $e.datepicker('getDate');
-        if (!d) return;
+        if (!d || $e.data('fixed')) return;
         var day = ('0'+d.getDate()).substr(-2),
             month = ('0'+(d.getMonth()+1)).substr(-2),
             year = d.getFullYear();
         $e.val(year+'-'+month+'-'+day);
+        $e.data('fixed', true);
+        $e.on('change', function() { $(this).data('fixed', false); });
     });
-});
+};
+$(document).on('submit', 'form', fixupDatePickers);
 
     /************ global inits *****************/
 
diff --git a/setup/cli/modules/class.module.php b/setup/cli/modules/class.module.php
index e01e507abc8a677ca2534895b18214dc2de0856a..c6af32572dfde210a1014d884d91cff4acf18d9a 100644
--- a/setup/cli/modules/class.module.php
+++ b/setup/cli/modules/class.module.php
@@ -98,11 +98,10 @@ class Option {
 class OutputStream {
     var $stream;
 
-    function OutputStream() {
-        call_user_func_array(array($this, '__construct'), func_get_args());
-    }
     function __construct($stream) {
-        $this->stream = fopen($stream, 'w');
+        if (!($this->stream = fopen($stream, 'w')))
+            throw new Exception(sprintf('%s: Cannot open for writing',
+                $stream));
     }
 
     function write($what) {
diff --git a/setup/cli/modules/file.php b/setup/cli/modules/file.php
index ee83169958c3e25a9c514e979564b7f6b699d588..f5057230ac293cae0b23b0944e0b419aca033369 100644
--- a/setup/cli/modules/file.php
+++ b/setup/cli/modules/file.php
@@ -12,6 +12,7 @@ class FileManager extends Module {
                 'list' => 'List files matching criteria',
                 'export' => 'Export files from the system',
                 'import' => 'Load files exported via `export`',
+                'zip' => 'Create a zip file of the matching files',
                 'dump' => 'Dump file content to stdout',
                 'load' => 'Load file contents from stdin',
                 'migrate' => 'Migrate a file to another backend',
@@ -176,11 +177,224 @@ class FileManager extends Module {
             $this->stdout->write("Migrated $count files\n");
             break;
 
+        /**
+         * export
+         *
+         * Export file contents to a stream file. The format of the stream
+         * will be a continuous stream of file information in the following
+         * format:
+         *
+         * AFIL<meta-length><data-length><meta><data>EOF\x1c
+         *
+         * Where
+         *   A              is the version code of the export
+         *   "FIL"          is the literal text 'FIL'
+         *   meta-length    is 'V' packed header length (bytes)
+         *   data-length    is 'V' packed data length (bytes)
+         *   meta           is the %file record, php serialized
+         *   data           is the raw content of the file
+         *   "EOF"          is the literal text 'EOF'
+         *   \x1c           is an ASCII 0x1c byte (file separator)
+         *
+         * Options:
+         * --file       File to which to direct the stream output, default
+         *              is stdout
+         */
         case 'export':
-            // Create a temporary ZIP file
             $files = FileModel::objects();
             $this->_applyCriteria($options, $files);
 
+            if (!$options['file'] || $options['file'] == '-')
+                $options['file'] = 'php://stdout';
+
+            if (!($stream = fopen($options['file'], 'wb')))
+                $this->fail($options['file'].': Unable to open file for export stream');
+
+            foreach ($files as $m) {
+                $f = AttachmentFile::lookup($m->id);
+                if ($options['verbose'])
+                    $this->stderr->write($m->name."\n");
+
+                // TODO: Log %attachment and %ticket_attachment entries
+                $info = array('file' => $f->getInfo());
+                $header = serialize($info);
+                fwrite($stream, 'AFIL'.pack('VV', strlen($header), $f->getSize()));
+                fwrite($stream, $header);
+                $FS = $f->open();
+                while ($block = $FS->read())
+                    fwrite($stream, $block);
+                fwrite($stream, "EOF\x1c");
+            }
+            fclose($stream);
+            break;
+
+        /**
+         * import
+         *
+         * Import a collection of file contents exported by the `export`.
+         * See the export function above for details about the stream
+         * format.
+         *
+         * Options:
+         * --file       File from which to read the export stream, default
+         *              is stdin
+         * --to         Backend to receive the contents (@see `backends`)
+         * --verbose    Show file names while importing
+         */
+        case 'import':
+            if (!$options['file'] || $options['file'] == '-')
+                $options['file'] = 'php://stdin';
+
+            if (!($stream = fopen($options['file'], 'rb')))
+                $this->fail($options['file'].': Unable to open import stream');
+
+            while (true) {
+                // Read the file header
+                // struct file_data_header {
+                //   char[4] marker; // Four chars, 'AFIL'
+                //   int     lenMeta;
+                //   int     lenData;
+                // };
+                if (!($header = fread($stream, 12)))
+                    break; // EOF
+
+                list(, $mark, $hlen, $dlen) = unpack('V3', $header);
+
+                // AFIL written as little-endian 4-byte int is 0x4c4946xx (LIFA),
+                // where 'A' is the version code of the export
+                $version = $mark & 0xff;
+                if (($mark >> 8) != 0x4c4946)
+                    $this->fail('Bad file record');
+
+                // Read the header
+                $header = fread($stream, $hlen);
+                if (strlen($header) != $hlen)
+                    $this->fail('Short read getting header info');
+
+                $header = unserialize($header);
+                if (!$header)
+                    $this->fail('Unable to decipher file header');
+
+                // Find or create the file record
+                $finfo = $header['file'];
+                // TODO: Consider the $version code
+                $f = AttachmentFile::lookup($finfo['id']);
+                if ($f) {
+                    // Verify file information
+                    if ($f->getSize() != $finfo['size']
+                        || $f->getSignature() != $finfo['signature']
+                    ) {
+                        $this->fail(sprintf(
+                            '%s: File data does not match existing file record',
+                            $finfo['name']
+                        ));
+                    }
+                    // Drop existing file contents, if any
+                    try {
+                        if ($bk = $f->open())
+                            $bk->unlink();
+                    }
+                    catch (Exception $e) {}
+                }
+                // Create a new file
+                else {
+                    $fm = FileModel::create($finfo);
+                    if (!$fm->save() || !($f = AttachmentFile::lookup($fm->id))) {
+                        $this->fail(sprintf(
+                            '%s: Unable to create new file record',
+                            $finfo['name']));
+                    }
+                }
+
+                // Determine the backend to recieve the file contents
+                if ($options['to']) {
+                    $bk = FileStorageBackend::lookup($options['to'], $f);
+                }
+                // Use the system default
+                else {
+                    $bk = AttachmentFile::getBackendForFile($f);
+                }
+
+                if ($options['verbose'])
+                    $this->stdout->write('Importing '.$f->getName()."\n");
+
+                // Write file contents to the backend
+                $md5 = hash_init('md5');
+                $sha1 = hash_init('sha1');
+                $written = 0;
+
+                // Handle exceptions by dropping imported file contents and
+                // then returning the error to the error output stream.
+                try {
+                    while ($dlen > 0) {
+                        $read_size = min($dlen, $bk->getBlockSize());
+                        $contents = '';
+                        // reading from the stream will likely return an amount of
+                        // data different from the backend requested block size. Loop
+                        // until $read_size bytes are recieved.
+                        while ($read_size > 0 && ($block = fread($stream, $read_size))) {
+                            $contents .= $block;
+                            $read_size -= strlen($block);
+                        }
+                        if ($read_size != 0) {
+                            // short read
+                            throw new Exception(sprintf(
+                                '%s: Some contents are missing from the stream',
+                                $f->getName()
+                            ));
+                        }
+                        // Calculate MD5 and SHA1 hashes of the file to verify
+                        // contents after successfully written to backend
+                        if (!$bk->write($contents))
+                            throw new Exception(
+                                'Unable to send file contents to backend');
+                        hash_update($md5, $contents);
+                        hash_update($sha1, $contents);
+                        $dlen -= strlen($contents);
+                        $written += strlen($contents);
+                    }
+                    // Some backends cannot handle flush() without a
+                    // corresponding write() call.
+                    if ($written && !$bk->flush())
+                        throw new Exception(
+                            'Unable to commit file contents to backend');
+
+                    // Check the signature hash
+                    if ($finfo['signature']) {
+                        $md5 = base64_encode(hash_final($md5, true));
+                        $sha1 = base64_encode(hash_final($sha1, true));
+                        $sig = str_replace(
+                            array('=','+','/'),
+                            array('','-','_'),
+                            substr($sha1, 0, 16) . substr($md5, 0, 16));
+                        if ($sig != $finfo['signature']) {
+                            throw new Exception(sprintf(
+                                '%s: Signature verification failed',
+                                $f->getName()
+                            ));
+                        }
+                    }
+                } // end try
+                catch (Exception $ex) {
+                    if ($bk) $bk->unlink();
+                    $this->fail($ex->getMessage());
+                }
+
+                // Read file record footer
+                $footer = fread($stream, 4);
+                if (strlen($footer) != 4)
+                    $this->fail('Unable to read file EOF marker');
+                list(, $footer) = unpack('N', $footer);
+                // Footer should be EOF\x1c as an int
+                if ($footer != 0x454f461c)
+                    $this->fail('Incorrect file EOF marker');
+            }
+            break;
+
+        case 'zip':
+            // Create a temporary ZIP file
+            $files = FileModel::objects();
+            $this->_applyCriteria($options, $files);
             if (!$options['file'])
                 $this->fail('Please specify zip file with `-f`');
 
@@ -189,30 +403,33 @@ class FileManager extends Module {
                     ZipArchive::CREATE)))
                 $this->fail($reason.': Unable to create zip file');
 
-            $manifest = array();
             foreach ($files as $m) {
                 $f = AttachmentFile::lookup($m->id);
-                $zip->addFromString($f->getId(), $f->getData());
-                $zip->setCommentName($f->getId(), $f->getName());
-                // TODO: Log %attachment and %ticket_attachment entries
-                $info = array('file' => $f->getInfo());
-                foreach ($m->tickets as $t)
-                    $info['tickets'][] = $t->ht;
-
-                $manifest[$f->getId()] = $info;
+                if ($options['verbose'])
+                    $this->stderr->write($m->name."\n");
+                $name = Format::encode(sprintf(
+                    '%d-%s', $f->getId(), $f->getName()
+                    ), 'utf-8', 'cp437');
+                $zip->addFromString($name, $f->getData());
             }
-            $zip->addFromString('MANIFEST', serialize($manifest));
             $zip->close();
             break;
 
         case 'expunge':
-            // Create a temporary ZIP file
             $files = FileModel::objects();
             $this->_applyCriteria($options, $files);
 
-            foreach ($files as $f) {
-                $f->tickets->expunge();
-                $f->unlink() && $f->delete();
+            foreach ($files as $m) {
+                // Drop associated attachment links
+                $m->tickets->expunge();
+                $f = AttachmentFile::lookup($m->id);
+
+                // Drop file contents
+                if ($bk = $f->open())
+                    $bk->unlink();
+
+                // Drop file record
+                $f->delete();
             }
         }
     }
diff --git a/setup/cli/modules/i18n.php b/setup/cli/modules/i18n.php
index fca97e411f50560c9e8a4e3d0b5b1d18f026edb5..9d5e6ca4c1fe6af88c246ff7798f3418853e195d 100644
--- a/setup/cli/modules/i18n.php
+++ b/setup/cli/modules/i18n.php
@@ -18,6 +18,10 @@ class i18n_Compiler extends Module {
                 'sign' =>       'Sign a language pack',
             ),
         ),
+        'file(s)' => array(
+            'required' => false,
+            'help' => 'File(s) to be signed, used with `sign`',
+        ),
     );
 
     var $options = array(
@@ -35,8 +39,15 @@ class i18n_Compiler extends Module {
         'domain' => array('-D', '--domain', 'metavar'=>'name',
             'default' => '',
             'help' => 'Add a domain to the path/context of PO strings'),
+        'dns' => array('-d', '--dns', 'default' => false, 'metavar' => 'zone-id',
+            'help' => 'Write signature to DNS (via this AWS HostedZoneId)'),
     );
 
+    var $epilog = "Note: If updating DNS, you will need to set
+        AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the AWS credentials
+        profile in your home folder or in your environment. See AWS
+        configuration  docs for more information";
+
     static $project = 'osticket-official';
     static $crowdin_api_url = 'http://i18n.osticket.com/api/project/{project}/{command}';
 
@@ -96,9 +107,14 @@ class i18n_Compiler extends Module {
             $this->_make_pot($options);
             break;
         case 'sign':
-            if (!$options['file'] || !file_exists($options['file']))
-                $this->fail('Specify a language pack to sign with --file=');
-            $this->_sign($options['file'], $options);
+            if (count($args) < 2)
+                $this->fail('Specify a language pack to sign');
+            foreach (range(1, count($args)-1, 1) as $i) {
+                $plugin = $args[$i];
+                if (!is_file($args[$i]))
+                    $this->fail($args[$i].': No such file');
+                $this->_sign($args[$i], $options);
+            }
             break;
         }
     }
@@ -303,9 +319,50 @@ class i18n_Compiler extends Module {
 
         $this->stdout->write(sprintf("Signature: %s\n",
             strtolower($signature['hash'])));
-        $this->stdout->write(
-            sprintf("Seal: \"v=1; i=%s; s=%s; V=%s;\"\n",
-            $info['Id'], base64_encode($seal), $info['Version']));
+        $seal =
+            sprintf('"v=1; i=%s; s=%s; V=%s;"',
+            $info['Id'], base64_encode($seal), $info['Version']);
+
+        if ($options['dns']) {
+            if (!is_file(INCLUDE_DIR . 'aws.phar'))
+                $this->fail('Unable to include AWS phar file. Download to INCLUDE_DIR');
+            require_once INCLUDE_DIR . 'aws.phar';
+
+            $aws = Aws\Common\Aws::factory(array());
+            $client = $aws->get('Route53');
+
+            try {
+            $resp = $client->changeResourceRecordSets(array(
+                'HostedZoneId' => $options['dns'],
+                'ChangeBatch' => array(
+                    'Changes' => array(
+                        array(
+                            'Action' => 'CREATE',
+                            'ResourceRecordSet' => array(
+                                'Name' => "{$signature['hash']}.updates.osticket.com.",
+                                'Type' => 'TXT',
+                                'TTL' => 172800,
+                                'ResourceRecords' => array(
+                                    array(
+                                        'Value' => $seal,
+                                    ),
+                                ),
+                            ),
+                        ),
+                    ),
+                ),
+            ));
+            $this->stdout->write(sprintf('%s: %s', $resp['ChangeInfo']['Comment'],
+                $resp['ChangeInfo']['Status']));
+            }
+            catch (Exception $ex) {
+                $this->stdout->write("Seal: $seal\n");
+                $this->fail('!! AWS Update Failed: '.$ex->getMessage());
+            }
+        }
+        else {
+            $this->stdout->write("Seal: $seal\n");
+        }
     }
 
     function __read_next_string($tokens) {
diff --git a/setup/test/tests/stubs.php b/setup/test/tests/stubs.php
index dbd057d515db677b957cd3d8345ad26872ecebd8..fa0706313a77a2edde33868231556c017d88b306 100644
--- a/setup/test/tests/stubs.php
+++ b/setup/test/tests/stubs.php
@@ -11,6 +11,7 @@ class mysqli {
     function select_db() {}
     function set_charset() {}
     function autocommit() {}
+    function rollback() {}
 }
 
 class mysqli_stmt {
@@ -122,6 +123,7 @@ class IntlBreakIterator {
 
 class SqlFunction {
     static function NOW() {}
+    static function COALESCE() {}
 }
 
 class SqlExpression {
diff --git a/tickets.php b/tickets.php
index 052762488b814df35147478e39892fa07385cdb5..10f70676a4713d3c7b696f8304a2c6bf74067a70 100644
--- a/tickets.php
+++ b/tickets.php
@@ -88,7 +88,7 @@ if ($_POST && is_object($ticket) && $ticket->getId()) {
                 Draft::deleteForNamespace('ticket.client.' . $ticket->getId());
                 // Drop attachments
                 $attachments->reset();
-                $tform->setSource(array());
+                $attachments->getForm()->setSource(array());
             } else {
                 $errors['err']=__('Unable to post the message. Try again');
             }
@@ -130,6 +130,8 @@ if($ticket && $ticket->checkUserAccess($thisclient)) {
 }
 include(CLIENTINC_DIR.'header.inc.php');
 include(CLIENTINC_DIR.$inc);
+if ($tform instanceof DynamicFormEntry)
+    $tform = $tform->getForm();
 print $tform->getMedia();
 include(CLIENTINC_DIR.'footer.inc.php');
 ?>